M8-T18: add source and multi-commodity meter UI
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
}
|
||||
Reference in New Issue
Block a user