import { 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, Pencil, Plus, Trash2 } from "lucide-react"; import { useDeleteWidgetInstance, useSaveWidgetInstance, useWidgetInstances, } 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; } 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 textarea for fields that tend to hold complex // multi-line values (PromQL, text blocks, etc.). The widget kind's // config schema can opt in via `format: "textarea"`; the well-known // `query` field is treated as textarea by default. const schemaFormat = (schema as { format?: string }).format; const isTextarea = schemaFormat === "textarea" || key === "query"; return ( {isTextarea ? (