737 lines
23 KiB
TypeScript
737 lines
23 KiB
TypeScript
import React, { useEffect, useMemo, useState } from "react";
|
|
|
|
import { ApiClient } from "./api-client.ts";
|
|
import {
|
|
addNodeToDraft,
|
|
createDefaultWorkflowDraft,
|
|
removeNodeFromDraft,
|
|
resolveDefinitionIdForNode,
|
|
serializeWorkflowDraft,
|
|
workflowDraftFromVersion,
|
|
type WorkflowDraft,
|
|
} from "./workflow-editor-state.ts";
|
|
|
|
type NavItem = "Assets" | "Workflows" | "Runs" | "Explore" | "Labels" | "Admin";
|
|
|
|
type BootstrapContext = {
|
|
userId: string;
|
|
workspace: { _id: string; name: string };
|
|
project: { _id: string; name: string };
|
|
};
|
|
|
|
type AppProps = {
|
|
apiBaseUrl: string;
|
|
};
|
|
|
|
function usePathname() {
|
|
const [pathname, setPathname] = useState(
|
|
typeof window === "undefined" ? "/assets" : window.location.pathname || "/assets",
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handle = () => setPathname(window.location.pathname || "/assets");
|
|
window.addEventListener("popstate", handle);
|
|
return () => window.removeEventListener("popstate", handle);
|
|
}, []);
|
|
|
|
return pathname === "/" ? "/assets" : pathname;
|
|
}
|
|
|
|
function AppShell(props: {
|
|
workspaceName: string;
|
|
projectName: string;
|
|
active: NavItem;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const navItems: Array<{ label: NavItem; href: string }> = [
|
|
{ label: "Assets", href: "/assets" },
|
|
{ label: "Workflows", href: "/workflows" },
|
|
{ label: "Runs", href: "/runs" },
|
|
{ label: "Explore", href: "/explore" },
|
|
{ label: "Labels", href: "/labels" },
|
|
{ label: "Admin", href: "/admin" },
|
|
];
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<header className="app-header">
|
|
<div className="app-header__group">
|
|
<div className="app-header__pill">
|
|
<span className="app-header__label">Workspace</span>
|
|
<strong>{props.workspaceName}</strong>
|
|
</div>
|
|
<div className="app-header__pill">
|
|
<span className="app-header__label">Project</span>
|
|
<strong>{props.projectName}</strong>
|
|
</div>
|
|
</div>
|
|
<div className="app-header__group">
|
|
<span className="app-header__label">Runs</span>
|
|
<span className="app-header__label">Local Dev</span>
|
|
</div>
|
|
</header>
|
|
<aside className="app-sidebar">
|
|
<ul>
|
|
{navItems.map((item) => (
|
|
<li key={item.label}>
|
|
<a href={item.href} data-active={String(item.label === props.active)}>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</aside>
|
|
<main className="app-main">{props.children}</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AssetsPage(props: {
|
|
api: ApiClient;
|
|
bootstrap: BootstrapContext;
|
|
}) {
|
|
const [sourcePath, setSourcePath] = useState("");
|
|
const [assets, setAssets] = useState<any[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadAssets = async () => {
|
|
try {
|
|
setAssets(await props.api.listAssets(props.bootstrap.project._id));
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load assets");
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
void loadAssets();
|
|
}, [props.bootstrap.project._id]);
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="panel">
|
|
<h1>Assets</h1>
|
|
<p>Register local folders, archives, or dataset files, then probe them into managed asset metadata.</p>
|
|
<div className="field-grid">
|
|
<label>
|
|
Local Path
|
|
<input
|
|
value={sourcePath}
|
|
onChange={(event) => setSourcePath(event.target.value)}
|
|
placeholder="/Users/longtaowu/workspace/emboldata/data"
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="button-row" style={{ marginTop: 12 }}>
|
|
<button
|
|
className="button-primary"
|
|
onClick={async () => {
|
|
setError(null);
|
|
try {
|
|
const asset = await props.api.registerLocalAsset({
|
|
workspaceId: props.bootstrap.workspace._id,
|
|
projectId: props.bootstrap.project._id,
|
|
sourcePath,
|
|
});
|
|
await props.api.probeAsset(asset._id);
|
|
await loadAssets();
|
|
} catch (registerError) {
|
|
setError(
|
|
registerError instanceof Error
|
|
? registerError.message
|
|
: "Failed to register local asset",
|
|
);
|
|
}
|
|
}}
|
|
>
|
|
Register Local Path
|
|
</button>
|
|
</div>
|
|
{error ? <p>{error}</p> : null}
|
|
</section>
|
|
|
|
<section className="panel">
|
|
<div className="list-grid">
|
|
{assets.length === 0 ? (
|
|
<p className="empty-state">No assets have been registered yet.</p>
|
|
) : (
|
|
assets.map((asset) => (
|
|
<article key={asset._id} className="asset-card">
|
|
<div className="toolbar">
|
|
<a href={`/assets/${asset._id}`}>
|
|
<strong>{asset.displayName}</strong>
|
|
</a>
|
|
<span className="status-pill" data-status={asset.status}>
|
|
{asset.status}
|
|
</span>
|
|
</div>
|
|
<p>Type: {asset.type}</p>
|
|
<p>Source: {asset.sourceType}</p>
|
|
<p>Detected: {(asset.detectedFormats ?? []).join(", ") || "pending"}</p>
|
|
<p>Top-level entries: {(asset.topLevelPaths ?? []).slice(0, 6).join(", ") || "n/a"}</p>
|
|
</article>
|
|
))
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AssetDetailPage(props: {
|
|
api: ApiClient;
|
|
assetId: string;
|
|
}) {
|
|
const [asset, setAsset] = useState<any | null>(null);
|
|
const [probeReport, setProbeReport] = useState<any | null>(null);
|
|
const [artifactId, setArtifactId] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
const [assetData, reportData] = await Promise.all([
|
|
props.api.getAsset(props.assetId),
|
|
props.api.getProbeReport(props.assetId).catch(() => null),
|
|
]);
|
|
setAsset(assetData);
|
|
setProbeReport(reportData);
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load asset detail");
|
|
}
|
|
})();
|
|
}, [props.assetId]);
|
|
|
|
if (error) {
|
|
return <section className="panel">{error}</section>;
|
|
}
|
|
if (!asset) {
|
|
return <section className="panel">Loading asset detail...</section>;
|
|
}
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="two-column">
|
|
<div className="panel">
|
|
<h1>{asset.displayName}</h1>
|
|
<p>Asset ID: {asset._id}</p>
|
|
<p>Type: {asset.type}</p>
|
|
<p>Status: {asset.status}</p>
|
|
<p>Source path: {asset.sourcePath ?? "n/a"}</p>
|
|
<div className="button-row" style={{ marginTop: 12 }}>
|
|
<button
|
|
className="button-primary"
|
|
onClick={async () => {
|
|
const report = await props.api.probeAsset(asset._id);
|
|
setProbeReport(report);
|
|
const reloaded = await props.api.getAsset(asset._id);
|
|
setAsset(reloaded);
|
|
}}
|
|
>
|
|
Probe Again
|
|
</button>
|
|
<button
|
|
className="button-secondary"
|
|
onClick={async () => {
|
|
if (!probeReport) {
|
|
return;
|
|
}
|
|
const artifact = await props.api.createArtifact({
|
|
type: "json",
|
|
title: `Probe Report: ${asset.displayName}`,
|
|
producerType: "asset",
|
|
producerId: asset._id,
|
|
payload: probeReport.rawReport ?? {},
|
|
});
|
|
setArtifactId(artifact._id);
|
|
}}
|
|
>
|
|
Create Explore Artifact
|
|
</button>
|
|
{artifactId ? <a href={`/explore/${artifactId}`}>Open Explore View</a> : null}
|
|
</div>
|
|
</div>
|
|
|
|
<aside className="panel">
|
|
<h2>Probe Summary</h2>
|
|
{probeReport ? (
|
|
<>
|
|
<p>Detected: {(probeReport.detectedFormatCandidates ?? []).join(", ")}</p>
|
|
<p>Warnings: {(probeReport.warnings ?? []).join(", ") || "none"}</p>
|
|
<p>
|
|
Recommended nodes: {(probeReport.recommendedNextNodes ?? []).join(", ") || "inspect_asset"}
|
|
</p>
|
|
<pre className="mono-block">
|
|
{JSON.stringify(probeReport.structureSummary ?? {}, null, 2)}
|
|
</pre>
|
|
</>
|
|
) : (
|
|
<p className="empty-state">No probe report yet.</p>
|
|
)}
|
|
</aside>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkflowsPage(props: {
|
|
api: ApiClient;
|
|
bootstrap: BootstrapContext;
|
|
}) {
|
|
const [workflows, setWorkflows] = useState<any[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = async () => {
|
|
try {
|
|
setWorkflows(await props.api.listWorkflows(props.bootstrap.project._id));
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load workflows");
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
void load();
|
|
}, [props.bootstrap.project._id]);
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="panel">
|
|
<div className="toolbar">
|
|
<h1 style={{ margin: 0 }}>Workflows</h1>
|
|
<button
|
|
className="button-primary"
|
|
onClick={async () => {
|
|
await props.api.createWorkflow({
|
|
workspaceId: props.bootstrap.workspace._id,
|
|
projectId: props.bootstrap.project._id,
|
|
name: `Delivery Normalize ${workflows.length + 1}`,
|
|
});
|
|
await load();
|
|
}}
|
|
>
|
|
Create Workflow
|
|
</button>
|
|
</div>
|
|
{error ? <p>{error}</p> : null}
|
|
</section>
|
|
|
|
<section className="panel">
|
|
<div className="list-grid">
|
|
{workflows.length === 0 ? (
|
|
<p className="empty-state">No workflows yet.</p>
|
|
) : (
|
|
workflows.map((workflow) => (
|
|
<article key={workflow._id} className="asset-card">
|
|
<a href={`/workflows/${workflow._id}`}>
|
|
<strong>{workflow.name}</strong>
|
|
</a>
|
|
<p>Status: {workflow.status}</p>
|
|
<p>Latest version: {workflow.latestVersionNumber}</p>
|
|
</article>
|
|
))
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkflowEditorPage(props: {
|
|
api: ApiClient;
|
|
workflowId: string;
|
|
}) {
|
|
const [workflow, setWorkflow] = useState<any | null>(null);
|
|
const [nodes, setNodes] = useState<any[]>([]);
|
|
const [assets, setAssets] = useState<any[]>([]);
|
|
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
|
|
const [versions, setVersions] = useState<any[]>([]);
|
|
const [draft, setDraft] = useState<WorkflowDraft>(() => createDefaultWorkflowDraft());
|
|
const [selectedNodeId, setSelectedNodeId] = useState("rename-folder");
|
|
const [lastRunId, setLastRunId] = useState<string | null>(null);
|
|
const [dirty, setDirty] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
const workflowDefinition = await props.api.getWorkflowDefinition(props.workflowId);
|
|
const [nodeDefs, workflowVersions, workflowAssets] = await Promise.all([
|
|
props.api.listNodeDefinitions(),
|
|
props.api.listWorkflowVersions(props.workflowId),
|
|
props.api.listAssets(workflowDefinition.projectId),
|
|
]);
|
|
setWorkflow(workflowDefinition);
|
|
setNodes(nodeDefs);
|
|
setAssets(workflowAssets);
|
|
setSelectedAssetId((previous) => {
|
|
if (previous && workflowAssets.some((asset) => asset._id === previous)) {
|
|
return previous;
|
|
}
|
|
return workflowAssets[0]?._id ?? null;
|
|
});
|
|
setVersions(workflowVersions);
|
|
const nextDraft = workflowDraftFromVersion(workflowVersions[0] ?? null);
|
|
setDraft(nextDraft);
|
|
setSelectedNodeId(nextDraft.logicGraph.nodes[0]?.id ?? "rename-folder");
|
|
setDirty(false);
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load workflow");
|
|
}
|
|
})();
|
|
}, [props.workflowId]);
|
|
|
|
const selectedNode = useMemo(
|
|
() =>
|
|
nodes.find((node) => node.id === resolveDefinitionIdForNode(draft, selectedNodeId)) ?? null,
|
|
[draft, nodes, selectedNodeId],
|
|
);
|
|
|
|
async function saveCurrentDraft() {
|
|
const version = await props.api.saveWorkflowVersion(
|
|
props.workflowId,
|
|
serializeWorkflowDraft(draft),
|
|
);
|
|
setVersions((previous) => [version, ...previous.filter((item) => item._id !== version._id)]);
|
|
setDirty(false);
|
|
return version;
|
|
}
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="panel">
|
|
<div className="toolbar">
|
|
<h1 style={{ margin: 0 }}>{workflow?.name ?? "Workflow Editor"}</h1>
|
|
<label style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span>Run Asset</span>
|
|
<select
|
|
value={selectedAssetId ?? ""}
|
|
onChange={(event) => setSelectedAssetId(event.target.value || null)}
|
|
>
|
|
{assets.length === 0 ? <option value="">No assets available</option> : null}
|
|
{assets.map((asset) => (
|
|
<option key={asset._id} value={asset._id}>
|
|
{asset.displayName}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button
|
|
className="button-primary"
|
|
onClick={async () => {
|
|
await saveCurrentDraft();
|
|
}}
|
|
>
|
|
Save Workflow Version
|
|
</button>
|
|
<button
|
|
className="button-secondary"
|
|
onClick={async () => {
|
|
if (!selectedAssetId) {
|
|
setError("Select an asset before triggering a workflow run.");
|
|
return;
|
|
}
|
|
const latestVersion = dirty || versions.length === 0
|
|
? await saveCurrentDraft()
|
|
: versions[0];
|
|
const run = await props.api.createRun({
|
|
workflowDefinitionId: props.workflowId,
|
|
workflowVersionId: latestVersion._id,
|
|
assetIds: [selectedAssetId],
|
|
});
|
|
setLastRunId(run._id);
|
|
}}
|
|
disabled={!selectedAssetId}
|
|
>
|
|
Trigger Workflow Run
|
|
</button>
|
|
<button
|
|
className="button-secondary"
|
|
onClick={() => {
|
|
const latestDraft = workflowDraftFromVersion(versions[0] ?? null);
|
|
setDraft(latestDraft);
|
|
setSelectedNodeId(latestDraft.logicGraph.nodes[0]?.id ?? "rename-folder");
|
|
setDirty(false);
|
|
}}
|
|
>
|
|
Reload Latest Saved
|
|
</button>
|
|
{lastRunId ? <a href={`/runs/${lastRunId}`}>Open Latest Run</a> : null}
|
|
</div>
|
|
{error ? <p>{error}</p> : null}
|
|
</section>
|
|
|
|
<section className="editor-layout">
|
|
<aside className="panel">
|
|
<h2>Node Library</h2>
|
|
<div className="list-grid">
|
|
{nodes.map((node) => (
|
|
<button
|
|
key={node.id}
|
|
className="button-secondary"
|
|
onClick={() => {
|
|
const result = addNodeToDraft(draft, node);
|
|
setDraft(result.draft);
|
|
setSelectedNodeId(result.nodeId);
|
|
setDirty(true);
|
|
}}
|
|
>
|
|
{node.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
|
|
<section className="panel">
|
|
<h2>Canvas</h2>
|
|
<div className="list-grid">
|
|
{draft.logicGraph.nodes.map((node) => (
|
|
<div
|
|
key={node.id}
|
|
className="node-card"
|
|
data-selected={String(selectedNodeId === node.id)}
|
|
onClick={() => setSelectedNodeId(node.id)}
|
|
style={{ cursor: "pointer" }}
|
|
>
|
|
<strong>{node.id}</strong>
|
|
<p>Type: {node.type}</p>
|
|
<p>Definition: {resolveDefinitionIdForNode(draft, node.id)}</p>
|
|
<button
|
|
className="button-secondary"
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const next = removeNodeFromDraft(draft, node.id);
|
|
setDraft(next);
|
|
setSelectedNodeId(next.logicGraph.nodes[0]?.id ?? "");
|
|
setDirty(true);
|
|
}}
|
|
>
|
|
Remove Node
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p style={{ marginTop: 12 }}>
|
|
Latest saved versions: {versions.length > 0 ? versions.map((item) => item.versionNumber).join(", ") : "none"}
|
|
</p>
|
|
<p>Draft status: {dirty ? "unsaved changes" : "synced"}</p>
|
|
</section>
|
|
|
|
<aside className="panel">
|
|
<h2>Node Configuration</h2>
|
|
{selectedNode ? (
|
|
<>
|
|
<p><strong>{selectedNode.name}</strong></p>
|
|
<p>{selectedNode.description}</p>
|
|
<p>Category: {selectedNode.category}</p>
|
|
</>
|
|
) : (
|
|
<p className="empty-state">Select a node.</p>
|
|
)}
|
|
</aside>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RunsIndexPage() {
|
|
return (
|
|
<section className="panel">
|
|
<h1>Runs</h1>
|
|
<p className="empty-state">Open a specific run from the workflow editor.</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function RunDetailPage(props: {
|
|
api: ApiClient;
|
|
runId: string;
|
|
}) {
|
|
const [run, setRun] = useState<any | null>(null);
|
|
const [tasks, setTasks] = useState<any[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
const [runData, taskData] = await Promise.all([
|
|
props.api.getRun(props.runId),
|
|
props.api.listRunTasks(props.runId),
|
|
]);
|
|
setRun(runData);
|
|
setTasks(taskData);
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load run detail");
|
|
}
|
|
})();
|
|
}, [props.runId]);
|
|
|
|
if (error) {
|
|
return <section className="panel">{error}</section>;
|
|
}
|
|
if (!run) {
|
|
return <section className="panel">Loading run...</section>;
|
|
}
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="panel">
|
|
<h1>Run Detail</h1>
|
|
<p>Run ID: {run._id}</p>
|
|
<p>Status: {run.status}</p>
|
|
<p>
|
|
Input assets:{" "}
|
|
{(run.assetIds ?? []).length > 0
|
|
? run.assetIds.map((assetId: string) => assetId).join(", ")
|
|
: "none"}
|
|
</p>
|
|
</section>
|
|
<section className="two-column">
|
|
<div className="panel">
|
|
<h2>Run Graph</h2>
|
|
<div className="list-grid">
|
|
{tasks.map((task) => (
|
|
<article key={task._id} className="task-card">
|
|
<div className="toolbar">
|
|
<strong>{task.nodeId}</strong>
|
|
<span className="status-pill" data-status={task.status}>
|
|
{task.status}
|
|
</span>
|
|
</div>
|
|
<p>Node type: {task.nodeType}</p>
|
|
<p>Bound assets: {(task.assetIds ?? []).join(", ") || "none"}</p>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<aside className="panel">
|
|
<h2>Selected Task</h2>
|
|
{tasks[0] ? (
|
|
<>
|
|
<p>Node: {tasks[0].nodeId}</p>
|
|
<p>Status: {tasks[0].status}</p>
|
|
<pre className="mono-block">{JSON.stringify(tasks[0], null, 2)}</pre>
|
|
</>
|
|
) : (
|
|
<p className="empty-state">No tasks created.</p>
|
|
)}
|
|
</aside>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ExplorePage(props: {
|
|
api: ApiClient;
|
|
artifactId: string | null;
|
|
}) {
|
|
const [artifact, setArtifact] = useState<any | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!props.artifactId) {
|
|
return;
|
|
}
|
|
void (async () => {
|
|
try {
|
|
setArtifact(await props.api.getArtifact(props.artifactId!));
|
|
} catch (loadError) {
|
|
setError(loadError instanceof Error ? loadError.message : "Failed to load artifact");
|
|
}
|
|
})();
|
|
}, [props.artifactId]);
|
|
|
|
if (!props.artifactId) {
|
|
return (
|
|
<section className="panel">
|
|
<h1>Explore</h1>
|
|
<p className="empty-state">Create an artifact from asset detail to inspect it here.</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return <section className="panel">{error}</section>;
|
|
}
|
|
if (!artifact) {
|
|
return <section className="panel">Loading artifact...</section>;
|
|
}
|
|
|
|
return (
|
|
<div className="page-stack">
|
|
<section className="panel">
|
|
<h1>{artifact.title}</h1>
|
|
<p>Artifact ID: {artifact._id}</p>
|
|
<p>Type: {artifact.type}</p>
|
|
<pre className="mono-block">{JSON.stringify(artifact.payload, null, 2)}</pre>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function App(props: AppProps) {
|
|
const api = useMemo(() => new ApiClient(props.apiBaseUrl), [props.apiBaseUrl]);
|
|
const pathname = usePathname();
|
|
const [bootstrap, setBootstrap] = useState<BootstrapContext | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
try {
|
|
setBootstrap(await api.bootstrapDev());
|
|
} catch (bootstrapError) {
|
|
setError(
|
|
bootstrapError instanceof Error ? bootstrapError.message : "Failed to bootstrap local context",
|
|
);
|
|
}
|
|
})();
|
|
}, [api]);
|
|
|
|
if (error) {
|
|
return <section className="panel">{error}</section>;
|
|
}
|
|
if (!bootstrap) {
|
|
return <section className="panel">Bootstrapping local workspace...</section>;
|
|
}
|
|
|
|
const assetMatch = pathname.match(/^\/assets\/([^/]+)$/);
|
|
const workflowMatch = pathname.match(/^\/workflows\/([^/]+)$/);
|
|
const runMatch = pathname.match(/^\/runs\/([^/]+)$/);
|
|
const exploreMatch = pathname.match(/^\/explore\/([^/]+)$/);
|
|
|
|
let active: NavItem = "Assets";
|
|
let content: React.ReactNode = <AssetsPage api={api} bootstrap={bootstrap} />;
|
|
|
|
if (pathname === "/workflows") {
|
|
active = "Workflows";
|
|
content = <WorkflowsPage api={api} bootstrap={bootstrap} />;
|
|
} else if (workflowMatch) {
|
|
active = "Workflows";
|
|
content = <WorkflowEditorPage api={api} workflowId={workflowMatch[1]} />;
|
|
} else if (pathname === "/runs") {
|
|
active = "Runs";
|
|
content = <RunsIndexPage />;
|
|
} else if (runMatch) {
|
|
active = "Runs";
|
|
content = <RunDetailPage api={api} runId={runMatch[1]} />;
|
|
} else if (exploreMatch) {
|
|
active = "Explore";
|
|
content = <ExplorePage api={api} artifactId={exploreMatch[1]} />;
|
|
} else if (pathname === "/explore") {
|
|
active = "Explore";
|
|
content = <ExplorePage api={api} artifactId={null} />;
|
|
} else if (assetMatch) {
|
|
active = "Assets";
|
|
content = <AssetDetailPage api={api} assetId={assetMatch[1]} />;
|
|
}
|
|
|
|
return (
|
|
<AppShell
|
|
workspaceName={bootstrap.workspace.name}
|
|
projectName={bootstrap.project.name}
|
|
active={active}
|
|
>
|
|
{content}
|
|
</AppShell>
|
|
);
|
|
}
|