5dad98231f
Add PrometheusWidgetSource._fetch_chart hitting /api/v1/query_range directly (SC-101..104). New shared helpers in widgets/prometheus_range.py: step_for_window (window preset -> step, ~200pts) and normalize_prometheus_matrix (extracted label/dedup rule, retargeted at Prom matrix, robust to malformed data). Chart widget kind moved grafana->prometheus in both registries; GrafanaChartWidget renamed -> PrometheusChartWidget (git mv, recharts body preserved). Grafana binding/service untouched (removed in slice 3). Backend: 298 pytest pass, ruff clean. Frontend: 128 vitest pass, build+lint green.
270 lines
7.0 KiB
TypeScript
270 lines
7.0 KiB
TypeScript
import type { ComponentType } from "react";
|
|
import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget";
|
|
import { BackupsWidget } from "../widgets/BackupsWidget";
|
|
import { GrafanaLinkWidget } from "../widgets/GrafanaLinkWidget";
|
|
import { PrometheusChartWidget } from "../widgets/PrometheusChartWidget";
|
|
import { JellyfinWidget } from "../widgets/JellyfinWidget";
|
|
import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget";
|
|
import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget";
|
|
import { SshTaskWidget } from "../widgets/SshTaskWidget";
|
|
import { StaticWidget } from "../widgets/StaticWidget";
|
|
import type {
|
|
ServiceInstance,
|
|
ServiceTypeInfo,
|
|
WidgetInstance,
|
|
} from "../types";
|
|
|
|
/**
|
|
* Closed frontend registry mirroring the backend service definitions.
|
|
*
|
|
* Each service type maps its widget kinds to a presentational component and a
|
|
* refresh interval. Built-in (service-less) kinds are listed separately.
|
|
*/
|
|
|
|
export interface WidgetComponentProps {
|
|
widget: WidgetInstance;
|
|
refreshIntervalMs: number;
|
|
description?: string;
|
|
}
|
|
|
|
export interface ServiceWidgetBinding {
|
|
kind: string;
|
|
name: string;
|
|
description: string;
|
|
refreshIntervalMs: number;
|
|
defaultConfig: Record<string, unknown>;
|
|
configSchema: Record<string, unknown>;
|
|
component: ComponentType<WidgetComponentProps>;
|
|
}
|
|
|
|
export interface ServiceBinding {
|
|
serviceType: string;
|
|
name: string;
|
|
description: string;
|
|
widgets: ServiceWidgetBinding[];
|
|
}
|
|
|
|
export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
|
alertmanager: {
|
|
serviceType: "alertmanager",
|
|
name: "Alertmanager",
|
|
description: "Alertmanager alerts and status.",
|
|
widgets: [
|
|
{
|
|
kind: "active_alerts",
|
|
name: "Active alerts",
|
|
description: "Firing alerts summary from Alertmanager.",
|
|
refreshIntervalMs: 30_000,
|
|
defaultConfig: {},
|
|
configSchema: {
|
|
type: "object",
|
|
properties: {
|
|
severity_filter: { type: "string" },
|
|
},
|
|
required: [],
|
|
},
|
|
component: AlertmanagerAlertsWidget,
|
|
},
|
|
],
|
|
},
|
|
grafana: {
|
|
serviceType: "grafana",
|
|
name: "Grafana",
|
|
description: "Dashboards, metrics, and logs.",
|
|
widgets: [
|
|
{
|
|
kind: "link",
|
|
name: "Dashboard link",
|
|
description: "Deep-link to a Grafana dashboard or panel.",
|
|
refreshIntervalMs: 0,
|
|
defaultConfig: { dashboard_uid: "" },
|
|
configSchema: {
|
|
type: "object",
|
|
properties: {
|
|
dashboard_uid: { type: "string" },
|
|
panel_id: { type: "integer" },
|
|
},
|
|
required: ["dashboard_uid"],
|
|
},
|
|
component: GrafanaLinkWidget,
|
|
},
|
|
],
|
|
},
|
|
prometheus: {
|
|
serviceType: "prometheus",
|
|
name: "Prometheus",
|
|
description: "Metrics storage and PromQL queries.",
|
|
widgets: [
|
|
{
|
|
kind: "metric",
|
|
name: "Metric",
|
|
description: "Instant query result rendered as a metric.",
|
|
refreshIntervalMs: 30_000,
|
|
defaultConfig: { promql: "" },
|
|
configSchema: {
|
|
type: "object",
|
|
properties: { promql: { type: "string" } },
|
|
required: ["promql"],
|
|
},
|
|
component: PrometheusMetricWidget,
|
|
},
|
|
{
|
|
kind: "chart",
|
|
name: "Chart",
|
|
description: "Multi-series line chart from a PromQL range query.",
|
|
refreshIntervalMs: 60_000,
|
|
defaultConfig: { promql: "", window: "1h" },
|
|
configSchema: {
|
|
type: "object",
|
|
properties: {
|
|
promql: {
|
|
type: "string",
|
|
description: "PromQL range query expression",
|
|
},
|
|
window: {
|
|
type: "string",
|
|
description: "Time window preset (1h, 6h, 24h, 7d)",
|
|
},
|
|
},
|
|
required: ["promql"],
|
|
},
|
|
component: PrometheusChartWidget,
|
|
},
|
|
],
|
|
},
|
|
jellyfin: {
|
|
serviceType: "jellyfin",
|
|
name: "Jellyfin",
|
|
description: "Media server with live session activity.",
|
|
widgets: [
|
|
{
|
|
kind: "activity",
|
|
name: "Activity",
|
|
description: "Live sessions and idle users.",
|
|
refreshIntervalMs: 30_000,
|
|
defaultConfig: {},
|
|
configSchema: { type: "object", properties: {}, required: [] },
|
|
component: JellyfinWidget,
|
|
},
|
|
{
|
|
kind: "now_playing",
|
|
name: "Now Playing",
|
|
description: "Only sessions actively playing media.",
|
|
refreshIntervalMs: 30_000,
|
|
defaultConfig: {},
|
|
configSchema: { type: "object", properties: {}, required: [] },
|
|
component: JellyfinNowPlayingWidget,
|
|
},
|
|
],
|
|
},
|
|
nextcloud: {
|
|
serviceType: "nextcloud",
|
|
name: "Nextcloud",
|
|
description: "Self-hosted files and collaboration.",
|
|
widgets: [],
|
|
},
|
|
ssh_tasks: {
|
|
serviceType: "ssh_tasks",
|
|
name: "SSH task runner",
|
|
description: "Run reusable saved tasks over SSH and keep run history.",
|
|
widgets: [
|
|
{
|
|
kind: "task_output",
|
|
name: "Task output",
|
|
description: "Output of a saved task run.",
|
|
refreshIntervalMs: 0,
|
|
defaultConfig: { task_id: "" },
|
|
configSchema: {
|
|
type: "object",
|
|
properties: { task_id: { type: "string" } },
|
|
required: ["task_id"],
|
|
},
|
|
component: SshTaskWidget,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
export const BUILTIN_WIDGETS: Record<string, ServiceWidgetBinding> = {
|
|
backups: {
|
|
kind: "backups",
|
|
name: "Backups",
|
|
description: "Backup job summary and active alerts.",
|
|
refreshIntervalMs: 60_000,
|
|
defaultConfig: {},
|
|
configSchema: { type: "object", properties: {}, required: [] },
|
|
component: BackupsWidget,
|
|
},
|
|
static: {
|
|
kind: "static",
|
|
name: "Static text",
|
|
description: "Plain text or markdown note.",
|
|
refreshIntervalMs: 0,
|
|
defaultConfig: { text: "" },
|
|
configSchema: {
|
|
type: "object",
|
|
properties: { text: { type: "string" } },
|
|
required: ["text"],
|
|
},
|
|
component: StaticWidget,
|
|
},
|
|
};
|
|
|
|
export function getServiceBinding(
|
|
serviceType: string,
|
|
): ServiceBinding | undefined {
|
|
return SERVICE_REGISTRY[serviceType];
|
|
}
|
|
|
|
export function getBuiltinBinding(
|
|
kind: string,
|
|
): ServiceWidgetBinding | undefined {
|
|
return BUILTIN_WIDGETS[kind];
|
|
}
|
|
|
|
export interface ResolvedWidget {
|
|
component: ComponentType<WidgetComponentProps>;
|
|
description: string;
|
|
refreshIntervalMs: number;
|
|
}
|
|
|
|
/**
|
|
* Resolve a widget instance to its component + metadata.
|
|
*
|
|
* Service-bound widgets are resolved via the parent service's type (looked up
|
|
* from the services list); built-in widgets are resolved directly.
|
|
*/
|
|
export function resolveWidget(
|
|
widget: WidgetInstance,
|
|
services: ServiceInstance[],
|
|
): ResolvedWidget | undefined {
|
|
if (widget.service_id) {
|
|
const service = services.find((s) => s.id === widget.service_id);
|
|
if (!service) return undefined;
|
|
const binding = getServiceBinding(service.service_type);
|
|
const widgetBinding = binding?.widgets.find(
|
|
(w) => w.kind === widget.widget_kind,
|
|
);
|
|
if (!widgetBinding) return undefined;
|
|
return {
|
|
component: widgetBinding.component,
|
|
description: widgetBinding.description,
|
|
refreshIntervalMs: widgetBinding.refreshIntervalMs,
|
|
};
|
|
}
|
|
const builtin = getBuiltinBinding(widget.widget_kind);
|
|
if (!builtin) return undefined;
|
|
return {
|
|
component: builtin.component,
|
|
description: builtin.description,
|
|
refreshIntervalMs: builtin.refreshIntervalMs,
|
|
};
|
|
}
|
|
|
|
/** Merge backend type metadata (config_schema, secret_fields) onto bindings. */
|
|
export function enrichServiceTypes(
|
|
types: ServiceTypeInfo[],
|
|
): ServiceTypeInfo[] {
|
|
return types;
|
|
}
|