mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 09:25:48 -07:00
Redesigned Assembly List
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user