Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/__tests__/editor-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ triggers: {}
expect(names).toContain('health-handler');
});

it('round-trips module satisfies keys for derived IaC modules', () => {
const { config, yaml } = roundTrip(`
modules:
- name: otel-collector
type: infra.otel_collector
satisfies:
- observability.telemetry.default
- observability.metrics.default
config:
endpoint: "\${OTEL_EXPORTER_OTLP_ENDPOINT}"
workflows: {}
triggers: {}
`);

const collector = config.modules.find((m) => m.name === 'otel-collector');
expect(collector?.satisfies).toEqual([
'observability.telemetry.default',
'observability.metrics.default',
]);
expect(yaml).toContain('satisfies:');
expect(yaml).toContain('observability.telemetry.default');
});

it('adding a node and exporting produces 4 modules', () => {
const { nodes, edges, parsed } = roundTrip(httpYaml);
// Simulate adding a middleware node
Expand Down
49 changes: 49 additions & 0 deletions src/components/properties/PropertyPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,55 @@ describe('PropertyPanel', () => {
expect(updatedNode?.data.label).toBe('My Custom Server');
});

it('edits module satisfies keys outside config', () => {
act(() => {
useModuleSchemaStore.getState().loadEditorBundle({
version: 'editor-bundle/v1',
moduleSchemas: {
'infra.otel_collector': {
type: 'infra.otel_collector',
label: 'OTel Collector',
category: 'observability',
configFields: [],
defaultConfig: {},
},
},
coercionRules: {},
contracts: {},
messages: {},
schemas: { app: {} },
});
useWorkflowStore.setState({
nodes: [{
id: 'otel-collector',
type: 'observabilityNode',
position: { x: 0, y: 0 },
data: {
moduleType: 'infra.otel_collector',
label: 'otel-collector',
config: {},
satisfies: ['observability.telemetry.default'],
},
}],
selectedNodeId: 'otel-collector',
});
});

render(<PropertyPanel />);

const satisfiesInput = screen.getByLabelText('Satisfies');
fireEvent.change(satisfiesInput, {
target: { value: 'observability.telemetry.default\nobservability.metrics.default' },
});

const updatedNode = useWorkflowStore.getState().nodes.find((n) => n.id === 'otel-collector');
expect(updatedNode?.data.satisfies).toEqual([
'observability.telemetry.default',
'observability.metrics.default',
]);
expect(updatedNode?.data.config.satisfies).toBeUndefined();
});

it('close button clears selection', () => {
act(() => {
useWorkflowStore.getState().addNode('http.server', { x: 0, y: 0 });
Expand Down
13 changes: 13 additions & 0 deletions src/components/properties/PropertyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default function PropertyPanel() {
const selectedNodeId = useWorkflowStore((s) => s.selectedNodeId);
const updateNodeConfig = useWorkflowStore((s) => s.updateNodeConfig);
const updateNodeName = useWorkflowStore((s) => s.updateNodeName);
const updateNodeSatisfies = useWorkflowStore((s) => s.updateNodeSatisfies);
const removeNode = useWorkflowStore((s) => s.removeNode);
const setSelectedNode = useWorkflowStore((s) => s.setSelectedNode);

Expand Down Expand Up @@ -477,6 +478,18 @@ export default function PropertyPanel() {
</div>
)}

{/* Module requirement keys */}
<label style={{ display: 'block', marginBottom: 16 }}>
<span style={{ color: '#a6adc8', fontSize: 11, display: 'block', marginBottom: 4 }}>Satisfies</span>
<textarea
aria-label="Satisfies"
value={((node.data.satisfies as string[] | undefined) ?? []).join('\n')}
onChange={(e) => updateNodeSatisfies(node.id, e.target.value.split('\n'))}
rows={Math.max(2, ((node.data.satisfies as string[] | undefined)?.length ?? 1))}
style={{ ...inputStyle, resize: 'vertical', fontFamily: 'monospace' }}
/>
</label>

{/* I/O Signature */}
{info?.ioSignature && (info.ioSignature.inputs.length > 0 || info.ioSignature.outputs.length > 0) && (
<div style={{ marginBottom: 16 }}>
Expand Down
12 changes: 12 additions & 0 deletions src/stores/workflowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface WorkflowNodeData extends Record<string, unknown> {
moduleType: string;
label: string;
config: Record<string, unknown>;
satisfies?: string[];
synthesized?: boolean;
/** Source file path this node originated from (multi-file configs) */
sourceFile?: string;
Expand Down Expand Up @@ -93,6 +94,7 @@ interface WorkflowStore {
removeNode: (id: string) => void;
updateNodeConfig: (id: string, config: Record<string, unknown>) => void;
updateNodeName: (id: string, name: string) => void;
updateNodeSatisfies: (id: string, satisfies: string[]) => void;
updateHandlerRoutes: (nodeId: string, routes: Array<{
method: string;
path: string;
Expand Down Expand Up @@ -453,6 +455,16 @@ const useWorkflowStore = create<WorkflowStore>()(
});
},

updateNodeSatisfies: (id, satisfies) => {
get().pushHistory();
const next = satisfies.map((key) => key.trim()).filter(Boolean);
set({
nodes: get().nodes.map((n) =>
n.id === id ? { ...n, data: { ...n.data, satisfies: next } } : n
),
});
},

updateHandlerRoutes: (nodeId, routes) => {
get().pushHistory();
set({
Expand Down
1 change: 1 addition & 0 deletions src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ModuleConfig {
type: string;
config?: Record<string, unknown>;
dependsOn?: string[];
satisfies?: string[];
branches?: Record<string, string>;
ui_position?: { x: number; y: number };
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ export function nodesToConfig(
mod.config = { ...node.data.config };
}

if (node.data.satisfies && node.data.satisfies.length > 0) {
mod.satisfies = [...node.data.satisfies];
}

const deps = dependencyMap[node.id];
if (deps && deps.length > 0) {
mod.dependsOn = deps;
Expand Down Expand Up @@ -575,6 +579,7 @@ export function configToNodes(
moduleType: mod.type,
label: mod.name,
config: mod.config ?? (info ? { ...info.defaultConfig } : {}),
...(mod.satisfies && mod.satisfies.length > 0 ? { satisfies: [...mod.satisfies] } : {}),
...(sourceFile ? { sourceFile } : {}),
},
});
Expand Down
Loading