mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 09:25:48 -07:00
Assembly Execution (SYSTEM & CURRENTUSER) Resolved
This commit is contained in:
@@ -54,6 +54,14 @@ import "prismjs/themes/prism-okaidia.css";
|
||||
import Editor from "react-simple-code-editor";
|
||||
import ReactFlow, { Handle, Position } from "reactflow";
|
||||
import "reactflow/dist/style.css";
|
||||
import { DomainBadge } from "../Assemblies/Assembly_Badges";
|
||||
import {
|
||||
buildAssemblyIndex,
|
||||
buildAssemblyTree,
|
||||
normalizeAssemblyPath,
|
||||
parseAssemblyExport,
|
||||
resolveAssemblyForComponent
|
||||
} from "../Assemblies/assemblyUtils";
|
||||
|
||||
const hiddenHandleStyle = {
|
||||
width: 12,
|
||||
@@ -149,75 +157,6 @@ function renderTreeNodes(nodes = [], map = {}) {
|
||||
));
|
||||
}
|
||||
|
||||
// --- Scripts tree helpers (reuse approach from Quick_Job) ---
|
||||
function buildScriptTree(scripts, folders) {
|
||||
const map = {};
|
||||
const rootNode = { id: "root_s", label: "Scripts", 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;
|
||||
});
|
||||
});
|
||||
(scripts || []).forEach((s) => {
|
||||
const parts = (s.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 ? (s.name || s.file_name || part) : part, path, isFolder: !isFile, fileName: s.file_name, script: isFile ? s : null, children: [] };
|
||||
children.push(node); map[path] = node;
|
||||
}
|
||||
if (!isFile) { children = node.children; parentPath = path; }
|
||||
});
|
||||
});
|
||||
return { root: [rootNode], map };
|
||||
}
|
||||
|
||||
// --- Ansible tree helpers (reuse scripts tree builder) ---
|
||||
function buildAnsibleTree(playbooks, folders) {
|
||||
return buildScriptTree(playbooks, folders);
|
||||
}
|
||||
|
||||
// --- Workflows tree helpers (reuse approach from Workflow_List) ---
|
||||
function buildWorkflowTree(workflows, folders) {
|
||||
const map = {};
|
||||
const rootNode = { id: "root_w", 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?.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; }
|
||||
});
|
||||
});
|
||||
return { root: [rootNode], map };
|
||||
}
|
||||
|
||||
function normalizeVariableDefinitions(vars = []) {
|
||||
return (Array.isArray(vars) ? vars : [])
|
||||
.map((raw) => {
|
||||
@@ -342,9 +281,12 @@ function ComponentCard({ comp, onRemove, onVariableChange, errors = {} }) {
|
||||
<Paper sx={{ bgcolor: "#2a2a2a", border: "1px solid #3a3a3a", p: 1.2, mb: 1.2, borderRadius: 1 }}>
|
||||
<Box sx={{ display: "flex", gap: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: "#e6edf3" }}>
|
||||
{comp.type === "script" ? comp.name : comp.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: "#e6edf3" }}>
|
||||
{comp.name}
|
||||
</Typography>
|
||||
{comp.domain ? <DomainBadge domain={comp.domain} size="small" /> : null}
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
{description}
|
||||
</Typography>
|
||||
@@ -430,6 +372,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
const [credentialError, setCredentialError] = useState("");
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState("");
|
||||
const [useSvcAccount, setUseSvcAccount] = useState(true);
|
||||
const [assembliesPayload, setAssembliesPayload] = useState({ items: [], queue: [] });
|
||||
const [assembliesLoading, setAssembliesLoading] = useState(false);
|
||||
const [assembliesError, setAssembliesError] = useState("");
|
||||
const assemblyExportCacheRef = useRef(new Map());
|
||||
|
||||
const loadCredentials = useCallback(async () => {
|
||||
setCredentialLoading(true);
|
||||
@@ -449,9 +395,73 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadAssemblies = useCallback(async () => {
|
||||
setAssembliesLoading(true);
|
||||
setAssembliesError("");
|
||||
try {
|
||||
const resp = await fetch("/api/assemblies");
|
||||
if (!resp.ok) {
|
||||
const detail = await resp.text();
|
||||
throw new Error(detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
assemblyExportCacheRef.current.clear();
|
||||
setAssembliesPayload({
|
||||
items: Array.isArray(data?.items) ? data.items : [],
|
||||
queue: Array.isArray(data?.queue) ? data.queue : []
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to load assemblies:", err);
|
||||
setAssembliesPayload({ items: [], queue: [] });
|
||||
setAssembliesError(err?.message || "Failed to load assemblies");
|
||||
} finally {
|
||||
setAssembliesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const assemblyIndex = useMemo(
|
||||
() => buildAssemblyIndex(assembliesPayload.items, assembliesPayload.queue),
|
||||
[assembliesPayload.items, assembliesPayload.queue]
|
||||
);
|
||||
const scriptTreeData = useMemo(
|
||||
() => buildAssemblyTree(assemblyIndex.grouped?.scripts || [], { rootLabel: "Scripts" }),
|
||||
[assemblyIndex]
|
||||
);
|
||||
const ansibleTreeData = useMemo(
|
||||
() => buildAssemblyTree(assemblyIndex.grouped?.ansible || [], { rootLabel: "Ansible Playbooks" }),
|
||||
[assemblyIndex]
|
||||
);
|
||||
const workflowTreeData = useMemo(
|
||||
() => buildAssemblyTree(assemblyIndex.grouped?.workflows || [], { rootLabel: "Workflows" }),
|
||||
[assemblyIndex]
|
||||
);
|
||||
|
||||
const loadAssemblyExport = useCallback(
|
||||
async (assemblyGuid) => {
|
||||
const cacheKey = assemblyGuid.toLowerCase();
|
||||
if (assemblyExportCacheRef.current.has(cacheKey)) {
|
||||
return assemblyExportCacheRef.current.get(cacheKey);
|
||||
}
|
||||
const resp = await fetch(`/api/assemblies/${encodeURIComponent(assemblyGuid)}/export`);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to load assembly (HTTP ${resp.status})`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
assemblyExportCacheRef.current.set(cacheKey, data);
|
||||
return data;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadCredentials();
|
||||
}, [loadCredentials]);
|
||||
useEffect(() => {
|
||||
loadAssemblies();
|
||||
}, [loadAssemblies]);
|
||||
useEffect(() => {
|
||||
setSelectedNodeId("");
|
||||
}, [compTab]);
|
||||
|
||||
const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]);
|
||||
const handleExecContextChange = useCallback((value) => {
|
||||
@@ -490,9 +500,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
// dialogs state
|
||||
const [addCompOpen, setAddCompOpen] = useState(false);
|
||||
const [compTab, setCompTab] = useState("scripts");
|
||||
const [scriptTree, setScriptTree] = useState([]); const [scriptMap, setScriptMap] = useState({});
|
||||
const [workflowTree, setWorkflowTree] = useState([]); const [workflowMap, setWorkflowMap] = useState({});
|
||||
const [ansibleTree, setAnsibleTree] = useState([]); const [ansibleMap, setAnsibleMap] = useState({});
|
||||
const [selectedNodeId, setSelectedNodeId] = useState("");
|
||||
|
||||
const [addTargetOpen, setAddTargetOpen] = useState(false);
|
||||
@@ -757,41 +764,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
});
|
||||
return arr;
|
||||
}, [deviceFiltered, deviceOrder, deviceOrderBy]);
|
||||
|
||||
const normalizeComponentPath = useCallback((type, rawPath) => {
|
||||
const trimmed = (rawPath || "").replace(/\\/g, "/").replace(/^\/+/, "").trim();
|
||||
if (!trimmed) return "";
|
||||
if (type === "script") {
|
||||
return trimmed.startsWith("Scripts/") ? trimmed : `Scripts/${trimmed}`;
|
||||
}
|
||||
return trimmed;
|
||||
}, []);
|
||||
|
||||
const fetchAssemblyDoc = useCallback(async (type, rawPath) => {
|
||||
const normalizedPath = normalizeComponentPath(type, rawPath);
|
||||
if (!normalizedPath) return { doc: null, normalizedPath: "" };
|
||||
const trimmed = normalizedPath.replace(/\\/g, "/").replace(/^\/+/, "").trim();
|
||||
if (!trimmed) return { doc: null, normalizedPath: "" };
|
||||
let requestPath = trimmed;
|
||||
if (type === "script" && requestPath.toLowerCase().startsWith("scripts/")) {
|
||||
requestPath = requestPath.slice("Scripts/".length);
|
||||
} else if (type === "ansible" && requestPath.toLowerCase().startsWith("ansible_playbooks/")) {
|
||||
requestPath = requestPath.slice("Ansible_Playbooks/".length);
|
||||
}
|
||||
if (!requestPath) return { doc: null, normalizedPath };
|
||||
try {
|
||||
const island = type === "ansible" ? "ansible" : "scripts";
|
||||
const resp = await fetch(`/api/assembly/load?island=${island}&path=${encodeURIComponent(requestPath)}`);
|
||||
if (!resp.ok) {
|
||||
return { doc: null, normalizedPath };
|
||||
}
|
||||
const data = await resp.json();
|
||||
return { doc: data, normalizedPath };
|
||||
} catch {
|
||||
return { doc: null, normalizedPath };
|
||||
}
|
||||
}, [normalizeComponentPath]);
|
||||
|
||||
const hydrateExistingComponents = useCallback(async (rawComponents = []) => {
|
||||
const results = [];
|
||||
for (const raw of rawComponents) {
|
||||
@@ -806,24 +778,68 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const type = typeRaw === "ansible" ? "ansible" : "script";
|
||||
const basePath = raw.path || raw.script_path || raw.rel_path || "";
|
||||
const { doc, normalizedPath } = await fetchAssemblyDoc(type, basePath);
|
||||
const assembly = doc?.assembly || {};
|
||||
const docVars = assembly?.variables || doc?.variables || [];
|
||||
const kind = typeRaw === "ansible" ? "ansible" : "script";
|
||||
const assemblyGuidRaw = raw.assembly_guid || raw.assemblyGuid;
|
||||
let record = null;
|
||||
if (assemblyGuidRaw) {
|
||||
const guidKey = String(assemblyGuidRaw).trim().toLowerCase();
|
||||
record = assemblyIndex.byGuid?.get(guidKey) || null;
|
||||
}
|
||||
if (!record) {
|
||||
record = resolveAssemblyForComponent(assemblyIndex, raw, kind);
|
||||
}
|
||||
if (!record) {
|
||||
const fallbackPath =
|
||||
raw.path ||
|
||||
raw.script_path ||
|
||||
raw.playbook_path ||
|
||||
raw.rel_path ||
|
||||
raw.scriptPath ||
|
||||
raw.playbookPath ||
|
||||
"";
|
||||
const normalizedFallback = normalizeAssemblyPath(
|
||||
kind,
|
||||
fallbackPath,
|
||||
raw.name || raw.file_name || raw.tab_name || ""
|
||||
);
|
||||
record = assemblyIndex.byPath?.get(normalizedFallback.toLowerCase()) || null;
|
||||
}
|
||||
if (!record) {
|
||||
const mergedFallback = mergeComponentVariables([], raw.variables, raw.variable_values);
|
||||
results.push({
|
||||
...raw,
|
||||
type: kind,
|
||||
path: normalizeAssemblyPath(
|
||||
kind,
|
||||
raw.path || raw.script_path || raw.playbook_path || "",
|
||||
raw.name || raw.file_name || ""
|
||||
),
|
||||
name: raw.name || raw.file_name || raw.tab_name || raw.path || "Assembly",
|
||||
description: raw.description || raw.path || "",
|
||||
variables: mergedFallback,
|
||||
localId: generateLocalId()
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const exportDoc = await loadAssemblyExport(record.assemblyGuid);
|
||||
const parsed = parseAssemblyExport(exportDoc);
|
||||
const docVars = Array.isArray(parsed.rawVariables) ? parsed.rawVariables : [];
|
||||
const mergedVariables = mergeComponentVariables(docVars, raw.variables, raw.variable_values);
|
||||
results.push({
|
||||
...raw,
|
||||
type,
|
||||
path: normalizedPath || basePath,
|
||||
name: raw.name || assembly?.name || raw.file_name || raw.tab_name || normalizedPath || basePath,
|
||||
description: raw.description || assembly?.description || normalizedPath || basePath,
|
||||
type: kind,
|
||||
path: normalizeAssemblyPath(kind, record.path || "", record.displayName),
|
||||
name: raw.name || record.displayName,
|
||||
description: raw.description || record.summary || record.path,
|
||||
variables: mergedVariables,
|
||||
localId: generateLocalId()
|
||||
localId: generateLocalId(),
|
||||
assemblyGuid: record.assemblyGuid,
|
||||
domain: record.domain,
|
||||
domainLabel: record.domainLabel
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, [fetchAssemblyDoc, generateLocalId]);
|
||||
}, [assemblyIndex, loadAssemblyExport, mergeComponentVariables, generateLocalId]);
|
||||
|
||||
const sanitizeComponentsForSave = useCallback((items) => {
|
||||
return (Array.isArray(items) ? items : []).map((comp) => {
|
||||
@@ -1401,67 +1417,52 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
|
||||
const openAddComponent = async () => {
|
||||
setAddCompOpen(true);
|
||||
try {
|
||||
// scripts
|
||||
const sResp = await fetch("/api/assembly/list?island=scripts");
|
||||
if (sResp.ok) {
|
||||
const sData = await sResp.json();
|
||||
const { root, map } = buildScriptTree(sData.items || [], sData.folders || []);
|
||||
setScriptTree(root); setScriptMap(map);
|
||||
} else { setScriptTree([]); setScriptMap({}); }
|
||||
} catch { setScriptTree([]); setScriptMap({}); }
|
||||
try {
|
||||
// workflows
|
||||
const wResp = await fetch("/api/assembly/list?island=workflows");
|
||||
if (wResp.ok) {
|
||||
const wData = await wResp.json();
|
||||
const { root, map } = buildWorkflowTree(wData.items || [], wData.folders || []);
|
||||
setWorkflowTree(root); setWorkflowMap(map);
|
||||
} else { setWorkflowTree([]); setWorkflowMap({}); }
|
||||
} catch { setWorkflowTree([]); setWorkflowMap({}); }
|
||||
try {
|
||||
// ansible playbooks
|
||||
const aResp = await fetch("/api/assembly/list?island=ansible");
|
||||
if (aResp.ok) {
|
||||
const aData = await aResp.json();
|
||||
const { root, map } = buildAnsibleTree(aData.items || [], aData.folders || []);
|
||||
setAnsibleTree(root); setAnsibleMap(map);
|
||||
} else { setAnsibleTree([]); setAnsibleMap({}); }
|
||||
} catch { setAnsibleTree([]); setAnsibleMap({}); }
|
||||
setSelectedNodeId("");
|
||||
if (!assembliesPayload.items.length && !assembliesLoading) {
|
||||
loadAssemblies();
|
||||
}
|
||||
};
|
||||
|
||||
const addSelectedComponent = useCallback(async () => {
|
||||
const map = compTab === "scripts" ? scriptMap : (compTab === "ansible" ? ansibleMap : workflowMap);
|
||||
const node = map[selectedNodeId];
|
||||
const treeData =
|
||||
compTab === "ansible" ? ansibleTreeData : compTab === "workflows" ? workflowTreeData : scriptTreeData;
|
||||
const node = treeData.map[selectedNodeId];
|
||||
if (!node || node.isFolder) return false;
|
||||
if (compTab === "workflows" && node.workflow) {
|
||||
if (compTab === "workflows") {
|
||||
alert("Workflows within Scheduled Jobs are not supported yet");
|
||||
return false;
|
||||
}
|
||||
if (compTab === "scripts" || compTab === "ansible") {
|
||||
const type = compTab === "scripts" ? "script" : "ansible";
|
||||
const rawPath = node.path || node.id || "";
|
||||
const { doc, normalizedPath } = await fetchAssemblyDoc(type, rawPath);
|
||||
const assembly = doc?.assembly || {};
|
||||
const docVars = assembly?.variables || doc?.variables || [];
|
||||
const mergedVars = mergeComponentVariables(docVars, [], {});
|
||||
const record = node.assembly;
|
||||
if (!record || !record.assemblyGuid) return false;
|
||||
try {
|
||||
const exportDoc = await loadAssemblyExport(record.assemblyGuid);
|
||||
const parsed = parseAssemblyExport(exportDoc);
|
||||
const docVars = Array.isArray(parsed.rawVariables) ? parsed.rawVariables : [];
|
||||
const mergedVariables = mergeComponentVariables(docVars, [], {});
|
||||
const type = compTab === "ansible" ? "ansible" : "script";
|
||||
const normalizedPath = normalizeAssemblyPath(type, record.path || "", record.displayName);
|
||||
setComponents((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type,
|
||||
path: normalizedPath || rawPath,
|
||||
name: assembly?.name || node.fileName || node.label,
|
||||
description: assembly?.description || normalizedPath || rawPath,
|
||||
variables: mergedVars,
|
||||
localId: generateLocalId()
|
||||
path: normalizedPath,
|
||||
name: record.displayName,
|
||||
description: record.summary || normalizedPath,
|
||||
variables: mergedVariables,
|
||||
localId: generateLocalId(),
|
||||
assemblyGuid: record.assemblyGuid,
|
||||
domain: record.domain,
|
||||
domainLabel: record.domainLabel
|
||||
}
|
||||
]);
|
||||
setSelectedNodeId("");
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Failed to load assembly export:", err);
|
||||
alert(err?.message || "Failed to load assembly details.");
|
||||
return false;
|
||||
}
|
||||
setSelectedNodeId("");
|
||||
return false;
|
||||
}, [compTab, scriptMap, ansibleMap, workflowMap, selectedNodeId, fetchAssemblyDoc, generateLocalId]);
|
||||
}, [compTab, selectedNodeId, ansibleTreeData, workflowTreeData, scriptTreeData, loadAssemblyExport, mergeComponentVariables, generateLocalId, normalizeAssemblyPath]);
|
||||
|
||||
const openAddTargets = async () => {
|
||||
setAddTargetOpen(true);
|
||||
@@ -2006,22 +2007,32 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
sx={{ textTransform: "none", color: "#58a6ff", borderColor: "#58a6ff" }}>
|
||||
Ansible
|
||||
</Button>
|
||||
<Button size="small" variant={compTab === "workflows" ? "outlined" : "text"} onClick={() => setCompTab("workflows")}
|
||||
sx={{ textTransform: "none", color: "#58a6ff", borderColor: "#58a6ff" }}>
|
||||
Workflows
|
||||
</Button>
|
||||
</Box>
|
||||
<Button size="small" variant={compTab === "workflows" ? "outlined" : "text"} onClick={() => setCompTab("workflows")}
|
||||
sx={{ textTransform: "none", color: "#58a6ff", borderColor: "#58a6ff" }}>
|
||||
Workflows
|
||||
</Button>
|
||||
</Box>
|
||||
{assembliesError ? (
|
||||
<Typography variant="body2" sx={{ color: "#ff8080", mb: 1 }}>{assembliesError}</Typography>
|
||||
) : null}
|
||||
{compTab === "scripts" && (
|
||||
<Paper sx={{ p: 1, bgcolor: "#1e1e1e", maxHeight: 400, overflow: "auto" }}>
|
||||
<SimpleTreeView onItemSelectionToggle={(_, id) => {
|
||||
const n = scriptMap[id];
|
||||
const n = scriptTreeData.map[id];
|
||||
if (n && !n.isFolder) setSelectedNodeId(id);
|
||||
}}>
|
||||
{scriptTree.length ? (scriptTree.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, scriptMap) : null}
|
||||
</TreeItem>
|
||||
))) : (
|
||||
{assembliesLoading ? (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, py: 0.5, color: "#7db7ff" }}>
|
||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||
<Typography variant="body2">Loading assemblies…</Typography>
|
||||
</Box>
|
||||
) : Array.isArray(scriptTreeData.root) && scriptTreeData.root.length ? (
|
||||
scriptTreeData.root.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, scriptTreeData.map) : null}
|
||||
</TreeItem>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#888", p: 1 }}>No scripts found.</Typography>
|
||||
)}
|
||||
</SimpleTreeView>
|
||||
@@ -2030,14 +2041,21 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
{compTab === "workflows" && (
|
||||
<Paper sx={{ p: 1, bgcolor: "#1e1e1e", maxHeight: 400, overflow: "auto" }}>
|
||||
<SimpleTreeView onItemSelectionToggle={(_, id) => {
|
||||
const n = workflowMap[id];
|
||||
const n = workflowTreeData.map[id];
|
||||
if (n && !n.isFolder) setSelectedNodeId(id);
|
||||
}}>
|
||||
{workflowTree.length ? (workflowTree.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, workflowMap) : null}
|
||||
</TreeItem>
|
||||
))) : (
|
||||
{assembliesLoading ? (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, py: 0.5, color: "#7db7ff" }}>
|
||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||
<Typography variant="body2">Loading assemblies…</Typography>
|
||||
</Box>
|
||||
) : Array.isArray(workflowTreeData.root) && workflowTreeData.root.length ? (
|
||||
workflowTreeData.root.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, workflowTreeData.map) : null}
|
||||
</TreeItem>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#888", p: 1 }}>No workflows found.</Typography>
|
||||
)}
|
||||
</SimpleTreeView>
|
||||
@@ -2046,14 +2064,21 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
{compTab === "ansible" && (
|
||||
<Paper sx={{ p: 1, bgcolor: "#1e1e1e", maxHeight: 400, overflow: "auto" }}>
|
||||
<SimpleTreeView onItemSelectionToggle={(_, id) => {
|
||||
const n = ansibleMap[id];
|
||||
const n = ansibleTreeData.map[id];
|
||||
if (n && !n.isFolder) setSelectedNodeId(id);
|
||||
}}>
|
||||
{ansibleTree.length ? (ansibleTree.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, ansibleMap) : null}
|
||||
</TreeItem>
|
||||
))) : (
|
||||
{assembliesLoading ? (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, py: 0.5, color: "#7db7ff" }}>
|
||||
<CircularProgress size={18} sx={{ color: "#58a6ff" }} />
|
||||
<Typography variant="body2">Loading assemblies…</Typography>
|
||||
</Box>
|
||||
) : Array.isArray(ansibleTreeData.root) && ansibleTreeData.root.length ? (
|
||||
ansibleTreeData.root.map((n) => (
|
||||
<TreeItem key={n.id} itemId={n.id} label={n.label}>
|
||||
{n.children && n.children.length ? renderTreeNodes(n.children, ansibleTreeData.map) : null}
|
||||
</TreeItem>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#888", p: 1 }}>No playbooks found.</Typography>
|
||||
)}
|
||||
</SimpleTreeView>
|
||||
@@ -2139,3 +2164,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user