feat: show custom node contracts in the editor

This commit is contained in:
eust-w 2026-03-30 03:18:54 +08:00
parent 3e0f96dc4d
commit 91e7d6a802
6 changed files with 87 additions and 1 deletions

View File

@ -9,7 +9,7 @@ bootstrap:
test: test:
python3 -m unittest discover -s tests -p 'test_*.py' python3 -m unittest discover -s tests -p 'test_*.py'
pnpm --filter api test pnpm --filter api test
pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx src/runtime/workflow-editor-state.test.ts src/runtime/i18n.test.ts pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx src/runtime/workflow-editor-state.test.ts src/runtime/i18n.test.ts src/runtime/custom-node-presenter.test.ts
pnpm --filter web build pnpm --filter web build
pnpm --filter worker test pnpm --filter worker test

View File

@ -85,6 +85,7 @@ The shell now also exposes a dedicated Nodes page for project-scoped custom cont
The Workflows workspace now includes a template gallery. Projects can start from default or saved templates, or create a blank workflow directly. The Workflows workspace now includes a template gallery. Projects can start from default or saved templates, or create a blank workflow directly.
The workflow editor center panel now uses a real draggable node canvas with zoom, pan, mini-map, dotted background, handle-based edge creation, persisted node positions, and localized validation feedback instead of a static list of node cards. The workflow editor center panel now uses a real draggable node canvas with zoom, pan, mini-map, dotted background, handle-based edge creation, persisted node positions, and localized validation feedback instead of a static list of node cards.
The workflow editor right panel now also supports saving the current workflow draft as a reusable workflow template, in addition to editing per-node runtime settings and Python hooks. The workflow editor right panel now also supports saving the current workflow draft as a reusable workflow template, in addition to editing per-node runtime settings and Python hooks.
When a custom node is selected on the canvas, the right panel now also exposes its declared input contract, output contract, artifact type, and container source so the operator can confirm compatibility without leaving the editor.
The node library now supports both click-to-append and drag-and-drop placement into the canvas. When a node is inserted from the library, the editor now seeds its default runtime contract directly into the workflow draft, so custom Docker nodes keep their declared executor type and I/O contract without extra manual edits. V1 connection rules block self-edges, duplicate edges, cycles, incoming edges into source nodes, outgoing edges from export nodes, and multiple upstream edges into ordinary nodes, while allowing multi-input set nodes such as `union-assets`, `intersect-assets`, and `difference-assets` plus any custom node whose runtime contract declares `inputMode=multi_asset_set`. The node library now supports both click-to-append and drag-and-drop placement into the canvas. When a node is inserted from the library, the editor now seeds its default runtime contract directly into the workflow draft, so custom Docker nodes keep their declared executor type and I/O contract without extra manual edits. V1 connection rules block self-edges, duplicate edges, cycles, incoming edges into source nodes, outgoing edges from export nodes, and multiple upstream edges into ordinary nodes, while allowing multi-input set nodes such as `union-assets`, `intersect-assets`, and `difference-assets` plus any custom node whose runtime contract declares `inputMode=multi_asset_set`.
The Runs workspace now shows project-scoped run history, run-level aggregated summaries, cancel/retry controls, and run detail views with persisted task summaries, stdout/stderr sections, result previews, and artifact links into Explore. The Runs workspace now shows project-scoped run history, run-level aggregated summaries, cancel/retry controls, and run detail views with persisted task summaries, stdout/stderr sections, result previews, and artifact links into Explore.
Selected run tasks now expose the frozen node definition id, executor config snapshot, and code-hook metadata that were captured when the run was created. Selected run tasks now expose the frozen node definition id, executor config snapshot, and code-hook metadata that were captured when the run was created.

View File

