Unified Assembly-Based API Endpoints

This commit is contained in:
2025-09-29 02:08:26 -06:00
parent 2f062b21a0
commit ff87233b53
7 changed files with 478 additions and 378 deletions

View File

@@ -527,10 +527,16 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
}
};
try {
await fetch("/api/storage/save_workflow", {
const body = {
island: 'workflows',
kind: 'file',
path: payload.path,
content: payload.workflow
};
await fetch("/api/assembly/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
body: JSON.stringify(body)
});
setTabs((prev) =>
prev.map((t) => (t.id === activeTabId ? { ...t, tab_name: name } : t))
@@ -611,7 +617,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
if (workflow && workflow.rel_path) {
const folder = workflow.rel_path.split("/").slice(0, -1).join("/");
try {
const resp = await fetch(`/api/storage/load_workflow?path=${encodeURIComponent(workflow.rel_path)}`);
const resp = await fetch(`/api/assembly/load?island=workflows&path=${encodeURIComponent(workflow.rel_path)}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
setTabs([{ id: newId, tab_name: data.tab_name || workflow.name || workflow.file_name || "Workflow", nodes: data.nodes || [], edges: data.edges || [], folderPath: folder }]);
@@ -640,7 +646,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
if (workflow && workflow.rel_path) {
const folder = workflow.rel_path.split("/").slice(0, -1).join("/");
try {
const resp = await fetch(`/api/storage/load_workflow?path=${encodeURIComponent(workflow.rel_path)}`);
const resp = await fetch(`/api/assembly/load?island=workflows&path=${encodeURIComponent(workflow.rel_path)}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
setTabs([{ id: newId, tab_name: data.tab_name || workflow.name || workflow.file_name || "Workflow", nodes: data.nodes || [], edges: data.edges || [], folderPath: folder }]);

View File

@@ -133,10 +133,10 @@ function WorkflowsIsland({ onOpenWorkflow }) {
}
const newPath = target.path ? `${target.path}/${dragNode.fileName}` : dragNode.fileName;
try {
await fetch("/api/storage/move_workflow", {
await fetch("/api/assembly/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: dragNode.path, new_path: newPath })
body: JSON.stringify({ island: 'workflows', kind: 'file', path: dragNode.path, new_path: newPath })
});
loadTree();
} catch (err) {
@@ -147,10 +147,10 @@ function WorkflowsIsland({ onOpenWorkflow }) {
const loadTree = useCallback(async () => {
try {
const resp = await fetch("/api/storage/load_workflows");
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.workflows || [], data.folders || []);
const { root, map } = buildWorkflowTree(data.items || [], data.folders || []);
setTree(root);
setNodeMap(map);
} catch (err) {
@@ -211,10 +211,10 @@ function WorkflowsIsland({ onOpenWorkflow }) {
const saveRenameWorkflow = async () => {
if (!selectedNode) return;
try {
await fetch("/api/storage/rename_workflow", {
await fetch("/api/assembly/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
body: JSON.stringify({ island: 'workflows', kind: 'file', path: selectedNode.path, new_name: renameValue })
});
loadTree();
} catch (err) {
@@ -226,18 +226,18 @@ function WorkflowsIsland({ onOpenWorkflow }) {
const saveRenameFolder = async () => {
try {
if (folderDialogMode === "rename" && selectedNode) {
await fetch("/api/storage/rename_folder", {
await fetch("/api/assembly/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
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/storage/create_folder", {
await fetch("/api/assembly/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: newPath })
body: JSON.stringify({ island: 'workflows', kind: 'folder', path: newPath })
});
}
loadTree();
@@ -258,16 +258,16 @@ function WorkflowsIsland({ onOpenWorkflow }) {
if (!selectedNode) return;
try {
if (selectedNode.isFolder) {
await fetch("/api/storage/delete_folder", {
await fetch("/api/assembly/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path })
body: JSON.stringify({ island: 'workflows', kind: 'folder', path: selectedNode.path })
});
} else {
await fetch("/api/storage/delete_workflow", {
await fetch("/api/assembly/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path })
body: JSON.stringify({ island: 'workflows', kind: 'file', path: selectedNode.path })
});
}
loadTree();
@@ -464,12 +464,17 @@ function ScriptsLikeIsland({
const apiRef = useTreeViewApiRef();
const [dragNode, setDragNode] = useState(null);
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(`${baseApi}/list`);
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.scripts || data.items || [], data.folders || []);
const { root, map } = buildFileTree(rootLabel, data.items || [], data.folders || []);
setTree(root);
setNodeMap(map);
} catch (err) {
@@ -477,7 +482,7 @@ function ScriptsLikeIsland({
setTree([]);
setNodeMap({});
}
}, [baseApi, title, rootLabel]);
}, [island, title, rootLabel]);
useEffect(() => { loadTree(); }, [loadTree]);
@@ -497,10 +502,10 @@ function ScriptsLikeIsland({
}
const newPath = target.path ? `${target.path}/${dragNode.fileName}` : dragNode.fileName;
try {
await fetch(`${baseApi}/move_file`, {
await fetch(`/api/assembly/move`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: dragNode.path, new_path: newPath })
body: JSON.stringify({ island, kind: 'file', path: dragNode.path, new_path: newPath })
});
loadTree();
} catch (err) {
@@ -519,10 +524,13 @@ function ScriptsLikeIsland({
const saveRenameFile = async () => {
try {
const res = await fetch(`${baseApi}/rename_file`, {
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`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
@@ -537,18 +545,18 @@ function ScriptsLikeIsland({
const saveRenameFolder = async () => {
try {
if (folderDialogMode === "rename" && selectedNode) {
await fetch(`${baseApi}/rename_folder`, {
await fetch(`/api/assembly/rename`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
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(`${baseApi}/create_folder`, {
await fetch(`/api/assembly/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: newPath })
body: JSON.stringify({ island, kind: 'folder', path: newPath })
});
}
setRenameFolderOpen(false);
@@ -563,16 +571,16 @@ function ScriptsLikeIsland({
if (!selectedNode) return;
try {
if (selectedNode.isFolder) {
await fetch(`${baseApi}/delete_folder`, {
await fetch(`/api/assembly/delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path })
body: JSON.stringify({ island, kind: 'folder', path: selectedNode.path })
});
} else {
await fetch(`${baseApi}/delete_file`, {
await fetch(`/api/assembly/delete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: selectedNode.path })
body: JSON.stringify({ island, kind: 'file', path: selectedNode.path })
});
}
setDeleteOpen(false);
@@ -593,11 +601,11 @@ function ScriptsLikeIsland({
else name += '.ps1';
}
const newPath = folder ? `${folder}/${name}` : name;
// create empty file by saving blank content
const res = await fetch(`${baseApi}/save`, {
// create empty file via unified API
const res = await fetch(`/api/assembly/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newPath, content: "" })
body: JSON.stringify({ island, kind: 'file', path: newPath, content: "", type: island === 'ansible' ? 'ansible' : 'powershell' })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));

View File

@@ -87,7 +87,6 @@ function NewItemDialog({ open, name, type, typeOptions, onChangeName, onChangeTy
export default function ScriptEditor({ mode = "scripts", initialPath = "", onConsumedInitialPath, onSaved }) {
const isAnsible = mode === "ansible";
const baseApi = isAnsible ? "/api/ansible" : "/api/scripts";
const TYPE_OPTIONS = useMemo(() => (isAnsible ? TYPE_OPTIONS_ALL.filter(o => o.key === 'ansible') : TYPE_OPTIONS_ALL.filter(o => o.key !== 'ansible')), [isAnsible]);
const [currentPath, setCurrentPath] = useState("");
@@ -128,18 +127,36 @@ export default function ScriptEditor({ mode = "scripts", initialPath = "", onCon
setNewOpen(true);
return;
}
const normalizedName = currentPath ? undefined : ensureExt(fileName, type);
const payload = { path: currentPath || undefined, name: normalizedName, content: code, type };
const island = isAnsible ? 'ansible' : 'scripts';
const normalizedName = currentPath ? currentPath : ensureExt(fileName, type);
try {
const resp = await fetch(`${baseApi}/save`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
const data = await resp.json();
if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
if (data.rel_path) {
setCurrentPath(data.rel_path);
const fname = data.rel_path.split('/').pop();
setFileName(fname);
setType(typeFromFilename(fname));
// If we already have a path, edit; otherwise create
if (currentPath) {
const resp = await fetch(`/api/assembly/edit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ island, path: currentPath, content: code })
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
onSaved && onSaved();
} else {
const resp = await fetch(`/api/assembly/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ island, kind: 'file', path: normalizedName, content: code, type })
});
const data = await resp.json();
if (!resp.ok) throw new Error(data?.error || `HTTP ${resp.status}`);
if (data.rel_path) {
setCurrentPath(data.rel_path);
const fname = data.rel_path.split('/').pop();
setFileName(fname);
setType(typeFromFilename(fname));
onSaved && onSaved();
}
}
} catch (err) {
console.error("Failed to save:", err);
@@ -148,8 +165,9 @@ export default function ScriptEditor({ mode = "scripts", initialPath = "", onCon
const saveRenameFile = async () => {
try {
const island = isAnsible ? 'ansible' : 'scripts';
const finalName = ensureExt(renameValue, type);
const res = await fetch(`${baseApi}/rename_file`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: currentPath, new_name: finalName, type }) });
const res = await fetch(`/api/assembly/rename`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ island, kind: 'file', path: currentPath, new_name: finalName, type }) });
const data = await res.json();
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
setCurrentPath(data.rel_path || currentPath);
@@ -201,4 +219,3 @@ export default function ScriptEditor({ mode = "scripts", initialPath = "", onCon
</Box>
);
}

View File

@@ -366,19 +366,19 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
setAddCompOpen(true);
try {
// scripts
const sResp = await fetch("/api/scripts/list");
const sResp = await fetch("/api/assembly/list?island=scripts");
if (sResp.ok) {
const sData = await sResp.json();
const { root, map } = buildScriptTree(sData.scripts || [], sData.folders || []);
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/storage/load_workflows");
const wResp = await fetch("/api/assembly/list?island=workflows");
if (wResp.ok) {
const wData = await wResp.json();
const { root, map } = buildWorkflowTree(wData.workflows || [], wData.folders || []);
const { root, map } = buildWorkflowTree(wData.items || [], wData.folders || []);
setWorkflowTree(root); setWorkflowMap(map);
} else { setWorkflowTree([]); setWorkflowMap({}); }
} catch { setWorkflowTree([]); setWorkflowMap({}); }
@@ -391,7 +391,8 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
if (compTab === "scripts" && node.script) {
setComponents((prev) => [
...prev,
{ type: "script", path: node.path, name: node.fileName || node.label, description: node.path }
// Store path relative to Assemblies root with 'Scripts/' prefix for scheduler compatibility
{ type: "script", path: (node.path.startsWith('Scripts/') ? node.path : `Scripts/${node.path}`), name: node.fileName || node.label, description: node.path }
]);
setSelectedNodeId("");
return true;

View File

@@ -83,10 +83,10 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
const loadTree = useCallback(async () => {
try {
const resp = await fetch("/api/scripts/list");
const resp = await fetch("/api/assembly/list?island=scripts");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
const { root, map } = buildTree(data.scripts || [], data.folders || []);
const { root, map } = buildTree(data.items || [], data.folders || []);
setTree(root);
setNodeMap(map);
} catch (err) {
@@ -140,10 +140,12 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
setRunning(true);
setError("");
try {
// quick_run expects a path relative to Assemblies root with 'Scripts/' prefix
const script_path = selectedPath.startsWith('Scripts/') ? selectedPath : `Scripts/${selectedPath}`;
const resp = await fetch("/api/scripts/quick_run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ script_path: selectedPath, hostnames, run_mode: runAsCurrentUser ? "current_user" : "system" })
body: JSON.stringify({ script_path, hostnames, run_mode: runAsCurrentUser ? "current_user" : "system" })
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || `HTTP ${resp.status}`);