diff --git a/Data/Engine/web-interface/src/Assemblies/Assembly_List.jsx b/Data/Engine/web-interface/src/Assemblies/Assembly_List.jsx
index 5f5fc1dd..95457e39 100644
--- a/Data/Engine/web-interface/src/Assemblies/Assembly_List.jsx
+++ b/Data/Engine/web-interface/src/Assemblies/Assembly_List.jsx
@@ -1,777 +1,617 @@
-import React, { useState, useEffect, useCallback } from "react";
-import { Paper, Box, Typography, Menu, MenuItem, Button } from "@mui/material";
-import { Folder as FolderIcon, Description as DescriptionIcon, Polyline as WorkflowsIcon, Code as ScriptIcon, MenuBook as BookIcon } from "@mui/icons-material";
+import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import {
- SimpleTreeView,
- TreeItem,
- useTreeViewApiRef
-} from "@mui/x-tree-view";
-import {
- RenameWorkflowDialog,
- RenameFolderDialog,
- NewWorkflowDialog,
- ConfirmDeleteDialog
-} from "../Dialogs";
+ Paper,
+ Box,
+ Typography,
+ Button,
+ Menu,
+ MenuItem,
+ IconButton,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ CircularProgress,
+} from "@mui/material";
+import AddIcon from "@mui/icons-material/Add";
+import CachedIcon from "@mui/icons-material/Cached";
+import PolylineIcon from "@mui/icons-material/Polyline";
+import CodeIcon from "@mui/icons-material/Code";
+import MenuBookIcon from "@mui/icons-material/MenuBook";
+import { AgGridReact } from "ag-grid-react";
+import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
+import { ConfirmDeleteDialog, NewWorkflowDialog } from "../Dialogs";
-// Generic Island wrapper with large icon, stacked title/description, and actions on the right
-const Island = ({ title, description, icon, actions, children, sx }) => (
-
-
-
- {icon ? (
-
- {icon}
-
- ) : null}
-
-
- {title}
-
- {description ? (
-
- {description}
-
- ) : null}
-
-
- {actions ? (
-
- {actions}
-
- ) : null}
-
- {children}
-
-);
+ModuleRegistry.registerModules([AllCommunityModule]);
-// ---------------- Workflows Island -----------------
-const sortTree = (node) => {
- if (!node || !Array.isArray(node.children)) return;
- node.children.sort((a, b) => {
- const aFolder = Boolean(a.isFolder);
- const bFolder = Boolean(b.isFolder);
- if (aFolder !== bFolder) return aFolder ? -1 : 1;
- return String(a.label || "").localeCompare(String(b.label || ""), undefined, {
- sensitivity: "base"
- });
- });
- node.children.forEach(sortTree);
+const myTheme = themeQuartz.withParams({
+ accentColor: "#FFA6FF",
+ backgroundColor: "#1f2836",
+ browserColorScheme: "dark",
+ chromeBackgroundColor: {
+ ref: "foregroundColor",
+ mix: 0.07,
+ onto: "backgroundColor",
+ },
+ fontFamily: {
+ googleFont: "IBM Plex Sans",
+ },
+ foregroundColor: "#FFF",
+ headerFontSize: 14,
+});
+
+const themeClassName = myTheme.themeName || "ag-theme-quartz";
+const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
+const iconFontFamily = '"Quartz Regular"';
+const BOREALIS_BLUE = "#58a6ff";
+const PAGE_SIZE = 25;
+
+const TYPE_METADATA = {
+ workflows: {
+ label: "Workflow",
+ Icon: PolylineIcon,
+ },
+ scripts: {
+ label: "Script",
+ Icon: CodeIcon,
+ },
+ ansible: {
+ label: "Playbook",
+ Icon: MenuBookIcon,
+ },
};
-function buildWorkflowTree(workflows, folders) {
- const map = {};
- const rootNode = { id: "root", label: "Workflows", path: "", isFolder: true, children: [] };
- map[rootNode.id] = rootNode;
- (folders || []).forEach((f) => {
- const parts = (f || "").split("/");
- let children = rootNode.children;
- let parentPath = "";
- parts.forEach((part) => {
- const path = parentPath ? `${parentPath}/${part}` : part;
- let node = children.find((n) => n.id === path);
- if (!node) {
- node = { id: path, label: part, path, isFolder: true, children: [] };
- children.push(node);
- map[path] = node;
- }
- children = node.children;
- parentPath = path;
- });
- });
- (workflows || []).forEach((w) => {
- const parts = (w.rel_path || "").split("/");
- let children = rootNode.children;
- let parentPath = "";
- parts.forEach((part, idx) => {
- const path = parentPath ? `${parentPath}/${part}` : part;
- const isFile = idx === parts.length - 1;
- let node = children.find((n) => n.id === path);
- if (!node) {
- node = {
- id: path,
- label: isFile ? ((w.tab_name && w.tab_name.trim()) || w.file_name) : part,
- path,
- isFolder: !isFile,
- fileName: w.file_name,
- workflow: isFile ? w : null,
- children: []
- };
- children.push(node);
- map[path] = node;
- }
- if (!isFile) {
- children = node.children;
- parentPath = path;
- }
- });
- });
- sortTree(rootNode);
- return { root: [rootNode], map };
-}
+const TypeCellRenderer = React.memo(function TypeCellRenderer(props) {
+ const typeKey = props?.data?.typeKey;
+ const meta = typeKey ? TYPE_METADATA[typeKey] : null;
+ if (!meta) {
+ return null;
+ }
+ const { Icon, label } = meta;
+ return (
+
+
+
+ {label}
+
+
+ );
+});
-function WorkflowsIsland({ onOpenWorkflow }) {
- const [tree, setTree] = useState([]);
- const [nodeMap, setNodeMap] = useState({});
- const [contextMenu, setContextMenu] = useState(null);
- const [selectedNode, setSelectedNode] = useState(null);
- const [renameValue, setRenameValue] = useState("");
- const [renameOpen, setRenameOpen] = useState(false);
- const [renameFolderOpen, setRenameFolderOpen] = useState(false);
- const [folderDialogMode, setFolderDialogMode] = useState("rename");
- const [newWorkflowOpen, setNewWorkflowOpen] = useState(false);
- const [newWorkflowName, setNewWorkflowName] = useState("");
- const [deleteOpen, setDeleteOpen] = useState(false);
- const apiRef = useTreeViewApiRef();
- const [dragNode, setDragNode] = useState(null);
-
- const handleDrop = async (target) => {
- if (!dragNode || !target.isFolder) return;
- if (dragNode.path === target.path || target.path.startsWith(`${dragNode.path}/`)) {
- setDragNode(null);
- return;
- }
- const newPath = target.path ? `${target.path}/${dragNode.fileName}` : dragNode.fileName;
- try {
- await fetch("/api/assembly/move", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'file', path: dragNode.path, new_path: newPath })
- });
- loadTree();
- } catch (err) {
- console.error("Failed to move workflow:", err);
- }
- setDragNode(null);
+const normalizeRow = (island, item) => {
+ const relPath = String(item?.rel_path || "").replace(/\\/g, "/");
+ const fileName = String(item?.file_name || relPath.split("/").pop() || "");
+ const folder = relPath ? relPath.split("/").slice(0, -1).join("/") : "";
+ const idSeed = relPath || fileName || `${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ const name =
+ island === "workflows"
+ ? item?.tab_name || fileName.replace(/\.[^.]+$/, "") || fileName || "Workflow"
+ : item?.name || fileName.replace(/\.[^.]+$/, "") || fileName || "Assembly";
+ const category =
+ island === "workflows"
+ ? folder || "Workflows"
+ : item?.category || "";
+ const description =
+ island === "workflows" ? "" : item?.description || "";
+ return {
+ id: `${island}:${idSeed}`,
+ typeKey: island,
+ name,
+ category,
+ description,
+ relPath,
+ fileName,
+ folder,
+ raw: item || {},
};
+};
- const loadTree = useCallback(async () => {
+export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
+ const gridRef = useRef(null);
+ const [rows, setRows] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const [newMenuAnchor, setNewMenuAnchor] = useState(null);
+ const [scriptDialog, setScriptDialog] = useState({ open: false, island: null });
+ const [scriptName, setScriptName] = useState("");
+ const [workflowDialogOpen, setWorkflowDialogOpen] = useState(false);
+ const [workflowName, setWorkflowName] = useState("");
+
+ const [contextMenu, setContextMenu] = useState(null);
+ const [activeRow, setActiveRow] = useState(null);
+
+ const [renameDialogOpen, setRenameDialogOpen] = useState(false);
+ const [renameValue, setRenameValue] = useState("");
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+
+ const fetchAssemblies = useCallback(async () => {
+ setLoading(true);
+ setError("");
try {
- const resp = await fetch(`/api/assembly/list?island=workflows`);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
- const { root, map } = buildWorkflowTree(data.items || [], data.folders || []);
- setTree(root);
- setNodeMap(map);
+ const islands = ["workflows", "scripts", "ansible"];
+ const results = await Promise.all(
+ islands.map(async (island) => {
+ const resp = await fetch(`/api/assembly/list?island=${encodeURIComponent(island)}`);
+ if (!resp.ok) {
+ const problem = await resp.text();
+ throw new Error(problem || `Failed to load ${island} assemblies (HTTP ${resp.status})`);
+ }
+ const data = await resp.json();
+ const items = Array.isArray(data?.items) ? data.items : [];
+ return items.map((item) => normalizeRow(island, item));
+ }),
+ );
+ setRows(results.flat());
} catch (err) {
- console.error("Failed to load workflows:", err);
- setTree([]);
- setNodeMap({});
+ console.error("Failed to load assemblies:", err);
+ setRows([]);
+ setError(err?.message || "Failed to load assemblies");
+ } finally {
+ setLoading(false);
}
}, []);
- useEffect(() => { loadTree(); }, [loadTree]);
+ useEffect(() => {
+ fetchAssemblies();
+ }, [fetchAssemblies]);
- const handleContextMenu = (e, node) => {
- e.preventDefault();
- setSelectedNode(node);
- setContextMenu(
- contextMenu === null ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 } : null
- );
- };
-
- const handleRename = () => {
- setContextMenu(null);
- if (!selectedNode) return;
- setRenameValue(selectedNode.label);
- if (selectedNode.isFolder) {
- setFolderDialogMode("rename");
- setRenameFolderOpen(true);
- } else setRenameOpen(true);
- };
-
- const handleEdit = () => {
- setContextMenu(null);
- if (selectedNode && !selectedNode.isFolder && onOpenWorkflow) {
- onOpenWorkflow(selectedNode.workflow);
- }
- };
-
- const handleDelete = () => {
- setContextMenu(null);
- if (!selectedNode) return;
- setDeleteOpen(true);
- };
-
- const handleNewFolder = () => {
- if (!selectedNode) return;
- setContextMenu(null);
- setFolderDialogMode("create");
- setRenameValue("");
- setRenameFolderOpen(true);
- };
-
- const handleNewWorkflow = () => {
- if (!selectedNode) return;
- setContextMenu(null);
- setNewWorkflowName("");
- setNewWorkflowOpen(true);
- };
-
- const saveRenameWorkflow = async () => {
- if (!selectedNode) return;
- try {
- await fetch("/api/assembly/rename", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'file', path: selectedNode.path, new_name: renameValue })
- });
- loadTree();
- } catch (err) {
- console.error("Failed to rename workflow:", err);
- }
- setRenameOpen(false);
- };
-
- const saveRenameFolder = async () => {
- try {
- if (folderDialogMode === "rename" && selectedNode) {
- await fetch("/api/assembly/rename", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'folder', path: selectedNode.path, new_name: renameValue })
- });
- } else {
- const basePath = selectedNode ? selectedNode.path : "";
- const newPath = basePath ? `${basePath}/${renameValue}` : renameValue;
- await fetch("/api/assembly/create", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'folder', path: newPath })
- });
- }
- loadTree();
- } catch (err) {
- console.error("Folder operation failed:", err);
- }
- setRenameFolderOpen(false);
- };
-
- const handleNodeSelect = (_event, itemId) => {
- const node = nodeMap[itemId];
- if (node && !node.isFolder && onOpenWorkflow) {
- onOpenWorkflow(node.workflow);
- }
- };
-
- const confirmDelete = async () => {
- if (!selectedNode) return;
- try {
- if (selectedNode.isFolder) {
- await fetch("/api/assembly/delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'folder', path: selectedNode.path })
- });
- } else {
- await fetch("/api/assembly/delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island: 'workflows', kind: 'file', path: selectedNode.path })
- });
- }
- loadTree();
- } catch (err) {
- console.error("Failed to delete:", err);
- }
- setDeleteOpen(false);
- };
-
- const renderItems = (nodes) =>
- (nodes || []).map((n) => (
- !n.isFolder && setDragNode(n)}
- onDragOver={(e) => { if (dragNode && n.isFolder) e.preventDefault(); }}
- onDrop={(e) => { e.preventDefault(); handleDrop(n); }}
- onContextMenu={(e) => handleContextMenu(e, n)}
- >
- {n.isFolder ? (
-
- ) : (
-
- )}
- {n.label}
-
- }
- >
- {n.children && n.children.length > 0 ? renderItems(n.children) : null}
-
- ));
-
- const rootChildIds = tree[0]?.children?.map((c) => c.id) || [];
-
- return (
- }
- actions={
-
- }
- >
- { if (dragNode) e.preventDefault(); }}
- onDrop={(e) => { e.preventDefault(); handleDrop({ path: "", isFolder: true }); }}
- >
-
- {renderItems(tree)}
-
-
-
- setRenameOpen(false)} onSave={saveRenameWorkflow} />
- setRenameFolderOpen(false)} onSave={saveRenameFolder} title={folderDialogMode === "rename" ? "Rename Folder" : "New Folder"} confirmText={folderDialogMode === "rename" ? "Save" : "Create"} />
- setNewWorkflowOpen(false)} onCreate={() => { setNewWorkflowOpen(false); onOpenWorkflow && onOpenWorkflow(null, selectedNode?.path || "", newWorkflowName); }} />
- setDeleteOpen(false)} onConfirm={confirmDelete} />
-
- );
-}
-
-// ---------------- Generic Scripts-like Islands (used for Scripts and Ansible) -----------------
-function buildFileTree(rootLabel, items, folders) {
- // Some backends (e.g. /api/scripts) return paths relative to
- // the Assemblies root, which prefixes items with a top-level
- // folder like "Scripts". Others (e.g. /api/ansible) already
- // return paths relative to their specific root. Normalize by
- // stripping a matching top-level segment so the UI shows
- // "Scripts/<...>" rather than "Scripts/Scripts/<...>".
- const normalize = (p) => {
- const candidates = [
- String(rootLabel || "").trim(),
- String(rootLabel || "").replace(/\s+/g, "_")
- ].filter(Boolean);
- const parts = String(p || "").replace(/\\/g, "/").split("/").filter(Boolean);
- if (parts.length && candidates.includes(parts[0])) parts.shift();
- return parts;
- };
-
- const map = {};
- const rootNode = { id: "root", label: rootLabel, path: "", isFolder: true, children: [] };
- map[rootNode.id] = rootNode;
-
- (folders || []).forEach((f) => {
- const parts = normalize(f);
- let children = rootNode.children;
- let parentPath = "";
- parts.forEach((part) => {
- const path = parentPath ? `${parentPath}/${part}` : part;
- let node = children.find((n) => n.id === path);
- if (!node) {
- node = { id: path, label: part, path, isFolder: true, children: [] };
- children.push(node);
- map[path] = node;
- }
- children = node.children;
- parentPath = path;
- });
- });
-
- (items || []).forEach((s) => {
- const parts = normalize(s?.rel_path);
- let children = rootNode.children;
- let parentPath = "";
- parts.forEach((part, idx) => {
- const path = parentPath ? `${parentPath}/${part}` : part;
- const isFile = idx === parts.length - 1;
- let node = children.find((n) => n.id === path);
- if (!node) {
- node = {
- id: path,
- label: isFile ? (s.name || s.display_name || s.file_name || part) : part,
- path,
- isFolder: !isFile,
- fileName: s.file_name,
- meta: isFile ? s : null,
- children: []
+ const openRow = useCallback(
+ (row) => {
+ if (!row) return;
+ if (row.typeKey === "workflows") {
+ const payload = {
+ ...row.raw,
+ rel_path: row.relPath,
+ file_name: row.fileName,
};
- children.push(node);
- map[path] = node;
+ if (!payload.name) payload.name = row.name;
+ if (!payload.tab_name) payload.tab_name = row.name;
+ if (onOpenWorkflow) {
+ onOpenWorkflow(payload);
+ }
+ return;
}
- if (!isFile) {
- children = node.children;
- parentPath = path;
+ const mode = row.typeKey === "ansible" ? "ansible" : "scripts";
+ if (onOpenScript) {
+ onOpenScript(row.relPath, mode, null);
}
- });
- });
- sortTree(rootNode);
- return { root: [rootNode], map };
-}
+ },
+ [onOpenWorkflow, onOpenScript],
+ );
-function ScriptsLikeIsland({
- title,
- description,
- rootLabel,
- baseApi, // e.g. '/api/scripts' or '/api/ansible'
- newItemLabel = "New Script",
- onEdit // (rel_path) => void
-}) {
- const [tree, setTree] = useState([]);
- const [nodeMap, setNodeMap] = useState({});
- const [contextMenu, setContextMenu] = useState(null);
- const [selectedNode, setSelectedNode] = useState(null);
- const [renameValue, setRenameValue] = useState("");
- const [renameOpen, setRenameOpen] = useState(false);
- const [renameFolderOpen, setRenameFolderOpen] = useState(false);
- const [folderDialogMode, setFolderDialogMode] = useState("rename");
- const [newItemOpen, setNewItemOpen] = useState(false);
- const [newItemName, setNewItemName] = useState("");
- const [deleteOpen, setDeleteOpen] = useState(false);
- const apiRef = useTreeViewApiRef();
- const [dragNode, setDragNode] = useState(null);
+ const handleRowDoubleClicked = useCallback(
+ (event) => {
+ openRow(event?.data);
+ },
+ [openRow],
+ );
- const island = React.useMemo(() => {
- const b = String(baseApi || '').toLowerCase();
- return b.endsWith('/api/ansible') ? 'ansible' : 'scripts';
- }, [baseApi]);
-
- const loadTree = useCallback(async () => {
- try {
- const resp = await fetch(`/api/assembly/list?island=${encodeURIComponent(island)}`);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
- const { root, map } = buildFileTree(rootLabel, data.items || [], data.folders || []);
- setTree(root);
- setNodeMap(map);
- } catch (err) {
- console.error(`Failed to load ${title}:`, err);
- setTree([]);
- setNodeMap({});
- }
- }, [island, title, rootLabel]);
-
- useEffect(() => { loadTree(); }, [loadTree]);
-
- const handleContextMenu = (e, node) => {
- e.preventDefault();
- setSelectedNode(node);
+ const handleCellContextMenu = useCallback((params) => {
+ params.event?.preventDefault();
+ setActiveRow(params?.data || null);
setContextMenu(
- contextMenu === null ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 } : null
+ params?.event
+ ? {
+ mouseX: params.event.clientX + 2,
+ mouseY: params.event.clientY - 6,
+ }
+ : null,
);
+ }, []);
+
+ const closeContextMenu = () => setContextMenu(null);
+
+ const startRename = () => {
+ if (!activeRow) return;
+ setRenameValue(activeRow.name || activeRow.fileName || "");
+ setRenameDialogOpen(true);
+ closeContextMenu();
};
- const handleDrop = async (target) => {
- if (!dragNode || !target.isFolder) return;
- if (dragNode.path === target.path || target.path.startsWith(`${dragNode.path}/`)) {
- setDragNode(null);
+ const startDelete = () => {
+ if (!activeRow) return;
+ setDeleteDialogOpen(true);
+ closeContextMenu();
+ };
+
+ const handleRenameSave = async () => {
+ const target = activeRow;
+ const trimmed = renameValue.trim();
+ if (!target || !trimmed) {
+ setRenameDialogOpen(false);
return;
}
- const newPath = target.path ? `${target.path}/${dragNode.fileName}` : dragNode.fileName;
try {
- await fetch(`/api/assembly/move`, {
+ const payload = {
+ island: target.typeKey,
+ kind: "file",
+ path: target.relPath,
+ new_name: trimmed,
+ };
+ if (target.typeKey !== "workflows" && target.raw?.type) {
+ payload.type = target.raw.type;
+ }
+ const resp = await fetch(`/api/assembly/rename`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island, kind: 'file', path: dragNode.path, new_path: newPath })
+ body: JSON.stringify(payload),
});
- loadTree();
+ const data = await resp.json();
+ if (!resp.ok) {
+ throw new Error(data?.error || `HTTP ${resp.status}`);
+ }
+ setRenameDialogOpen(false);
+ await fetchAssemblies();
} catch (err) {
- console.error("Failed to move:", err);
- }
- setDragNode(null);
- };
-
- const handleNodeSelect = async (_e, itemId) => {
- const node = nodeMap[itemId];
- if (node && !node.isFolder) {
- setContextMenu(null);
- onEdit && onEdit(node.path);
+ console.error("Failed to rename assembly:", err);
+ setRenameDialogOpen(false);
}
};
- const saveRenameFile = async () => {
+ const handleDeleteConfirm = async () => {
+ const target = activeRow;
+ if (!target) {
+ setDeleteDialogOpen(false);
+ return;
+ }
try {
- const payload = { island, kind: 'file', path: selectedNode.path, new_name: renameValue };
- // preserve extension for scripts when no extension provided
- if (selectedNode?.meta?.type) payload.type = selectedNode.meta.type;
- const res = await fetch(`/api/assembly/rename`, {
+ const resp = await fetch(`/api/assembly/delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload)
+ body: JSON.stringify({
+ island: target.typeKey,
+ kind: "file",
+ path: target.relPath,
+ }),
});
- const data = await res.json();
- if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
- setRenameOpen(false);
- loadTree();
- } catch (err) {
- console.error("Failed to rename file:", err);
- setRenameOpen(false);
- }
- };
-
- const saveRenameFolder = async () => {
- try {
- if (folderDialogMode === "rename" && selectedNode) {
- await fetch(`/api/assembly/rename`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island, kind: 'folder', path: selectedNode.path, new_name: renameValue })
- });
- } else {
- const basePath = selectedNode ? selectedNode.path : "";
- const newPath = basePath ? `${basePath}/${renameValue}` : renameValue;
- await fetch(`/api/assembly/create`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island, kind: 'folder', path: newPath })
- });
+ const data = await resp.json();
+ if (!resp.ok) {
+ throw new Error(data?.error || `HTTP ${resp.status}`);
}
- setRenameFolderOpen(false);
- loadTree();
+ setDeleteDialogOpen(false);
+ await fetchAssemblies();
} catch (err) {
- console.error("Folder operation failed:", err);
- setRenameFolderOpen(false);
+ console.error("Failed to delete assembly:", err);
+ setDeleteDialogOpen(false);
}
};
- const confirmDelete = async () => {
- if (!selectedNode) return;
- try {
- if (selectedNode.isFolder) {
- await fetch(`/api/assembly/delete`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island, kind: 'folder', path: selectedNode.path })
- });
- } else {
- await fetch(`/api/assembly/delete`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ island, kind: 'file', path: selectedNode.path })
- });
- }
- setDeleteOpen(false);
- loadTree();
- } catch (err) {
- console.error("Failed to delete:", err);
- setDeleteOpen(false);
+ const columnDefs = useMemo(
+ () => [
+ {
+ field: "assemblyType",
+ headerName: "Assembly Type",
+ valueGetter: (params) => TYPE_METADATA[params?.data?.typeKey]?.label || "",
+ cellRenderer: TypeCellRenderer,
+ width: 200,
+ minWidth: 180,
+ flex: 0,
+ sortable: true,
+ filter: "agTextColumnFilter",
+ },
+ {
+ field: "category",
+ headerName: "Category",
+ valueGetter: (params) => params?.data?.category || "",
+ },
+ {
+ field: "name",
+ headerName: "Name",
+ valueGetter: (params) => params?.data?.name || "",
+ },
+ {
+ field: "description",
+ headerName: "Description",
+ valueGetter: (params) => params?.data?.description || "",
+ },
+ ],
+ [],
+ );
+
+ const defaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ filter: "agTextColumnFilter",
+ floatingFilter: true,
+ resizable: true,
+ flex: 1,
+ minWidth: 180,
+ }),
+ [],
+ );
+
+ const gridWrapperClass = themeClassName;
+
+ const handleRefresh = () => fetchAssemblies();
+
+ const handleNewAssemblyOption = (island) => {
+ setNewMenuAnchor(null);
+ if (island === "workflows") {
+ setWorkflowName("");
+ setWorkflowDialogOpen(true);
+ return;
}
+ setScriptName("");
+ setScriptDialog({ open: true, island });
};
- const createNewItem = () => {
- const trimmedName = (newItemName || '').trim();
- const folder = selectedNode?.isFolder
- ? selectedNode.path
- : (selectedNode?.path?.split("/").slice(0, -1).join("/") || "");
+ const handleCreateScript = () => {
+ const trimmed = scriptName.trim();
+ if (!trimmed || !scriptDialog.island) {
+ return;
+ }
+ const isAnsible = scriptDialog.island === "ansible";
const context = {
- folder,
- suggestedFileName: trimmedName,
- defaultType: island === 'ansible' ? 'ansible' : 'powershell',
- type: island === 'ansible' ? 'ansible' : 'powershell',
- category: island === 'ansible' ? 'application' : 'script'
+ folder: "",
+ suggestedFileName: trimmed,
+ name: trimmed,
+ defaultType: isAnsible ? "ansible" : "powershell",
+ type: isAnsible ? "ansible" : "powershell",
+ category: isAnsible ? "application" : "script",
};
- setNewItemOpen(false);
- setNewItemName("");
- onEdit && onEdit(null, context);
+ if (onOpenScript) {
+ onOpenScript(null, isAnsible ? "ansible" : "scripts", context);
+ }
+ setScriptDialog({ open: false, island: null });
+ setScriptName("");
};
- const renderItems = (nodes) =>
- (nodes || []).map((n) => (
- !n.isFolder && setDragNode(n)}
- onDragOver={(e) => { if (dragNode && n.isFolder) e.preventDefault(); }}
- onDrop={(e) => { e.preventDefault(); handleDrop(n); }}
- onContextMenu={(e) => handleContextMenu(e, n)}
- onDoubleClick={() => { if (!n.isFolder) onEdit && onEdit(n.path); }}
- >
- {n.isFolder ? (
-
- ) : (
-
- )}
- {n.label}
-
- }
- >
- {n.children && n.children.length > 0 ? renderItems(n.children) : null}
-
- ));
-
- const rootChildIds = tree[0]?.children?.map((c) => c.id) || [];
+ const handleCreateWorkflow = () => {
+ const trimmed = workflowName.trim();
+ if (!trimmed) {
+ return;
+ }
+ setWorkflowDialogOpen(false);
+ if (onOpenWorkflow) {
+ onOpenWorkflow(null, "", trimmed);
+ }
+ setWorkflowName("");
+ };
return (
- : }
- actions={
-
- }
+
- { if (dragNode) e.preventDefault(); }}
- onDrop={(e) => { e.preventDefault(); handleDrop({ path: "", isFolder: true }); }}
- >
-
- {renderItems(tree)}
-
+
+
+ Assemblies
+
+
+ Collections of scripts, workflows, and playbooks used to automate tasks across devices.
+
+
+
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ }
+ onClick={(event) => setNewMenuAnchor(event.currentTarget)}
+ sx={{
+ bgcolor: BOREALIS_BLUE,
+ "&:hover": { bgcolor: "#3975c7" },
+ textTransform: "none",
+ }}
+ >
+ New Assembly
+
+
+
+
+
+ params?.data?.id || params?.data?.relPath || params?.data?.fileName || String(params?.rowIndex ?? "")}
+ theme={myTheme}
+ style={{
+ width: "100%",
+ height: "100%",
+ fontFamily: gridFontFamily,
+ "--ag-icon-font-family": iconFontFamily,
+ }}
+ />
+ {loading ? (
+
+
+
+ ) : null}
+
+
+
- {/* Simple inline dialogs using shared components */}
- setRenameFolderOpen(false)} onSave={saveRenameFolder} title={folderDialogMode === "rename" ? "Rename Folder" : "New Folder"} confirmText={folderDialogMode === "rename" ? "Save" : "Create"} />
- {/* File rename */}
- } sx={{ display: renameOpen ? 'block' : 'none' }}>
-
-
- } sx={{ display: newItemOpen ? 'block' : 'none' }}>
-
-
- setDeleteOpen(false)} onConfirm={confirmDelete} />
-
- );
-}
-export default function AssemblyList({ onOpenWorkflow, onOpenScript }) {
- return (
-
-
- Assemblies
- Collections of various types of components used to perform various automations upon targeted devices.
-
-
-
- {/* Left: Workflows */}
-
+
- {/* Middle: Scripts */}
- onOpenScript && onOpenScript(rel, 'scripts', ctx)}
- />
+ setDeleteDialogOpen(false)}
+ onConfirm={handleDeleteConfirm}
+ />
- {/* Right: Ansible Playbooks */}
- onOpenScript && onOpenScript(rel, 'ansible', ctx)}
- />
-
-
+
+
+ {
+ setWorkflowDialogOpen(false);
+ setWorkflowName("");
+ }}
+ onCreate={handleCreateWorkflow}
+ />
);
}