import { useEffect, useMemo, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { ChevronDown, ChevronUp, Link2, Pencil, Plus, Trash2, Split, } from "lucide-react"; import { useCreateWidgetReference, useDeleteWidgetInstance, useDeleteWidgetReference, useDetachWidgetReference, useSaveWidgetInstance, useUpdateWidgetReference, useWidgetInstances, useWidgetReferences, } from "../hooks/useWidgets"; import { useServiceInstances } from "../hooks/useServices"; import { useTasks } from "../hooks/useSettings"; import { useIsMobile } from "../hooks/useIsMobile"; import { SheetForm } from "@/components/ui/sheet-form"; import type { WidgetInstance, WidgetInstanceInput } from "../types"; import { BUILTIN_WIDGETS, SERVICE_REGISTRY, type ServiceWidgetBinding, } from "../integrations/registry"; interface Props { open: boolean; onClose: () => void; /** When set, scope the dialog to a specific service instance's widgets. */ serviceId?: string; /** When set, enable widget references ("Add existing") for this dashboard scope. */ dashboardScope?: string; /** When set, auto-open in edit mode for this widget id (instead of the list view). */ editWidgetId?: string; } interface Draft { id?: string; serviceId: string | null; widgetKind: string; title: string; config: Record; enabled: boolean; sortOrder: number; } function Field({ label, htmlFor, helper, children, }: { label: string; htmlFor: string; helper?: string; children: React.ReactNode; }) { return (
{children} {helper ? (

{helper}

) : null}
); } function bindingLabel(serviceId: string | null, widgetKind: string): string { if (serviceId === null) return BUILTIN_WIDGETS[widgetKind]?.name ?? widgetKind; return widgetKind; } function WidgetConfigEditor({ binding, isTaskOutput, config, onChange, tasks, }: { binding: ServiceWidgetBinding | undefined; isTaskOutput: boolean; config: Record; onChange: (config: Record) => void; tasks: { id: string; name: string; enabled: boolean }[]; }) { // SSH task output gets a dedicated task picker; everything else gets a // generic text field per top-level schema property. if (isTaskOutput) { return ( ); } const properties = binding ? Object.entries( ( binding.configSchema as | { properties?: Record } | undefined )?.properties ?? {}, ) : []; if (properties.length === 0) return null; return (
{properties.map(([key, schema]) => { const isNumber = (schema as { type?: string }).type === "integer" || (schema as { type?: string }).type === "number"; // Use a multi-line resizable textarea for fields that tend to hold // complex multi-line values (PromQL expressions, Grafana query strings, // markdown/text blocks, etc.). The widget kind's config schema can opt // in via `format: "textarea"`; the well-known field names below are // treated as textarea by default. const schemaFormat = (schema as { format?: string }).format; const TEXTAREA_KEYS = new Set(["promql", "query", "text", "command", "notes"]); const isTextarea = schemaFormat === "textarea" || TEXTAREA_KEYS.has(key); return ( {isTextarea ? (