import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useState } from "react" import { Info, Loader2, MapPin, Trash2 } from "lucide-react" import { toast } from "sonner" import type { ConfigFieldDef, SourceDefinition } from "@/lib/api" import { fetchSourceConfig, pushLocation, replaceSource, updateProviderConfig } from "@/lib/api" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" interface SourceConfigPanelProps { source: SourceDefinition onUpdate: () => void } export function SourceConfigPanel({ source, onUpdate }: SourceConfigPanelProps) { const queryClient = useQueryClient() const [dirty, setDirty] = useState>({}) const { data: serverConfig, isLoading } = useQuery({ queryKey: ["sourceConfig", source.id], queryFn: () => fetchSourceConfig(source.id), }) const enabled = serverConfig?.enabled ?? false const serverValues = buildInitialValues(source.fields, serverConfig?.config) const formValues = { ...serverValues, ...dirty } function isCredentialField(field: ConfigFieldDef): boolean { return !!(field.secret && field.required) } function getUserConfig(): Record { const result: Record = {} for (const [name, value] of Object.entries(formValues)) { const field = source.fields[name] if (field && !isCredentialField(field)) { result[name] = value } } return result } function getCredentialFields(): Record { const creds: Record = {} for (const [name, value] of Object.entries(formValues)) { const field = source.fields[name] if (field && isCredentialField(field)) { creds[name] = value } } return creds } function invalidate() { queryClient.invalidateQueries({ queryKey: ["sourceConfig", source.id] }) queryClient.invalidateQueries({ queryKey: ["configs"] }) onUpdate() } const saveMutation = useMutation({ mutationFn: async () => { const promises: Promise[] = [ replaceSource(source.id, { enabled, config: getUserConfig() }), ] const credentialFields = getCredentialFields() const hasCredentials = Object.values(credentialFields).some( (v) => typeof v === "string" && v.length > 0, ) if (hasCredentials) { promises.push( updateProviderConfig(source.id, { credentials: credentialFields }), ) } await Promise.all(promises) }, onSuccess() { setDirty({}) invalidate() toast.success("Configuration saved") }, onError(err) { toast.error(err.message) }, }) const toggleMutation = useMutation({ mutationFn: (checked: boolean) => replaceSource(source.id, { enabled: checked, config: getUserConfig() }), onSuccess(_data, checked) { invalidate() toast.success(`Source ${checked ? "enabled" : "disabled"}`) }, onError(err) { toast.error(err.message) }, }) const deleteMutation = useMutation({ mutationFn: () => replaceSource(source.id, { enabled: false, config: {} }), onSuccess() { setDirty({}) invalidate() toast.success("Configuration deleted") }, onError(err) { toast.error(err.message) }, }) function handleFieldChange(fieldName: string, value: unknown) { setDirty((prev) => ({ ...prev, [fieldName]: value })) } const fieldEntries = Object.entries(source.fields) const hasFields = fieldEntries.length > 0 const busy = saveMutation.isPending || toggleMutation.isPending || deleteMutation.isPending const requiredFields = fieldEntries.filter(([, f]) => f.required) const optionalFields = fieldEntries.filter(([, f]) => !f.required) if (isLoading) { return (
) } return (
{/* Header */}

{source.name}

{source.alwaysEnabled ? ( Always on ) : enabled ? ( Enabled ) : ( Disabled )}

{source.description}

{!source.alwaysEnabled && ( toggleMutation.mutate(checked)} disabled={busy} /> )}
{/* Config form */} {hasFields && !source.alwaysEnabled && ( <> {/* Required fields */} {requiredFields.length > 0 && ( Credentials Required fields to connect this source. {requiredFields.map(([name, field]) => ( handleFieldChange(name, v)} disabled={busy} /> ))} )} {/* Optional fields */} {optionalFields.length > 0 && ( Options Optional configuration for this source.
1 ? "grid-cols-2" : ""}`}> {optionalFields.map(([name, field]) => ( handleFieldChange(name, v)} disabled={busy} /> ))}
)} {/* Actions */}
{serverConfig && ( )}
)} {/* Always-on sources */} {source.alwaysEnabled && source.id !== "aelis.location" && ( <>

This source is always enabled and requires no configuration.

)} {source.id === "aelis.location" && }
) } function LocationCard() { const [lat, setLat] = useState("") const [lng, setLng] = useState("") const locationMutation = useMutation({ mutationFn: (coords: { lat: number; lng: number }) => pushLocation({ lat: coords.lat, lng: coords.lng, accuracy: 10 }), onSuccess() { toast.success("Location updated") }, onError(err) { toast.error(err.message) }, }) function handlePush() { const latNum = parseFloat(lat) const lngNum = parseFloat(lng) if (isNaN(latNum) || isNaN(lngNum)) return locationMutation.mutate({ lat: latNum, lng: lngNum }) } function handleUseDevice() { navigator.geolocation.getCurrentPosition( (pos) => { setLat(String(pos.coords.latitude)) setLng(String(pos.coords.longitude)) locationMutation.mutate({ lat: pos.coords.latitude, lng: pos.coords.longitude, }) }, (err) => { locationMutation.reset() alert(`Geolocation error: ${err.message}`) }, ) } return ( Push Location Send a location update to the backend.
setLat(e.target.value)} placeholder="51.5074" disabled={locationMutation.isPending} />
setLng(e.target.value)} placeholder="-0.1278" disabled={locationMutation.isPending} />
) } function FieldInput({ name, field, value, onChange, disabled, }: { name: string field: ConfigFieldDef value: unknown onChange: (value: unknown) => void disabled?: boolean }) { const labelContent = (
{field.label} {field.required && *} {field.description && ( {field.description} )}
) if (field.type === "select" && field.options) { return (
) } if (field.type === "multiselect" && field.options) { const selected = Array.isArray(value) ? (value as string[]) : [] function toggle(optValue: string) { const next = selected.includes(optValue) ? selected.filter((v) => v !== optValue) : [...selected, optValue] onChange(next) } return (
{field.options!.map((opt) => { const isSelected = selected.includes(opt.value) return ( !disabled && toggle(opt.value)} > {opt.label} ) })}
) } if (field.type === "number") { return (
{ const v = e.target.value onChange(v === "" ? undefined : Number(v)) }} placeholder={field.defaultValue !== undefined ? String(field.defaultValue) : undefined} disabled={disabled} />
) } return (
onChange(e.target.value)} placeholder={field.defaultValue !== undefined ? String(field.defaultValue) : undefined} disabled={disabled} />
) } function buildInitialValues( fields: Record, saved: Record | undefined, ): Record { const values: Record = {} for (const [name, field] of Object.entries(fields)) { if (saved && name in saved) { values[name] = saved[name] } else if (field.defaultValue !== undefined) { values[name] = field.defaultValue } else if (field.type === "multiselect") { values[name] = [] } else { values[name] = field.type === "number" ? undefined : "" } } return values }