feat(widgets): add frontend widget runtime (types, API, hooks, registry, components)
PR 3 of 4 for configurable dashboard widgets. - Add TypeScript widget interfaces (WidgetInstance, WidgetInstanceInput, WidgetTypeInfo, WidgetDataResponse). - Create widget API client for CRUD, registry metadata, and per-widget data. - Create TanStack Query hooks for instances, data, sources, types, and mutations. - Create closed frontend widget registry with metadata, source type, refresh intervals, and config fields. - Add six shadcn/ui-based widget components: Jellyfin, Backups, Grafana link, Prometheus metric, SSH task output, and static text. - Add Vitest unit tests for registry metadata. Verification: - backend ruff clean; pytest 200 passed - frontend npm run lint: 0 errors - frontend npm run build: success - frontend npm run test -- src/widgets/registry.test.ts: 3 passed
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import type { ComponentType } from "react";
|
||||
import type { WidgetInstance } from "../types";
|
||||
import { BackupsWidget } from "./BackupsWidget";
|
||||
import { GrafanaLinkWidget } from "./GrafanaLinkWidget";
|
||||
import { JellyfinWidget } from "./JellyfinWidget";
|
||||
import { PrometheusMetricWidget } from "./PrometheusMetricWidget";
|
||||
import { SshTaskWidget } from "./SshTaskWidget";
|
||||
import { StaticWidget } from "./StaticWidget";
|
||||
|
||||
export interface WidgetConfigField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "string" | "select" | "boolean" | "number";
|
||||
options?: { label: string; value: string }[];
|
||||
helper?: string;
|
||||
}
|
||||
|
||||
export interface WidgetDefinition {
|
||||
widgetType: string;
|
||||
addonId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sourceType: string;
|
||||
refreshInterval: number;
|
||||
defaultConfig: Record<string, unknown>;
|
||||
configFields: WidgetConfigField[];
|
||||
component: ComponentType<{ widget: WidgetInstance }>;
|
||||
}
|
||||
|
||||
export const WIDGET_REGISTRY: Record<string, WidgetDefinition> = {
|
||||
jellyfin: {
|
||||
widgetType: "jellyfin",
|
||||
addonId: "core",
|
||||
name: "Jellyfin activity",
|
||||
description: "Live sessions and idle users from a Jellyfin server.",
|
||||
sourceType: "jellyfin",
|
||||
refreshInterval: 30_000,
|
||||
defaultConfig: { machine_id: "" },
|
||||
configFields: [
|
||||
{
|
||||
key: "machine_id",
|
||||
label: "Machine ID",
|
||||
type: "string",
|
||||
helper: "Jellyfin machine id (empty = default)",
|
||||
},
|
||||
],
|
||||
component: JellyfinWidget,
|
||||
},
|
||||
backups: {
|
||||
widgetType: "backups",
|
||||
addonId: "backups",
|
||||
name: "Backups",
|
||||
description: "Backup job summary and active alerts.",
|
||||
sourceType: "backups",
|
||||
refreshInterval: 60_000,
|
||||
defaultConfig: {},
|
||||
configFields: [],
|
||||
component: BackupsWidget,
|
||||
},
|
||||
"grafana-link": {
|
||||
widgetType: "grafana-link",
|
||||
addonId: "grafana",
|
||||
name: "Grafana link",
|
||||
description: "Deep-link to a Grafana dashboard or panel.",
|
||||
sourceType: "grafana",
|
||||
refreshInterval: 0,
|
||||
defaultConfig: { dashboard_uid: "" },
|
||||
configFields: [
|
||||
{ key: "dashboard_uid", label: "Dashboard UID", type: "string" },
|
||||
{
|
||||
key: "panel_id",
|
||||
label: "Panel ID",
|
||||
type: "number",
|
||||
helper: "Optional",
|
||||
},
|
||||
],
|
||||
component: GrafanaLinkWidget,
|
||||
},
|
||||
"prometheus-metric": {
|
||||
widgetType: "prometheus-metric",
|
||||
addonId: "prometheus",
|
||||
name: "Prometheus metric",
|
||||
description: "Instant query result rendered as a metric.",
|
||||
sourceType: "prometheus",
|
||||
refreshInterval: 30_000,
|
||||
defaultConfig: { promql: "" },
|
||||
configFields: [
|
||||
{ key: "promql", label: "PromQL query", type: "string" },
|
||||
],
|
||||
component: PrometheusMetricWidget,
|
||||
},
|
||||
"ssh-task": {
|
||||
widgetType: "ssh-task",
|
||||
addonId: "ssh-tasks",
|
||||
name: "SSH task output",
|
||||
description: "Output of a saved task run on a machine.",
|
||||
sourceType: "ssh_task",
|
||||
refreshInterval: 0,
|
||||
defaultConfig: { task_id: "" },
|
||||
configFields: [
|
||||
{ key: "task_id", label: "Saved task ID", type: "string" },
|
||||
],
|
||||
component: SshTaskWidget,
|
||||
},
|
||||
static: {
|
||||
widgetType: "static",
|
||||
addonId: "core",
|
||||
name: "Static text",
|
||||
description: "Plain text or markdown note.",
|
||||
sourceType: "static",
|
||||
refreshInterval: 0,
|
||||
defaultConfig: { text: "" },
|
||||
configFields: [{ key: "text", label: "Text", type: "string" }],
|
||||
component: StaticWidget,
|
||||
},
|
||||
};
|
||||
|
||||
export function getWidgetDefinition(
|
||||
widgetType: string,
|
||||
): WidgetDefinition | undefined {
|
||||
return WIDGET_REGISTRY[widgetType];
|
||||
}
|
||||
|
||||
export function listWidgetTypes(): WidgetDefinition[] {
|
||||
return Object.values(WIDGET_REGISTRY);
|
||||
}
|
||||
Reference in New Issue
Block a user