Redesigned Assembly List

This commit is contained in:
2025-11-02 01:50:22 -06:00
parent 18c64daa7b
commit 599e3776a0

View File

@@ -13,6 +13,7 @@ import {
DialogActions, DialogActions,
TextField, TextField,
CircularProgress, CircularProgress,
Link as MuiLink,
} from "@mui/material"; } from "@mui/material";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import CachedIcon from "@mui/icons-material/Cached"; import CachedIcon from "@mui/icons-material/Cached";
@@ -45,6 +46,7 @@ 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 BOREALIS_BLUE = "#58a6ff"; const BOREALIS_BLUE = "#58a6ff";
const DARKER_GRAY = "#9aa3ad";
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
const TYPE_METADATA = { const TYPE_METADATA = {
@@ -65,9 +67,7 @@ const TYPE_METADATA = {
const TypeCellRenderer = React.memo(function TypeCellRenderer(props) { const TypeCellRenderer = React.memo(function TypeCellRenderer(props) {
const typeKey = props?.data?.typeKey; const typeKey = props?.data?.typeKey;
const meta = typeKey ? TYPE_METADATA[typeKey] : null; const meta = typeKey ? TYPE_METADATA[typeKey] : null;
if (!meta) { if (!meta) return null;
return null;
}
const { Icon, label } = meta; const { Icon, label } = meta;
return ( return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
@@ -79,6 +79,42 @@ const TypeCellRenderer = React.memo(function TypeCellRenderer(props) {
); );
}); });
// Clickable name that opens the corresponding editor, styled in Borealis blue
const NameCellRenderer = React.memo(function NameCellRenderer(props) {
const { data, context } = props;
const openRow = context?.openRow;
if (!data) return null;
const handleClick = (e) => {
e.preventDefault();
openRow?.(data);
};
const handleKeyDown = (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openRow?.(data);
}
};
return (
<MuiLink
component="button"
onClick={handleClick}
onKeyDown={handleKeyDown}
sx={{
color: BOREALIS_BLUE,
textAlign: "left",
cursor: "pointer",
p: 0,
m: 0,
fontSize: 14,
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
}}
>
{data?.name || ""}
</MuiLink>
);
});
const normalizeRow = (island, item) => { const normalizeRow = (island, item) => {
const relPath = String(item?.rel_path || "").replace(/\\/g, "/"); const relPath = String(item?.rel_path || "").replace(/\\/g, "/");
const fileName = String(item?.file_name || relPath.split("/").pop() || ""); const fileName = String(item?.file_name || relPath.split("/").pop() || "");
@@ -88,12 +124,12 @@ const normalizeRow = (island, item) => {
island === "workflows" island === "workflows"
? item?.tab_name || fileName.replace(/\.[^.]+$/, "") || fileName || "Workflow" ? item?.tab_name || fileName.replace(/\.[^.]+$/, "") || fileName || "Workflow"
: item?.name || fileName.replace(/\.[^.]+$/, "") || fileName || "Assembly"; : item?.name || fileName.replace(/\.[^.]+$/, "") || fileName || "Assembly";
// For workflows, always show 'workflow' in Category per request
const category = const category =
island === "workflows" island === "workflows"
? folder || "Workflows" ? "workflow"
: item?.category || ""; : item?.category || "";
const description = const description = island === "workflows" ? "" : item?.description || "";
island === "workflows" ? "" : item?.description || "";
return { return {
id: `${island}:${idSeed}`, id: `${island}:${idSeed}`,
typeKey: island, typeKey: island,
@@ -144,6 +180,14 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
}), }),
); );
setRows(results.flat()); setRows(results.flat());
// After data load, auto-size specific columns
setTimeout(() => {
const columnApi = gridRef.current?.columnApi;
if (columnApi) {
const ids = ["assemblyType", "location", "category", "name"];
columnApi.autoSizeColumns(ids, false);
}
}, 0);
} catch (err) { } catch (err) {
console.error("Failed to load assemblies:", err); console.error("Failed to load assemblies:", err);
setRows([]); setRows([]);
@@ -168,15 +212,11 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
}; };
if (!payload.name) payload.name = row.name; if (!payload.name) payload.name = row.name;
if (!payload.tab_name) payload.tab_name = row.name; if (!payload.tab_name) payload.tab_name = row.name;
if (onOpenWorkflow) { onOpenWorkflow?.(payload);
onOpenWorkflow(payload);
}
return; return;
} }
const mode = row.typeKey === "ansible" ? "ansible" : "scripts"; const mode = row.typeKey === "ansible" ? "ansible" : "scripts";
if (onOpenScript) { onOpenScript?.(row.relPath, mode, null);
onOpenScript(row.relPath, mode, null);
}
}, },
[onOpenWorkflow, onOpenScript], [onOpenWorkflow, onOpenScript],
); );
@@ -239,9 +279,7 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
const data = await resp.json(); const data = await resp.json();
if (!resp.ok) { if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
throw new Error(data?.error || `HTTP ${resp.status}`);
}
setRenameDialogOpen(false); setRenameDialogOpen(false);
await fetchAssemblies(); await fetchAssemblies();
} catch (err) { } catch (err) {
@@ -267,9 +305,7 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
}), }),
}); });
const data = await resp.json(); const data = await resp.json();
if (!resp.ok) { if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
throw new Error(data?.error || `HTTP ${resp.status}`);
}
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
await fetchAssemblies(); await fetchAssemblies();
} catch (err) { } catch (err) {
@@ -281,30 +317,62 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
const columnDefs = useMemo( const columnDefs = useMemo(
() => [ () => [
{ {
colId: "assemblyType",
field: "assemblyType", field: "assemblyType",
headerName: "Assembly Type", headerName: "Assembly Type",
valueGetter: (params) => TYPE_METADATA[params?.data?.typeKey]?.label || "", valueGetter: (params) => TYPE_METADATA[params?.data?.typeKey]?.label || "",
cellRenderer: TypeCellRenderer, cellRenderer: TypeCellRenderer,
width: 200, minWidth: 160,
flex: 0,
sortable: true,
filter: "agTextColumnFilter",
resizable: true,
},
{
colId: "location",
field: "location",
headerName: "Location",
valueGetter: (params) => params?.data?.folder || "",
cellStyle: { color: DARKER_GRAY, fontSize: 13 },
minWidth: 180, minWidth: 180,
flex: 0, flex: 0,
sortable: true, sortable: true,
filter: "agTextColumnFilter", filter: "agTextColumnFilter",
resizable: true,
}, },
{ {
colId: "category",
field: "category", field: "category",
headerName: "Category", headerName: "Category",
valueGetter: (params) => params?.data?.category || "", valueGetter: (params) => params?.data?.category || "",
minWidth: 160,
flex: 0,
sortable: true,
filter: "agTextColumnFilter",
resizable: true,
}, },
{ {
colId: "name",
field: "name", field: "name",
headerName: "Name", headerName: "Name",
valueGetter: (params) => params?.data?.name || "", valueGetter: (params) => params?.data?.name || "",
cellRenderer: NameCellRenderer,
minWidth: 220,
flex: 0,
sortable: true,
filter: "agTextColumnFilter",
resizable: true,
}, },
{ {
colId: "description",
field: "description", field: "description",
headerName: "Description", headerName: "Description",
valueGetter: (params) => params?.data?.description || "", valueGetter: (params) => params?.data?.description || "",
flex: 1, // Only Description flexes to take remaining width
minWidth: 300,
sortable: true,
filter: "agTextColumnFilter",
resizable: true,
}, },
], ],
[], [],
@@ -314,10 +382,11 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
() => ({ () => ({
sortable: true, sortable: true,
filter: "agTextColumnFilter", filter: "agTextColumnFilter",
floatingFilter: true, // Remove floating textboxes at the top (use column menu filters instead)
floatingFilter: false,
resizable: true, resizable: true,
flex: 1, flex: 0,
minWidth: 180, minWidth: 140,
}), }),
[], [],
); );
@@ -339,9 +408,7 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
const handleCreateScript = () => { const handleCreateScript = () => {
const trimmed = scriptName.trim(); const trimmed = scriptName.trim();
if (!trimmed || !scriptDialog.island) { if (!trimmed || !scriptDialog.island) return;
return;
}
const isAnsible = scriptDialog.island === "ansible"; const isAnsible = scriptDialog.island === "ansible";
const context = { const context = {
folder: "", folder: "",
@@ -351,22 +418,16 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
type: isAnsible ? "ansible" : "powershell", type: isAnsible ? "ansible" : "powershell",
category: isAnsible ? "application" : "script", category: isAnsible ? "application" : "script",
}; };
if (onOpenScript) { onOpenScript?.(null, isAnsible ? "ansible" : "scripts", context);
onOpenScript(null, isAnsible ? "ansible" : "scripts", context);
}
setScriptDialog({ open: false, island: null }); setScriptDialog({ open: false, island: null });
setScriptName(""); setScriptName("");
}; };
const handleCreateWorkflow = () => { const handleCreateWorkflow = () => {
const trimmed = workflowName.trim(); const trimmed = workflowName.trim();
if (!trimmed) { if (!trimmed) return;
return;
}
setWorkflowDialogOpen(false); setWorkflowDialogOpen(false);
if (onOpenWorkflow) { onOpenWorkflow?.(null, "", trimmed);
onOpenWorkflow(null, "", trimmed);
}
setWorkflowName(""); setWorkflowName("");
}; };
@@ -460,6 +521,13 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
"& .ag-icon": { "& .ag-icon": {
fontFamily: iconFontFamily, fontFamily: iconFontFamily,
}, },
// Vertically center cell content across the board
"& .ag-cell": {
display: "flex",
alignItems: "center",
paddingTop: "8px",
paddingBottom: "8px",
},
}} }}
> >
<AgGridReact <AgGridReact
@@ -467,14 +535,18 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
rowData={rows} rowData={rows}
columnDefs={columnDefs} columnDefs={columnDefs}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
context={{ openRow }}
rowSelection="single" rowSelection="single"
pagination pagination
paginationPageSize={PAGE_SIZE} paginationPageSize={PAGE_SIZE}
animateRows animateRows
onRowDoubleClicked={handleRowDoubleClicked} onRowDoubleClicked={handleRowDoubleClicked}
onCellContextMenu={handleCellContextMenu} onCellContextMenu={handleCellContextMenu}
getRowId={(params) => params?.data?.id || params?.data?.relPath || params?.data?.fileName || String(params?.rowIndex ?? "")} getRowId={(params) =>
params?.data?.id || params?.data?.relPath || params?.data?.fileName || String(params?.rowIndex ?? "")
}
theme={myTheme} theme={myTheme}
rowHeight={44}
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
@@ -592,11 +664,7 @@ export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
> >
Cancel Cancel
</Button> </Button>
<Button <Button onClick={handleCreateScript} disabled={!scriptName.trim()} sx={{ textTransform: "none", color: BOREALIS_BLUE }}>
onClick={handleCreateScript}
disabled={!scriptName.trim()}
sx={{ textTransform: "none", color: BOREALIS_BLUE }}
>
Create Create
</Button> </Button>
</DialogActions> </DialogActions>