feat(services): frontend services runtime and widget rebind
PR 3 of 4 for the runtime service registry change. - Add service + new-shape widget TypeScript types; widgets carry service_id + widget_kind (service-bound) or null (built-in). - Add services API client + TanStack Query hooks; reconcile the widget API client/hooks to the new endpoints (remove sources/types; add builtin kinds). - Add closed frontend service registry (integrations/registry.ts) mirroring the backend, with resolveWidget(widget, services) mapping a widget to its component + refresh interval. - Add ServicePage at /services/:serviceType/:serviceId with config view, empty-on-edit secret inputs + 'set' badges, enable toggle, delete, and the service's widget-kind list. - Register /services/:serviceType/:serviceId in App.tsx. - Reconcile the six widget components to refreshIntervalMs + description props; rewrite WidgetConfigDialog around a service -> widget-kind picker. - Update Dashboard test; add integrations/registry.test.ts. Verification: frontend lint 0 errors, build success, 70 tests passed; backend ruff clean, 222 tests passed.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
BUILTIN_WIDGETS,
|
||||
SERVICE_REGISTRY,
|
||||
getBuiltinBinding,
|
||||
getServiceBinding,
|
||||
resolveWidget,
|
||||
} from "./registry";
|
||||
import type { ServiceInstance, WidgetInstance } from "../types";
|
||||
|
||||
describe("service registry", () => {
|
||||
it("registers the five backend service types", () => {
|
||||
expect(Object.keys(SERVICE_REGISTRY).sort()).toEqual([
|
||||
"grafana",
|
||||
"jellyfin",
|
||||
"nextcloud",
|
||||
"prometheus",
|
||||
"ssh_tasks",
|
||||
]);
|
||||
});
|
||||
|
||||
it("binds widget kinds per service", () => {
|
||||
expect(SERVICE_REGISTRY.grafana.widgets.map((w) => w.kind)).toEqual(["link"]);
|
||||
expect(SERVICE_REGISTRY.ssh_tasks.widgets.map((w) => w.kind)).toEqual([
|
||||
"task_output",
|
||||
]);
|
||||
expect(SERVICE_REGISTRY.nextcloud.widgets).toEqual([]);
|
||||
});
|
||||
|
||||
it("registers the two built-in widget kinds", () => {
|
||||
expect(Object.keys(BUILTIN_WIDGETS).sort()).toEqual(["backups", "static"]);
|
||||
});
|
||||
|
||||
it("resolves a service-bound widget via the services list", () => {
|
||||
const widget: WidgetInstance = {
|
||||
id: "w1",
|
||||
service_id: "s1",
|
||||
widget_kind: "link",
|
||||
title: "Dashboard",
|
||||
config: {},
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
};
|
||||
const services: ServiceInstance[] = [
|
||||
{
|
||||
id: "s1",
|
||||
service_type: "grafana",
|
||||
name: "Grafana",
|
||||
config: { base_url: "https://grafana.example.com" },
|
||||
secrets_set: { api_key: true },
|
||||
enabled: true,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
},
|
||||
];
|
||||
const resolved = resolveWidget(widget, services);
|
||||
expect(resolved).toBeDefined();
|
||||
expect(resolved?.refreshIntervalMs).toBe(0);
|
||||
});
|
||||
|
||||
it("resolves a built-in widget without a service", () => {
|
||||
const widget: WidgetInstance = {
|
||||
id: "w2",
|
||||
service_id: null,
|
||||
widget_kind: "static",
|
||||
title: "Note",
|
||||
config: { text: "hi" },
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
};
|
||||
const resolved = resolveWidget(widget, []);
|
||||
expect(resolved).toBeDefined();
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown widget kind", () => {
|
||||
const widget: WidgetInstance = {
|
||||
id: "w3",
|
||||
service_id: null,
|
||||
widget_kind: "bogus",
|
||||
title: "x",
|
||||
config: {},
|
||||
enabled: true,
|
||||
sort_order: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
};
|
||||
expect(resolveWidget(widget, [])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("lookups return undefined for unknown types", () => {
|
||||
expect(getServiceBinding("nope")).toBeUndefined();
|
||||
expect(getBuiltinBinding("nope")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user