@ -18,6 +18,11 @@ import {
type CustomNodeValidationIssue, type CustomNodeValidationIssue,
validateCustomNodeDefinition, validateCustomNodeDefinition,
} from "../../../../packages/contracts/src/custom-node.ts"; } from "../../../../packages/contracts/src/custom-node.ts";
import {
formatCustomNodeInputModeKey,
formatCustomNodeOutputModeKey,
formatCustomNodeSourceKindKey,
} from "./custom-node-presenter.ts";
import { import {
localizeNodeDefinition, localizeNodeDefinition,
type TranslationKey, type TranslationKey,
@ -1352,6 +1357,24 @@ function WorkflowEditorPage(props: {
() => getEffectiveNodeRuntimeConfig(selectedNodeRaw, selectedNodeRuntimeConfig), () => getEffectiveNodeRuntimeConfig(selectedNodeRaw, selectedNodeRuntimeConfig),
[selectedNodeRaw, selectedNodeRuntimeConfig], [selectedNodeRaw, selectedNodeRuntimeConfig],
); );
const selectedNodeContract = useMemo(
() =>
(selectedNodeEffectiveRuntimeConfig.executorConfig as {
contract?: {
inputMode?: string;
outputMode?: string;
artifactType?: string;
};
} | undefined)?.contract,
[selectedNodeEffectiveRuntimeConfig],
);
const selectedNodeSourceKind = useMemo(
() =>
(selectedNodeEffectiveRuntimeConfig.executorConfig as {
kind?: string;
} | undefined)?.kind,
[selectedNodeEffectiveRuntimeConfig],
);
const canvasNodes = useMemo<Array<Node>>( const canvasNodes = useMemo<Array<Node>>(
() => () =>
draft.logicGraph.nodes.map((node) => { draft.logicGraph.nodes.map((node) => {
@ -1669,6 +1692,14 @@ function WorkflowEditorPage(props: {
<p>{selectedNode.description}</p> <p>{selectedNode.description}</p>
<p>{t("category")}: {selectedNode.category}</p> <p>{t("category")}: {selectedNode.category}</p>
<p>{t("definition")}: {selectedNodeDefinitionId}</p> <p>{t("definition")}: {selectedNodeDefinitionId}</p>
{selectedNodeContract ? (
<>
<p>{t("customNodeInputMode")}: {t(formatCustomNodeInputModeKey(selectedNodeContract.inputMode))}</p>
<p>{t("customNodeOutputMode")}: {t(formatCustomNodeOutputModeKey(selectedNodeContract.outputMode))}</p>
<p>{t("customNodeArtifactType")}: {selectedNodeContract.artifactType ?? t("none")}</p>
<p>{t("customNodeSourceKind")}: {t(formatCustomNodeSourceKindKey(selectedNodeSourceKind))}</p>
</>
) : null}
<div className="field-grid"> <div className="field-grid">
<label> <label>
{t("executorType")} {t("executorType")}

View File

@ -0,0 +1,27 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
formatCustomNodeInputModeKey,
formatCustomNodeOutputModeKey,
formatCustomNodeSourceKindKey,
} from "./custom-node-presenter.ts";
test("map custom node input mode values to localized translation keys", () => {
assert.equal(formatCustomNodeInputModeKey("multi_asset_set"), "customNodeMultiAssetSet");
assert.equal(formatCustomNodeInputModeKey("single_asset_set"), "customNodeSingleAssetSet");
assert.equal(formatCustomNodeInputModeKey("unexpected"), "customNodeSingleAssetSet");
});
test("map custom node output mode values to localized translation keys", () => {
assert.equal(formatCustomNodeOutputModeKey("asset_set"), "customNodeAssetSet");
assert.equal(formatCustomNodeOutputModeKey("asset_set_with_report"), "customNodeAssetSetWithReport");
assert.equal(formatCustomNodeOutputModeKey("report"), "customNodeReport");
assert.equal(formatCustomNodeOutputModeKey("unexpected"), "customNodeReport");
});
test("map custom node source kind values to localized translation keys", () => {
assert.equal(formatCustomNodeSourceKindKey("image"), "customNodeSourceImage");
assert.equal(formatCustomNodeSourceKindKey("dockerfile"), "customNodeSourceDockerfile");
assert.equal(formatCustomNodeSourceKindKey("other"), "none");
});

View File

@ -0,0 +1,26 @@
import type { TranslationKey } from "./i18n.tsx";
export function formatCustomNodeInputModeKey(value: unknown): TranslationKey {
return value === "multi_asset_set" ? "customNodeMultiAssetSet" : "customNodeSingleAssetSet";
}
export function formatCustomNodeOutputModeKey(value: unknown): TranslationKey {
switch (value) {
case "asset_set":
return "customNodeAssetSet";
case "asset_set_with_report":
return "customNodeAssetSetWithReport";
default:
return "customNodeReport";
}
}
export function formatCustomNodeSourceKindKey(value: unknown): TranslationKey {
if (value === "dockerfile") {
return "customNodeSourceDockerfile";
}
if (value === "image") {
return "customNodeSourceImage";
}
return "none";
}

View File

@ -236,6 +236,7 @@ It should render:
This panel is critical. It should feel like a structured system console, not a generic form dump. This panel is critical. It should feel like a structured system console, not a generic form dump.
The current right panel also includes a workflow-level `Save As Template` section so an edited graph can be published back into the project template library. The current right panel also includes a workflow-level `Save As Template` section so an edited graph can be published back into the project template library.
For project-scoped custom nodes, the right panel should also surface the declared contract summary directly from the node definition, including input mode, output mode, artifact type, and whether the backing runtime came from a Docker image or Dockerfile definition.
## Screen 5: Workflow Run Detail ## Screen 5: Workflow Run Detail