mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
experimenting with config history
This commit is contained in:
@@ -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<Key extends ModuleKey = ModuleKey> = {
|
||||
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<any>,
|
||||
cb: () => Promise<ConfigUpdate>,
|
||||
|
||||
@@ -159,7 +159,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
)}
|
||||
|
||||
{Object.entries(row).map(([key, value], index) => (
|
||||
<td key={index} onClick={rowClick}>
|
||||
<td key={index} onClick={rowClick} valign="top">
|
||||
<div className="flex flex-row items-start py-3 px-3.5 font-normal ">
|
||||
<CellRender property={key} value={value} />
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/history" nest component={SettingsHistory} />
|
||||
|
||||
<SettingRoutesRoutes />
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Switch>
|
||||
<Route path="/" component={SettingsHistoryList} />
|
||||
<Route path="/:id" component={SettingsHistoryDetail} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsHistoryList() {
|
||||
const [history, setHistory] = useState<any[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<AppShell.SectionHeader>History</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-col flex-grow p-3 gap-1">
|
||||
<DataTable
|
||||
data={history}
|
||||
renderValue={renderValue}
|
||||
perPage={50}
|
||||
onClickRow={onClickRow}
|
||||
/>
|
||||
</div>
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={twMerge("flex gap-1", wrap ? "flex-col items-start" : "flex-row")}>
|
||||
{items.map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
||||
>
|
||||
{p}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderValue = ({ value, property }) => {
|
||||
if (property === "timestamp") {
|
||||
return <span>{new Date(value).toLocaleString()}</span>;
|
||||
}
|
||||
|
||||
if (property === "actions") {
|
||||
return (
|
||||
<Labels
|
||||
items={value.map((a) => {
|
||||
switch (a) {
|
||||
case "a":
|
||||
return "Add";
|
||||
case "r":
|
||||
return "Remove";
|
||||
case "e":
|
||||
return "Edit";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (property === "paths") {
|
||||
return <Labels wrap items={value.map((p) => p.join("."))} />;
|
||||
}
|
||||
|
||||
return <CellValue value={value} property={property} />;
|
||||
};
|
||||
|
||||
function SettingsHistoryDetail() {
|
||||
const { id } = useParams();
|
||||
const [item, setItem] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/system/config/history/${id}`)
|
||||
.then((res) => res.json())
|
||||
.then(setItem);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppShell.SectionHeader className="pl-3">
|
||||
<Breadcrumbs2 path={[{ label: "History", href: "/" }, { label: `#${id}` }]} />
|
||||
</AppShell.SectionHeader>
|
||||
<AppShell.Scrollable>
|
||||
<div className="flex flex-col flex-grow p-3 gap-1">
|
||||
{item?.json?.map((item, i) => (
|
||||
<DiffItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</AppShell.Scrollable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const DiffItem = ({ item }: { item: any }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 w-full border-b border-muted p-3">
|
||||
<div className="flex flex-row gap-1 w-full">
|
||||
<span className="font-mono">{item.t}</span>
|
||||
<span className="font-mono">{item.p.join(".")}</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 w-full">
|
||||
<JsonViewer json={item.o} expand={10} className="w-1/2" title="Old" />
|
||||
<JsonViewer json={item.n} expand={10} className="w-1/2" title="New" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user