import { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useDeleteServiceInstance, useSaveServiceInstance, useServiceInstances, useServiceTypes, } from "../hooks/useServices"; import type { ServiceInstance, ServiceInstanceInput, ServiceTypeInfo, } from "../types"; import { SectionCard } from "../components/SectionCard"; import { ConfirmDialog } from "../components/ConfirmDialog"; import { getServiceBinding } from "../integrations/registry"; function Field({ label, htmlFor, helper, children, }: { label: string; htmlFor: string; helper?: string; children: React.ReactNode; }) { return (
{children} {helper ? (

{helper}

) : null}
); } export function ServicePage() { const { serviceType = "", serviceId = "" } = useParams<{ serviceType: string; serviceId: string; }>(); const { data: services = [] } = useServiceInstances(serviceType || undefined); const { data: types = [] } = useServiceTypes(); const saveService = useSaveServiceInstance(); const deleteService = useDeleteServiceInstance(); const instance = useMemo( () => services.find((s) => s.id === serviceId), [services, serviceId], ); const binding = getServiceBinding(serviceType); const typeInfo = useMemo( () => types.find((t) => t.service_type === serviceType), [types, serviceType], ); const [name, setName] = useState(""); const [enabled, setEnabled] = useState(true); const [draftConfig, setDraftConfig] = useState>({}); const [deleteOpen, setDeleteOpen] = useState(false); const [hydrated, setHydrated] = useState(false); // Hydrate local form state once the instance loads. if (instance && !hydrated) { setName(instance.name); setEnabled(instance.enabled); setDraftConfig({ ...instance.config }); setHydrated(true); } if (!binding) { return ( Unknown service type: {serviceType} ); } if (!instance) { return ( Service not found. ); } function buildInput(): ServiceInstanceInput { return { id: instance!.id, service_type: instance!.service_type, name, config: draftConfig, secrets: {}, // secrets are managed via the dedicated inputs below enabled, }; } async function save() { await saveService.mutateAsync(buildInput()); } return (

{instance.name}

{binding.description}

{binding.name}
setName(e.target.value)} />
{binding.widgets.length > 0 ? (
{binding.widgets.map((w) => (
{w.name}
{w.description}
{w.kind}
))}

Add these to the dashboard from the dashboard's edit dialog.

) : null} setDeleteOpen(false)} onConfirm={() => { deleteService.mutate(instance.id); setDeleteOpen(false); }} />
); } function ServiceConnectionCard({ instance, typeInfo, draftConfig, onConfigChange, }: { instance: ServiceInstance; typeInfo: ServiceTypeInfo | undefined; draftConfig: Record; onConfigChange: (config: Record) => void; }) { const saveService = useSaveServiceInstance(); // Empty-on-edit: local state starts blank; a blank field means "keep existing". const [draftSecrets, setDraftSecrets] = useState>({}); const properties = ( (typeInfo?.config_schema ?? {}) as { properties?: Record< string, { type?: string; description?: string; default?: unknown } >; } ).properties ?? {}; const configEntries: Array< [string, { type?: string; description?: string }] > = Object.keys(properties).length > 0 ? Object.entries(properties).map(([key, schema]) => [ key, { type: schema?.type, description: schema?.description }, ]) : Object.entries(instance.config).map(([key, value]) => [ key, { type: typeof value === "number" ? "integer" : "string" }, ]); return (
{configEntries.length === 0 ? (

No connection config.

) : (
{configEntries.map(([key, schema]) => { const isNumber = schema.type === "integer" || schema.type === "number"; return ( onConfigChange({ ...draftConfig, [key]: isNumber ? e.target.value === "" ? undefined : Number(e.target.value) : e.target.value, }) } /> ); })}
)} {Object.keys(instance.secrets_set).length === 0 ? (

No secret fields.

) : (
{Object.entries(instance.secrets_set).map(([key, isSet]) => (
setDraftSecrets({ ...draftSecrets, [key]: e.target.value, }) } /> {isSet ? set : null}
))}
)}
); }