diff --git a/Data/Server/WebUI/src/Devices/Device_List.jsx b/Data/Server/WebUI/src/Devices/Device_List.jsx index 6bf2d72..fbf51d9 100644 --- a/Data/Server/WebUI/src/Devices/Device_List.jsx +++ b/Data/Server/WebUI/src/Devices/Device_List.jsx @@ -46,6 +46,151 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz"; const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif'; const iconFontFamily = '"Quartz Regular"'; +const DescriptionCellRenderer = React.memo(function DescriptionCellRenderer(props) { + const { value, data, onSaveDescription, fontFamily } = props; + const safeValue = typeof value === "string" ? value : value == null ? "" : String(value); + const [draft, setDraft] = useState(safeValue); + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + if (!editing && !saving) { + setDraft(safeValue); + } + }, [safeValue, editing, saving]); + + const handleFocus = useCallback((event) => { + event.stopPropagation(); + setEditing(true); + setError(""); + }, []); + + const handleChange = useCallback((event) => { + setDraft(event.target.value); + }, []); + + const handleKeyDown = useCallback( + async (event) => { + event.stopPropagation(); + if (event.key === "Enter") { + event.preventDefault(); + const trimmed = (draft || "").trim(); + if (trimmed === safeValue.trim()) { + setEditing(false); + setDraft(safeValue); + setError(""); + return; + } + if (typeof onSaveDescription !== "function" || !data) { + setEditing(false); + setError(""); + return; + } + setSaving(true); + setError(""); + const ok = await onSaveDescription(data, trimmed); + setSaving(false); + if (ok) { + setEditing(false); + } else { + setError("Failed to save description"); + } + } else if (event.key === "Escape") { + event.preventDefault(); + setDraft(safeValue); + setEditing(false); + setError(""); + } + }, + [data, draft, onSaveDescription, safeValue] + ); + + const handleBlur = useCallback( + (event) => { + event.stopPropagation(); + if (saving) return; + setEditing(false); + setDraft(safeValue); + setError(""); + }, + [saving, safeValue] + ); + + const stopPropagation = useCallback((event) => { + event.stopPropagation(); + }, []); + + const backgroundColor = saving + ? "rgba(255,255,255,0.04)" + : editing + ? "rgba(255,255,255,0.16)" + : "rgba(255,255,255,0.02)"; + + return ( + + ); +}); + function formatLastSeen(tsSec, offlineAfter = 300) { if (!tsSec) return "unknown"; const now = Date.now() / 1000; @@ -814,6 +959,58 @@ export default function DeviceList({ [openMenu] ); + const handleDescriptionSave = useCallback( + async (row, nextDescription) => { + if (!row) return false; + const trimmed = (nextDescription || "").trim(); + const targetHost = (row.hostname || row.summary?.hostname || "").trim(); + if (!targetHost) return false; + try { + const resp = await fetch(`/api/device/description/${targetHost}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ description: trimmed }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const matchValue = row.id || row.agentGuid || row.hostname || targetHost; + setRows((prev) => + prev.map((item) => { + const itemMatch = item.id || item.agentGuid || item.hostname || ""; + if (itemMatch !== matchValue) return item; + const updated = { + ...item, + description: trimmed, + summary: { ...(item.summary || {}), description: trimmed }, + }; + if (item.details) { + updated.details = { ...item.details, description: trimmed }; + } + return updated; + }) + ); + setSelected((prev) => { + if (!prev) return prev; + const prevMatch = prev.id || prev.agentGuid || prev.hostname || ""; + if (prevMatch !== matchValue) return prev; + const updated = { + ...prev, + description: trimmed, + summary: { ...(prev.summary || {}), description: trimmed }, + }; + if (prev.details) { + updated.details = { ...prev.details, description: trimmed }; + } + return updated; + }); + return true; + } catch (e) { + console.warn("Failed to save description", e); + return false; + } + }, + [setRows, setSelected] + ); + const columnDefs = useMemo(() => { const defs = columns.map((col) => { switch (col.id) { @@ -859,6 +1056,11 @@ export default function DeviceList({ width: 280, minWidth: 280, flex: 0, + cellRenderer: DescriptionCellRenderer, + cellRendererParams: { + onSaveDescription: handleDescriptionSave, + fontFamily: gridFontFamily, + }, }; case "lastUser": return { @@ -1023,7 +1225,14 @@ export default function DeviceList({ pinned: "right", }, ]; - }, [columns, actionCellRenderer, formatCreated, hostnameCellRenderer, statusCellRenderer]); + }, [ + columns, + actionCellRenderer, + formatCreated, + handleDescriptionSave, + hostnameCellRenderer, + statusCellRenderer, + ]); const defaultColDef = useMemo( () => ({