/** * EnergyPage — device management UI for the Energy (Modbus) domain. * * Features: * - Device list with status indicators (enabled, last poll). * - Create / edit via DeviceForm modal. * - Delete with二次确认; 409 (has readings) → friendly prompt to disable instead. * - "Test read" button per device — calls POST /devices/{uuid}/test, shows * decoded payload or error without persisting to the database. * * Out of scope (T07): readings cards, trend charts, Recharts import. */ import { useState } from 'react' import { Container, Title, Table, Button, Group, Text, Loader, Center, Alert, Stack, Badge, ScrollArea, Modal, Code, Tooltip, } from '@mantine/core' import { useDevices, useDeleteDevice, useTestReadDevice } from '../energy/hooks' import { DeviceForm } from '../energy/DeviceForm' import type { ModbusDevice, ModbusTestReadResponse } from '../energy/hooks' import { ApiError } from '../api/client' // --------------------------------------------------------------------------- // Delete confirmation modal (with 409 "disable instead" hint) // --------------------------------------------------------------------------- interface ConfirmDeleteProps { device: ModbusDevice onConfirm: () => void onCancel: () => void loading: boolean /** Set when the delete failed with 409. */ has409Error: boolean } function ConfirmDeleteModal({ device, onConfirm, onCancel, loading, has409Error }: ConfirmDeleteProps) { return ( Delete device {device.friendly_name}? {has409Error && ( This device has existing readings and cannot be deleted. Consider{' '} disabling it instead (click Edit → uncheck Enabled). )} ) } // --------------------------------------------------------------------------- // Test-read result modal // --------------------------------------------------------------------------- interface TestResultModalProps { result: ModbusTestReadResponse deviceName: string onClose: () => void } function TestResultModal({ result, deviceName, onClose }: TestResultModalProps) { return ( {result.ok ? ( Success {JSON.stringify(result.payload, null, 2)} ) : ( Failed {result.error ?? 'Unknown error'} )} ) } // --------------------------------------------------------------------------- // Device row action: test-read button (self-contained per row) // --------------------------------------------------------------------------- interface TestReadButtonProps { device: ModbusDevice } function TestReadButton({ device }: TestReadButtonProps) { const testMutation = useTestReadDevice() const [result, setResult] = useState(null) async function handleTest() { setResult(null) const res = await testMutation.mutateAsync(device.uuid) if (res.data) { setResult(res.data) } } return ( <> {result && ( setResult(null)} /> )} ) } // --------------------------------------------------------------------------- // Device list table // --------------------------------------------------------------------------- interface DeviceTableProps { devices: ModbusDevice[] onEdit: (device: ModbusDevice) => void onDelete: (device: ModbusDevice) => void } function DeviceTable({ devices, onEdit, onDelete }: DeviceTableProps) { if (devices.length === 0) { return ( No devices configured yet. Click "New Device" to add one. ) } return ( Name Host Port Unit ID Profile Poll (s) Status Actions {devices.map((device) => ( {device.friendly_name} {device.host} {device.port} {device.unit_id} {device.profile} {device.poll_interval_s} {device.enabled ? 'enabled' : 'disabled'} {device.last_poll_ok !== null && ( {device.last_poll_ok ? 'online' : 'offline'} )} ))}
) } // --------------------------------------------------------------------------- // EnergyPage — top-level // --------------------------------------------------------------------------- export function EnergyPage() { const devicesQuery = useDevices() const deleteMutation = useDeleteDevice() const [editDevice, setEditDevice] = useState(null) const [showCreateForm, setShowCreateForm] = useState(false) const [deleteDevice, setDeleteDevice] = useState(null) const [delete409, setDelete409] = useState(false) function openCreate() { setEditDevice(null) setShowCreateForm(true) } function openEdit(device: ModbusDevice) { setShowCreateForm(false) setEditDevice(device) } function openDelete(device: ModbusDevice) { setDeleteDevice(device) setDelete409(false) } function closeDelete() { setDeleteDevice(null) setDelete409(false) } async function handleDeleteConfirm() { if (!deleteDevice) return setDelete409(false) try { await deleteMutation.mutateAsync(deleteDevice.uuid) closeDelete() } catch (err) { if (err instanceof ApiError && err.status === 409) { setDelete409(true) } // Leave modal open so user sees the hint. } } // --------------------------------------------------------------------------- // Loading / error states // --------------------------------------------------------------------------- if (devicesQuery.isLoading) { return (
) } if (devicesQuery.isError || !devicesQuery.data) { return ( Failed to load devices. Please refresh. ) } const devices = devicesQuery.data.items // --------------------------------------------------------------------------- // Main render // --------------------------------------------------------------------------- return ( Energy — Devices {/* Create form */} {showCreateForm && ( setShowCreateForm(false)} onSaved={() => setShowCreateForm(false)} /> )} {/* Edit form */} {editDevice && ( setEditDevice(null)} onSaved={() => setEditDevice(null)} /> )} {/* Delete confirmation */} {deleteDevice && ( )} ) }