style(services): apply formatter to frontend services runtime
This commit is contained in:
@@ -452,7 +452,10 @@ function AppInner() {
|
|||||||
<Route path="/observability" element={<ObservabilityPage />} />
|
<Route path="/observability" element={<ObservabilityPage />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/addons/:addonId" element={<AddonPage />} />
|
<Route path="/addons/:addonId" element={<AddonPage />} />
|
||||||
<Route path="/services/:serviceType/:serviceId" element={<ServicePage />} />
|
<Route
|
||||||
|
path="/services/:serviceType/:serviceId"
|
||||||
|
element={<ServicePage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
@@ -485,7 +488,10 @@ function AppInner() {
|
|||||||
<Route path="/observability" element={<ObservabilityPage />} />
|
<Route path="/observability" element={<ObservabilityPage />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/addons/:addonId" element={<AddonPage />} />
|
<Route path="/addons/:addonId" element={<AddonPage />} />
|
||||||
<Route path="/services/:serviceType/:serviceId" element={<ServicePage />} />
|
<Route
|
||||||
|
path="/services/:serviceType/:serviceId"
|
||||||
|
element={<ServicePage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export async function fetchServiceTypes(): Promise<ServiceTypeInfo[]> {
|
|||||||
export async function fetchServiceInstances(
|
export async function fetchServiceInstances(
|
||||||
serviceType?: string,
|
serviceType?: string,
|
||||||
): Promise<ServiceInstance[]> {
|
): Promise<ServiceInstance[]> {
|
||||||
const query = serviceType ? `?service_type=${encodeURIComponent(serviceType)}` : "";
|
const query = serviceType
|
||||||
|
? `?service_type=${encodeURIComponent(serviceType)}`
|
||||||
|
: "";
|
||||||
const res = await fetch(`${API_BASE}/services/instances${query}`);
|
const res = await fetch(`${API_BASE}/services/instances${query}`);
|
||||||
if (!res.ok) throw new Error("Failed to fetch service instances");
|
if (!res.ok) throw new Error("Failed to fetch service instances");
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import type {
|
|||||||
|
|
||||||
const API_BASE = "/api";
|
const API_BASE = "/api";
|
||||||
|
|
||||||
export async function fetchBuiltinWidgetKinds(): Promise<BuiltinWidgetKindInfo[]> {
|
export async function fetchBuiltinWidgetKinds(): Promise<
|
||||||
|
BuiltinWidgetKindInfo[]
|
||||||
|
> {
|
||||||
const res = await fetch(`${API_BASE}/widgets/builtin`);
|
const res = await fetch(`${API_BASE}/widgets/builtin`);
|
||||||
if (!res.ok) throw new Error("Failed to fetch built-in widget kinds");
|
if (!res.ok) throw new Error("Failed to fetch built-in widget kinds");
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ function Field({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindingLabel(serviceId: string | null, widgetKind: string): string {
|
function bindingLabel(serviceId: string | null, widgetKind: string): string {
|
||||||
if (serviceId === null) return BUILTIN_WIDGETS[widgetKind]?.name ?? widgetKind;
|
if (serviceId === null)
|
||||||
|
return BUILTIN_WIDGETS[widgetKind]?.name ?? widgetKind;
|
||||||
return widgetKind;
|
return widgetKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +117,11 @@ function WidgetConfigEditor({
|
|||||||
|
|
||||||
const properties = binding
|
const properties = binding
|
||||||
? Object.entries(
|
? Object.entries(
|
||||||
(binding.configSchema as { properties?: Record<string, unknown> } | undefined)
|
(
|
||||||
?.properties ?? {},
|
binding.configSchema as
|
||||||
|
| { properties?: Record<string, unknown> }
|
||||||
|
| undefined
|
||||||
|
)?.properties ?? {},
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -188,8 +192,9 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startAddService(serviceId: string, kind: string) {
|
function startAddService(serviceId: string, kind: string) {
|
||||||
const binding = SERVICE_REGISTRY[services.find((s) => s.id === serviceId)?.service_type ?? ""]
|
const binding = SERVICE_REGISTRY[
|
||||||
?.widgets.find((w) => w.kind === kind);
|
services.find((s) => s.id === serviceId)?.service_type ?? ""
|
||||||
|
]?.widgets.find((w) => w.kind === kind);
|
||||||
setDraft({
|
setDraft({
|
||||||
serviceId,
|
serviceId,
|
||||||
widgetKind: kind,
|
widgetKind: kind,
|
||||||
@@ -267,19 +272,27 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
|
|
||||||
const draftBinding = draft
|
const draftBinding = draft
|
||||||
? draft.serviceId
|
? draft.serviceId
|
||||||
? SERVICE_REGISTRY[services.find((s) => s.id === draft.serviceId)?.service_type ?? ""]
|
? SERVICE_REGISTRY[
|
||||||
?.widgets.find((w) => w.kind === draft.widgetKind)
|
services.find((s) => s.id === draft.serviceId)?.service_type ?? ""
|
||||||
|
]?.widgets.find((w) => w.kind === draft.widgetKind)
|
||||||
: BUILTIN_WIDGETS[draft.widgetKind]
|
: BUILTIN_WIDGETS[draft.widgetKind]
|
||||||
: undefined;
|
: undefined;
|
||||||
const isTaskOutput =
|
const isTaskOutput =
|
||||||
draft?.serviceId !== null &&
|
draft?.serviceId !== null &&
|
||||||
services.find((s) => s.id === draft?.serviceId)?.service_type === "ssh_tasks";
|
services.find((s) => s.id === draft?.serviceId)?.service_type ===
|
||||||
|
"ssh_tasks";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{draft ? (draft.id ? "Edit widget" : "Add widget") : "Dashboard widgets"}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{draft
|
||||||
|
? draft.id
|
||||||
|
? "Edit widget"
|
||||||
|
: "Add widget"
|
||||||
|
: "Dashboard widgets"}
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{draft ? (
|
{draft ? (
|
||||||
@@ -289,7 +302,9 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
<Input
|
<Input
|
||||||
id="widget-title"
|
id="widget-title"
|
||||||
value={draft.title}
|
value={draft.title}
|
||||||
onChange={(e) => setDraft({ ...draft, title: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setDraft({ ...draft, title: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Sort order" htmlFor="widget-sort-order">
|
<Field label="Sort order" htmlFor="widget-sort-order">
|
||||||
@@ -300,7 +315,8 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setDraft({
|
setDraft({
|
||||||
...draft,
|
...draft,
|
||||||
sortOrder: e.target.value === "" ? 0 : Number(e.target.value),
|
sortOrder:
|
||||||
|
e.target.value === "" ? 0 : Number(e.target.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -310,7 +326,9 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
<Switch
|
<Switch
|
||||||
id="widget-enabled"
|
id="widget-enabled"
|
||||||
checked={draft.enabled}
|
checked={draft.enabled}
|
||||||
onCheckedChange={(checked) => setDraft({ ...draft, enabled: checked })}
|
onCheckedChange={(checked) =>
|
||||||
|
setDraft({ ...draft, enabled: checked })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="widget-enabled">Enabled</Label>
|
<Label htmlFor="widget-enabled">Enabled</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,7 +352,9 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{sortedInstances.length === 0 ? (
|
{sortedInstances.length === 0 ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>No widgets yet. Add one below.</AlertDescription>
|
<AlertDescription>
|
||||||
|
No widgets yet. Add one below.
|
||||||
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -343,31 +363,67 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
? services.find((s) => s.id === instance.service_id)?.name
|
? services.find((s) => s.id === instance.service_id)?.name
|
||||||
: "Built-in";
|
: "Built-in";
|
||||||
return (
|
return (
|
||||||
<div key={instance.id} className="flex items-center gap-2 rounded border p-2">
|
<div
|
||||||
|
key={instance.id}
|
||||||
|
className="flex items-center gap-2 rounded border p-2"
|
||||||
|
>
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{instance.title}</span>
|
<span className="font-medium">{instance.title}</span>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{bindingLabel(instance.service_id, instance.widget_kind)}
|
{bindingLabel(
|
||||||
|
instance.service_id,
|
||||||
|
instance.widget_kind,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{serviceName ? (
|
{serviceName ? (
|
||||||
<span className="text-xs text-muted-foreground">{serviceName}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{serviceName}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{!instance.enabled ? (
|
||||||
|
<Badge variant="secondary">disabled</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{!instance.enabled ? <Badge variant="secondary">disabled</Badge> : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={index === 0} onClick={() => moveInstance(index, -1)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
disabled={index === 0}
|
||||||
|
onClick={() => moveInstance(index, -1)}
|
||||||
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={index === sortedInstances.length - 1} onClick={() => moveInstance(index, 1)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
disabled={index === sortedInstances.length - 1}
|
||||||
|
onClick={() => moveInstance(index, 1)}
|
||||||
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Switch checked={instance.enabled} onCheckedChange={() => toggleEnabled(instance)} aria-label={`Toggle ${instance.title}`} />
|
<Switch
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => startEdit(instance)}>
|
checked={instance.enabled}
|
||||||
|
onCheckedChange={() => toggleEnabled(instance)}
|
||||||
|
aria-label={`Toggle ${instance.title}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => startEdit(instance)}
|
||||||
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive" onClick={() => removeInstance(instance)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-destructive"
|
||||||
|
onClick={() => removeInstance(instance)}
|
||||||
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,7 +437,12 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
<p className="text-sm font-medium">Add widget</p>
|
<p className="text-sm font-medium">Add widget</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{Object.values(BUILTIN_WIDGETS).map((b) => (
|
{Object.values(BUILTIN_WIDGETS).map((b) => (
|
||||||
<Button key={b.kind} variant="outline" size="sm" onClick={() => startAddBuiltIn(b.kind)}>
|
<Button
|
||||||
|
key={b.kind}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => startAddBuiltIn(b.kind)}
|
||||||
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
{b.name}
|
{b.name}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -389,21 +450,24 @@ export function WidgetConfigDialog({ open, onClose }: Props) {
|
|||||||
{services
|
{services
|
||||||
.filter((s) => s.enabled)
|
.filter((s) => s.enabled)
|
||||||
.flatMap((s) =>
|
.flatMap((s) =>
|
||||||
(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map((w) => (
|
(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map(
|
||||||
<Button
|
(w) => (
|
||||||
key={`${s.id}:${w.kind}`}
|
<Button
|
||||||
variant="outline"
|
key={`${s.id}:${w.kind}`}
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => startAddService(s.id, w.kind)}
|
size="sm"
|
||||||
>
|
onClick={() => startAddService(s.id, w.kind)}
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
>
|
||||||
{w.name} · {s.name}
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
</Button>
|
{w.name} · {s.name}
|
||||||
)),
|
</Button>
|
||||||
|
),
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Configure services on their service pages to unlock more widgets.
|
Configure services on their service pages to unlock more
|
||||||
|
widgets.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ describe("service registry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("binds widget kinds per service", () => {
|
it("binds widget kinds per service", () => {
|
||||||
expect(SERVICE_REGISTRY.grafana.widgets.map((w) => w.kind)).toEqual(["link"]);
|
expect(SERVICE_REGISTRY.grafana.widgets.map((w) => w.kind)).toEqual([
|
||||||
|
"link",
|
||||||
|
]);
|
||||||
expect(SERVICE_REGISTRY.ssh_tasks.widgets.map((w) => w.kind)).toEqual([
|
expect(SERVICE_REGISTRY.ssh_tasks.widgets.map((w) => w.kind)).toEqual([
|
||||||
"task_output",
|
"task_output",
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
|||||||
defaultConfig: { dashboard_uid: "" },
|
defaultConfig: { dashboard_uid: "" },
|
||||||
configSchema: {
|
configSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { dashboard_uid: { type: "string" }, panel_id: { type: "integer" } },
|
properties: {
|
||||||
|
dashboard_uid: { type: "string" },
|
||||||
|
panel_id: { type: "integer" },
|
||||||
|
},
|
||||||
required: ["dashboard_uid"],
|
required: ["dashboard_uid"],
|
||||||
},
|
},
|
||||||
component: GrafanaLinkWidget,
|
component: GrafanaLinkWidget,
|
||||||
@@ -151,11 +154,15 @@ export const BUILTIN_WIDGETS: Record<string, ServiceWidgetBinding> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getServiceBinding(serviceType: string): ServiceBinding | undefined {
|
export function getServiceBinding(
|
||||||
|
serviceType: string,
|
||||||
|
): ServiceBinding | undefined {
|
||||||
return SERVICE_REGISTRY[serviceType];
|
return SERVICE_REGISTRY[serviceType];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBuiltinBinding(kind: string): ServiceWidgetBinding | undefined {
|
export function getBuiltinBinding(
|
||||||
|
kind: string,
|
||||||
|
): ServiceWidgetBinding | undefined {
|
||||||
return BUILTIN_WIDGETS[kind];
|
return BUILTIN_WIDGETS[kind];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +206,8 @@ export function resolveWidget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Merge backend type metadata (config_schema, secret_fields) onto bindings. */
|
/** Merge backend type metadata (config_schema, secret_fields) onto bindings. */
|
||||||
export function enrichServiceTypes(types: ServiceTypeInfo[]): ServiceTypeInfo[] {
|
export function enrichServiceTypes(
|
||||||
|
types: ServiceTypeInfo[],
|
||||||
|
): ServiceTypeInfo[] {
|
||||||
return types;
|
return types;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ function Field({
|
|||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor={htmlFor}>{label}</Label>
|
<Label htmlFor={htmlFor}>{label}</Label>
|
||||||
{children}
|
{children}
|
||||||
{helper ? <p className="text-xs text-muted-foreground">{helper}</p> : null}
|
{helper ? (
|
||||||
|
<p className="text-xs text-muted-foreground">{helper}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,9 +68,7 @@ export function ServicePage() {
|
|||||||
if (!binding) {
|
if (!binding) {
|
||||||
return (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>Unknown service type: {serviceType}</AlertDescription>
|
||||||
Unknown service type: {serviceType}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,10 +127,7 @@ export function ServicePage() {
|
|||||||
<Button onClick={save} disabled={saveService.isPending}>
|
<Button onClick={save} disabled={saveService.isPending}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="destructive" onClick={() => setDeleteOpen(true)}>
|
||||||
variant="destructive"
|
|
||||||
onClick={() => setDeleteOpen(true)}
|
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +137,10 @@ export function ServicePage() {
|
|||||||
<ServiceSecretsCard instance={instance} />
|
<ServiceSecretsCard instance={instance} />
|
||||||
|
|
||||||
{binding.widgets.length > 0 ? (
|
{binding.widgets.length > 0 ? (
|
||||||
<SectionCard title="Widgets" description="Widget kinds this service provides.">
|
<SectionCard
|
||||||
|
title="Widgets"
|
||||||
|
description="Widget kinds this service provides."
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{binding.widgets.map((w) => (
|
{binding.widgets.map((w) => (
|
||||||
<div
|
<div
|
||||||
@@ -208,14 +208,21 @@ function ServiceSecretsCard({ instance }: { instance: ServiceInstance }) {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{Object.entries(instance.secrets_set).map(([key, isSet]) => (
|
{Object.entries(instance.secrets_set).map(([key, isSet]) => (
|
||||||
<div key={key} className="flex flex-col gap-1.5">
|
<div key={key} className="flex flex-col gap-1.5">
|
||||||
<Field label={key} htmlFor={`secret-${key}`} helper="Leave blank to keep the current value.">
|
<Field
|
||||||
|
label={key}
|
||||||
|
htmlFor={`secret-${key}`}
|
||||||
|
helper="Leave blank to keep the current value."
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
id={`secret-${key}`}
|
id={`secret-${key}`}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={isSet ? "•••••• (set)" : "Not set"}
|
placeholder={isSet ? "•••••• (set)" : "Not set"}
|
||||||
value={draftSecrets[key] ?? ""}
|
value={draftSecrets[key] ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setDraftSecrets({ ...draftSecrets, [key]: e.target.value })
|
setDraftSecrets({
|
||||||
|
...draftSecrets,
|
||||||
|
[key]: e.target.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BackupsWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function BackupsWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const summary = data?.data as BackupDashboardSummary | undefined;
|
const summary = data?.data as BackupDashboardSummary | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GrafanaLinkWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function GrafanaLinkWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const url = data?.data?.url as string | undefined;
|
const url = data?.data?.url as string | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JellyfinWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function JellyfinWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const sessions = data?.data?.sessions as NowPlayingSession[] | undefined;
|
const sessions = data?.data?.sessions as NowPlayingSession[] | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ function formatPrometheusValue(result: PromQLResult | undefined): string {
|
|||||||
return JSON.stringify(result, null, 2);
|
return JSON.stringify(result, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrometheusMetricWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function PrometheusMetricWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const result = data?.data?.result as PromQLResult | undefined;
|
const result = data?.data?.result as PromQLResult | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ type SshTaskResult = {
|
|||||||
stderr: string;
|
stderr: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SshTaskWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function SshTaskWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const result = data?.data as SshTaskResult | undefined;
|
const result = data?.data as SshTaskResult | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StaticWidget({ widget, refreshIntervalMs, description }: Props) {
|
export function StaticWidget({
|
||||||
|
widget,
|
||||||
|
refreshIntervalMs,
|
||||||
|
description,
|
||||||
|
}: Props) {
|
||||||
const { data } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const text = data?.data?.text as string | undefined;
|
const text = data?.data?.text as string | undefined;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user