Files
home-automation/frontend/src/energy/SourceForm.tsx
T

34 lines
4.1 KiB
TypeScript

import { useState } from 'react'
import { Alert, Button, Checkbox, Group, Modal, Select, Stack, TextInput } from '@mantine/core'
import { ApiError } from '../api/client'
import { useCreateSource, useSourceProfiles, useUpdateSource, type MeterSourceResponse } from './hooks'
export function SourceForm({ source, onClose }: { source?: MeterSourceResponse; onClose: () => void }) {
const profiles = useSourceProfiles(); const create = useCreateSource(); const update = useUpdateSource()
const [name, setName] = useState(source?.name ?? ''); const [kind, setKind] = useState<string | null>(source?.kind ?? null)
const [enabled, setEnabled] = useState(source?.enabled ?? true); const [config, setConfig] = useState<Record<string, string | boolean | number>>({}); const [error, setError] = useState<string | null>(null)
const profile = profiles.data?.items.find((item) => item.kind === kind)
async function submit(e: React.FormEvent) { e.preventDefault(); setError(null); if (!name.trim() || !kind) return setError('Name and source type are required.')
const values: Record<string, unknown> = {}; profile?.fields.forEach((field) => {
const changed = Object.prototype.hasOwnProperty.call(config, field.name)
const raw = changed ? config[field.name] : (source?.config[field.name] ?? field.default ?? '')
// A masked secret is deliberately absent from edit PATCHes until the user
// explicitly enters a replacement; sending an empty/masked value is unsafe.
if (field.secret && source && (!changed || raw === '')) return
if (field.value_type === 'bool' || field.value_type === 'boolean') values[field.name] = typeof raw === 'boolean' ? raw : raw === 'true'
else if (field.value_type === 'int' || field.value_type === 'integer') values[field.name] = typeof raw === 'number' ? raw : Number(raw)
else values[field.name] = typeof raw === 'string' ? raw : String(raw)
})
try { if (source) await update.mutateAsync({ uuid: source.uuid, body: { name: name.trim(), enabled, config: values } }); else await create.mutateAsync({ name: name.trim(), kind, enabled, config: values }); onClose() } catch (err) { setError(err instanceof ApiError ? String((err.body as { detail?: string })?.detail ?? `Error ${err.status}`) : 'Could not save source.') }
}
return <Modal opened onClose={onClose} title={source ? 'Edit Source' : 'New Source'}><form onSubmit={submit}><Stack>
<TextInput label="Name" required value={name} onChange={(e) => setName(e.currentTarget.value)} />
{profiles.isLoading && <Alert color="blue">Loading source profiles</Alert>}{profiles.isError && <Alert color="red">Failed to load source profiles.</Alert>}
<Select label="Source type" required data={profiles.data?.items.map((p) => ({ value: p.kind, label: p.kind })) ?? []} value={kind} onChange={setKind} disabled={!!source} />
{kind === 'warmtelink_serial' && <Alert color="blue">Serial sources use <code>/dev/serial/by-id/</code>; 115200 7N1.</Alert>}
{profile?.fields.map((field) => field.value_type === 'bool' || field.value_type === 'boolean' ? <Checkbox key={field.name} label={field.name} checked={Boolean(config[field.name] ?? source?.config[field.name] ?? field.default ?? false)} onChange={(e) => setConfig({ ...config, [field.name]: e.currentTarget.checked })} /> : <TextInput key={field.name} label={field.name} required={field.required} type={field.secret ? 'password' : (field.value_type === 'int' || field.value_type === 'integer' ? 'number' : 'text')} placeholder={field.secret && source ? 'Stored secret unchanged when blank' : undefined} value={String(config[field.name] ?? (field.secret ? '' : source?.config[field.name] ?? field.default ?? ''))} onChange={(e) => setConfig({ ...config, [field.name]: e.currentTarget.value })} />)}
<Checkbox label="Enabled" checked={enabled} onChange={(e) => setEnabled(e.currentTarget.checked)} />
{error && <Alert color="red">{error}</Alert>}<Group justify="flex-end"><Button variant="default" onClick={onClose}>Cancel</Button><Button type="submit" loading={create.isPending || update.isPending}>Save Source</Button></Group>
</Stack></form></Modal>
}