diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 8db32e42..df798845 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -32,6 +32,7 @@ import { getVersion } from "core/env"; import type { Module } from "modules/Module"; import { getSystemMcp } from "modules/mcp/system-mcp"; import type { DbModuleManager } from "modules/db/DbModuleManager"; +import type { Repository } from "data/entities"; export type ConfigUpdate = { success: true; @@ -129,6 +130,25 @@ export class SystemController extends Controller { }, ); + hono.get("/history", async (c) => { + // @ts-expect-error "repo" is private + const repo = manager.repo() as Repository; + const { data } = await repo.findMany({ + where: { type: "diff", version: manager.version() }, + sort: { by: "id", dir: "desc" }, + }); + return c.json(data); + }); + + hono.get("/history/:id", async (c) => { + // @ts-expect-error "repo" is private + const repo = manager.repo() as Repository; + const { data } = await repo.findId(c.req.param("id"), { + where: { type: "diff", version: manager.version() }, + }); + return c.json(data); + }); + async function handleConfigUpdateResponse( c: Context, cb: () => Promise, diff --git a/app/src/ui/components/table/DataTable.tsx b/app/src/ui/components/table/DataTable.tsx index 2ce2a42f..f5c211cb 100644 --- a/app/src/ui/components/table/DataTable.tsx +++ b/app/src/ui/components/table/DataTable.tsx @@ -159,7 +159,7 @@ export function DataTable = Record )} {Object.entries(row).map(([key, value], index) => ( - +
diff --git a/app/src/ui/routes/settings/index.tsx b/app/src/ui/routes/settings/index.tsx index 3cbfee55..e852d18d 100644 --- a/app/src/ui/routes/settings/index.tsx +++ b/app/src/ui/routes/settings/index.tsx @@ -13,6 +13,7 @@ import { DataSettings } from "./routes/data.settings"; import { FlowsSettings } from "./routes/flows.settings"; import { ServerSettings } from "./routes/server.settings"; import { IconButton } from "ui/components/buttons/IconButton"; +import { SettingsHistory } from "ui/routes/settings/routes/history"; function SettingsSidebar() { const { version, schema, actions, app } = useBknd(); @@ -75,6 +76,7 @@ export default function SettingsRoutes() { /> )} /> + diff --git a/app/src/ui/routes/settings/routes/history.tsx b/app/src/ui/routes/settings/routes/history.tsx new file mode 100644 index 00000000..ded9fea1 --- /dev/null +++ b/app/src/ui/routes/settings/routes/history.tsx @@ -0,0 +1,152 @@ +import { AppShell } from "ui/layouts/AppShell"; +import { Route, Switch, useParams } from "wouter"; +import { useEffect, useState } from "react"; +import { CellValue, DataTable } from "ui/components/table/DataTable"; +import { twMerge } from "tailwind-merge"; +import { JsonViewer } from "ui/components/code/JsonViewer"; +import { useNavigate } from "ui/lib/routes"; +import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; + +export function SettingsHistory() { + return ( + + + + + ); +} + +function SettingsHistoryList() { + const [history, setHistory] = useState([]); + const [navigate] = useNavigate(); + + useEffect(() => { + fetch("/api/system/config/history") + .then((res) => res.json()) + .then((data: any) => + data.map((item) => ({ + id: item.id, + timestamp: item.created_at, + actions: Array.from(new Set(item.json.map((j) => j.t))), + paths: Array.from(new Set(item.json.map((j) => j.p))), + })), + ) + .then(setHistory); + }, []); + + const onClickRow = (row) => { + navigate(`/${row.id}`); + }; + + return ( + <> + History + +
+ +
+
+ + ); +} + +const Labels = ({ + items = [], + max = 3, + wrap = false, +}: { items: string[]; max?: number; wrap?: boolean }) => { + const count = items.length; + if (count > max) { + items = [...items.slice(0, max), `+${count - max}`]; + } + + return ( +
+ {items.map((p, i) => ( + + {p} + + ))} +
+ ); +}; + +const renderValue = ({ value, property }) => { + if (property === "timestamp") { + return {new Date(value).toLocaleString()}; + } + + if (property === "actions") { + return ( + { + switch (a) { + case "a": + return "Add"; + case "r": + return "Remove"; + case "e": + return "Edit"; + default: + return "Unknown"; + } + })} + /> + ); + } + + if (property === "paths") { + return p.join("."))} />; + } + + return ; +}; + +function SettingsHistoryDetail() { + const { id } = useParams(); + const [item, setItem] = useState(); + + useEffect(() => { + fetch(`/api/system/config/history/${id}`) + .then((res) => res.json()) + .then(setItem); + }, []); + + return ( + <> + + + + +
+ {item?.json?.map((item, i) => ( + + ))} +
+
+ + ); +} + +const DiffItem = ({ item }: { item: any }) => { + return ( +
+
+ {item.t} + {item.p.join(".")} +
+
+ + +
+
+ ); +};