193 lines
6.3 KiB
TypeScript
193 lines
6.3 KiB
TypeScript
import { useMemo } from "react";
|
|
import { analyze, autoLayout, compile, tidyLayout } from "./api/client";
|
|
import { CanvasArea } from "./components/CanvasArea";
|
|
import { LeftPanel } from "./components/LeftPanel";
|
|
import { RightInspector } from "./components/RightInspector";
|
|
import { TopBar } from "./components/TopBar";
|
|
import type { TopBarStatusTone } from "./components/TopBar";
|
|
import sampleFixture from "./fixtures/sample.schemeta.json";
|
|
import { useSchemetaActions, useSchemetaSelector } from "./hooks";
|
|
import { createSchemetaStore } from "./state";
|
|
import type { SchemetaModel } from "./state";
|
|
|
|
type EndpointActionId = "compile" | "analyze" | "layout-auto" | "layout-tidy";
|
|
type LocalActionId = "load-sample" | "reset-sample";
|
|
type WorkflowActionId = EndpointActionId | LocalActionId;
|
|
|
|
type WorkflowAction = {
|
|
id: WorkflowActionId;
|
|
label: string;
|
|
};
|
|
|
|
const SAMPLE_PAYLOAD = sampleFixture as SchemetaModel;
|
|
|
|
function getPayloadOrThrow(payload: unknown): SchemetaModel {
|
|
if (typeof payload !== "object" || payload === null) {
|
|
throw new Error("No model loaded. Run 'Load Sample JSON' first.");
|
|
}
|
|
return payload as SchemetaModel;
|
|
}
|
|
|
|
function toCompileFromAnalyze(result: Awaited<ReturnType<typeof analyze>>): Record<string, unknown> {
|
|
return {
|
|
api_version: result.api_version,
|
|
schema_version: result.schema_version,
|
|
request_id: result.request_id,
|
|
ok: result.ok ?? true,
|
|
errors: Array.isArray(result.errors) ? result.errors : [],
|
|
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
topology: result.topology ?? {}
|
|
};
|
|
}
|
|
|
|
export function App() {
|
|
const store = useMemo(() => createSchemetaStore({ model: SAMPLE_PAYLOAD }), []);
|
|
const actionsApi = useSchemetaActions(store);
|
|
const model = useSchemetaSelector(store, (state) => state.model);
|
|
const selection = useSchemetaSelector(store, (state) => state.selection);
|
|
const compileResult = useSchemetaSelector(store, (state) => state.compileResult);
|
|
const lifecycle = useSchemetaSelector(store, (state) => state.lifecycle);
|
|
const jsonError = useSchemetaSelector(store, (state) => state.uiFlags.jsonError);
|
|
const lastApiVersion = useSchemetaSelector(store, (state) => {
|
|
const apiVersion = state.compileResult && typeof state.compileResult.api_version === "string" ? state.compileResult.api_version : null;
|
|
return apiVersion ?? "-";
|
|
});
|
|
|
|
const actions = useMemo<WorkflowAction[]>(
|
|
() => [
|
|
{ id: "load-sample", label: "Load Sample JSON" },
|
|
{ id: "compile", label: "Compile" },
|
|
{ id: "analyze", label: "Analyze" },
|
|
{ id: "layout-auto", label: "Auto Layout" },
|
|
{ id: "layout-tidy", label: "Auto Tidy" },
|
|
{ id: "reset-sample", label: "Reset Sample" }
|
|
],
|
|
[]
|
|
);
|
|
|
|
const status = jsonError
|
|
? `JSON parse failed: ${jsonError}`
|
|
: lifecycle.isCompiling
|
|
? `${lifecycle.lastAction ?? "Working"}...`
|
|
: lifecycle.lastError
|
|
? `${lifecycle.lastAction ?? "Action"} failed: ${lifecycle.lastError}`
|
|
: lifecycle.lastAction
|
|
? `${lifecycle.lastAction} complete`
|
|
: "Idle";
|
|
const statusTone: TopBarStatusTone = jsonError
|
|
? "error"
|
|
: lifecycle.isCompiling
|
|
? "busy"
|
|
: lifecycle.lastError
|
|
? "error"
|
|
: lifecycle.lastAction
|
|
? "success"
|
|
: "idle";
|
|
|
|
async function runAction(actionId: WorkflowActionId) {
|
|
const action = actions.find((item) => item.id === actionId);
|
|
if (!action) {
|
|
return;
|
|
}
|
|
|
|
actionsApi.beginCompile(action.label);
|
|
try {
|
|
if (action.id === "load-sample" || action.id === "reset-sample") {
|
|
const parsed = actionsApi.applyJsonText(JSON.stringify(SAMPLE_PAYLOAD));
|
|
if (!parsed.ok) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
actionsApi.setCompileResult(null);
|
|
actionsApi.setSelection({ selectedRefs: [], selectedNet: null, selectedPin: null });
|
|
actionsApi.setViewport({ scale: 1, panX: 40, panY: 40 });
|
|
actionsApi.completeCompile(action.label);
|
|
return;
|
|
}
|
|
|
|
const payload = getPayloadOrThrow(store.getState().model);
|
|
|
|
switch (action.id) {
|
|
case "compile":
|
|
{
|
|
const result = await compile(payload);
|
|
actionsApi.setCompileResult(result);
|
|
}
|
|
break;
|
|
case "analyze":
|
|
{
|
|
const result = await analyze(payload);
|
|
actionsApi.setCompileResult(toCompileFromAnalyze(result));
|
|
}
|
|
break;
|
|
case "layout-auto":
|
|
{
|
|
const result = await autoLayout(payload);
|
|
if (result.model) {
|
|
actionsApi.setModel(result.model);
|
|
}
|
|
if (result.compile) {
|
|
actionsApi.setCompileResult(result.compile);
|
|
}
|
|
}
|
|
break;
|
|
case "layout-tidy":
|
|
{
|
|
const result = await tidyLayout(payload);
|
|
if (result.model) {
|
|
actionsApi.setModel(result.model);
|
|
}
|
|
if (result.compile) {
|
|
actionsApi.setCompileResult(result.compile);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
actionsApi.completeCompile(action.label);
|
|
} catch (err) {
|
|
actionsApi.failCompile(action.label, err);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<TopBar
|
|
title="React Frontend Skeleton"
|
|
actions={actions.map((action) => ({
|
|
id: action.id,
|
|
label: action.label,
|
|
onClick: () => {
|
|
void runAction(action.id);
|
|
},
|
|
disabled: lifecycle.isCompiling
|
|
}))}
|
|
statusMessage={status}
|
|
statusTone={statusTone}
|
|
apiVersion={lastApiVersion}
|
|
/>
|
|
<div className="workspace-grid">
|
|
<LeftPanel
|
|
model={model}
|
|
selection={selection}
|
|
onSelectInstance={(ref) =>
|
|
actionsApi.setSelection({
|
|
selectedRefs: selection.selectedRefs.includes(ref) ? [] : [ref],
|
|
selectedNet: null,
|
|
selectedPin: null
|
|
})
|
|
}
|
|
onSelectNet={(name) =>
|
|
actionsApi.setSelection({
|
|
selectedRefs: [],
|
|
selectedNet: selection.selectedNet === name ? null : name,
|
|
selectedPin: null
|
|
})
|
|
}
|
|
/>
|
|
<CanvasArea />
|
|
<RightInspector model={model} selection={selection} compileResult={compileResult} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|