mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:41:58 -06:00
Merge pull request #101 from bunny-lab-io:codex/add-editable-textbox-for-description-column
Add inline editing for device descriptions
This commit is contained in:
@@ -46,6 +46,151 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz";
|
|||||||
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
|
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
|
||||||
const iconFontFamily = '"Quartz Regular"';
|
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 (
|
||||||
|
<TextField
|
||||||
|
value={draft}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onClick={stopPropagation}
|
||||||
|
onMouseDown={stopPropagation}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
disabled={saving}
|
||||||
|
error={Boolean(error)}
|
||||||
|
helperText={error || undefined}
|
||||||
|
FormHelperTextProps={
|
||||||
|
error
|
||||||
|
? { sx: { minHeight: 18, fontSize: "0.75rem" } }
|
||||||
|
: { sx: { display: "none" } }
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
mt: 0.5,
|
||||||
|
mb: 0.5,
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
backgroundColor,
|
||||||
|
transition: "background-color 0.2s ease, border-color 0.2s ease",
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
fontFamily: fontFamily || gridFontFamily,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
height: 34,
|
||||||
|
py: 0,
|
||||||
|
pr: 0,
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: editing ? "#FFA6FF" : "rgba(255,255,255,0.25)",
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: "#FFA6FF",
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: "#FFA6FF",
|
||||||
|
},
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
py: 0.75,
|
||||||
|
px: 1.5,
|
||||||
|
},
|
||||||
|
'& .MuiFormHelperText-root': {
|
||||||
|
color: "#ff7b7b",
|
||||||
|
mt: 0.25,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function formatLastSeen(tsSec, offlineAfter = 300) {
|
function formatLastSeen(tsSec, offlineAfter = 300) {
|
||||||
if (!tsSec) return "unknown";
|
if (!tsSec) return "unknown";
|
||||||
const now = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
@@ -814,6 +959,58 @@ export default function DeviceList({
|
|||||||
[openMenu]
|
[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 columnDefs = useMemo(() => {
|
||||||
const defs = columns.map((col) => {
|
const defs = columns.map((col) => {
|
||||||
switch (col.id) {
|
switch (col.id) {
|
||||||
@@ -859,6 +1056,11 @@ export default function DeviceList({
|
|||||||
width: 280,
|
width: 280,
|
||||||
minWidth: 280,
|
minWidth: 280,
|
||||||
flex: 0,
|
flex: 0,
|
||||||
|
cellRenderer: DescriptionCellRenderer,
|
||||||
|
cellRendererParams: {
|
||||||
|
onSaveDescription: handleDescriptionSave,
|
||||||
|
fontFamily: gridFontFamily,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
case "lastUser":
|
case "lastUser":
|
||||||
return {
|
return {
|
||||||
@@ -1023,7 +1225,14 @@ export default function DeviceList({
|
|||||||
pinned: "right",
|
pinned: "right",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [columns, actionCellRenderer, formatCreated, hostnameCellRenderer, statusCellRenderer]);
|
}, [
|
||||||
|
columns,
|
||||||
|
actionCellRenderer,
|
||||||
|
formatCreated,
|
||||||
|
handleDescriptionSave,
|
||||||
|
hostnameCellRenderer,
|
||||||
|
statusCellRenderer,
|
||||||
|
]);
|
||||||
|
|
||||||
const defaultColDef = useMemo(
|
const defaultColDef = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user