Skip to content

Commit ec1e95a

Browse files
authored
Merge pull request transformerlab#394 from transformerlab/workflow-runs-ui
basic runs UI
2 parents b330c56 + 7440fc9 commit ec1e95a

File tree

3 files changed

+204
-1
lines changed

3 files changed

+204
-1
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { Button } from '@mui/joy';
2+
import {
3+
Background,
4+
ControlButton,
5+
Controls,
6+
ReactFlow,
7+
ReactFlowProvider,
8+
useEdgesState,
9+
useNodesState,
10+
useReactFlow,
11+
addEdge,
12+
reconnectEdge,
13+
} from '@xyflow/react';
14+
import { PlusCircleIcon } from 'lucide-react';
15+
import { useCallback, useEffect, useRef } from 'react';
16+
import CustomNode from './nodes/CustomNode';
17+
import StartNode from './nodes/StartNode';
18+
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
19+
20+
const nodeTypes = { customNode: CustomNode, startNode: StartNode };
21+
22+
function generateNodes(workflow: any, workflowRun: any): any[] {
23+
const workflowConfig = JSON.parse(workflow?.config);
24+
const workflowRunNodes = JSON.parse(workflowRun.run.node_ids);
25+
const workflowRunJobs = workflowRun.jobs;
26+
27+
if (workflowConfig.nodes.length == 0) {
28+
return [];
29+
}
30+
31+
let out: any[] = [];
32+
let currentTask = workflowConfig.nodes[0].id;
33+
let position = 0;
34+
35+
for (let i = 0; i < workflowConfig.nodes.length; i++) {
36+
const node = workflowConfig.nodes[i];
37+
let status = 'NOT QUEUED';
38+
39+
for (let i = 0; i < workflowRunNodes.length; i++) {
40+
if (node.id == workflowRunNodes[i]) {
41+
status = workflowRunJobs[i].status;
42+
}
43+
}
44+
console.log(status);
45+
46+
const data = {
47+
id: node?.id,
48+
label: node.name,
49+
jobType: status,
50+
template: node.template,
51+
metadata: node?.metadata,
52+
};
53+
54+
const savedPosition = node?.metadata?.position || { x: 0, y: position };
55+
56+
const nextNode = {
57+
id: node.id,
58+
type: node?.type == 'START' ? 'startNode' : 'customNode',
59+
position: savedPosition,
60+
data: data,
61+
};
62+
out.push(nextNode);
63+
position += 120;
64+
}
65+
66+
return out;
67+
}
68+
69+
function generateEdges(workflow: any) {
70+
const workflowConfig = JSON.parse(workflow?.config);
71+
const workflowId = workflow?.id;
72+
73+
if (workflowConfig.nodes.length < 1) {
74+
return [];
75+
}
76+
77+
let out: any[] = [];
78+
let currentTask = workflowConfig.nodes[0].id;
79+
let ids = workflowConfig.nodes[0].id;
80+
81+
// console.log(workflowConfig);
82+
83+
for (let i = 0; i < workflowConfig.nodes.length; i++) {
84+
const currentNode = workflowConfig.nodes[i];
85+
86+
if (!Array.isArray(currentNode.out)) {
87+
continue;
88+
}
89+
currentNode.out.forEach((nextId) => {
90+
// check if this edge already exist in the out array:
91+
if (
92+
out.some(
93+
(edge) => edge.id === `${workflowId}-${currentNode.id}-${nextId}`,
94+
)
95+
) {
96+
return;
97+
}
98+
out.push({
99+
id: `${workflowId}-${currentNode.id}-${nextId}`,
100+
source: currentNode.id,
101+
target: nextId,
102+
animated: true,
103+
type: 'default',
104+
style: {
105+
stroke: 'var(--joy-palette-primary-outlinedDisabledColor)',
106+
strokeWidth: 1.5,
107+
},
108+
markerEnd: {
109+
type: 'arrow',
110+
color: 'var(--joy-palette-primary-outlinedDisabledColor)',
111+
width: 12,
112+
height: 10,
113+
strokeWidth: 2,
114+
},
115+
});
116+
});
117+
}
118+
// console.log(out);
119+
return out;
120+
}
121+
122+
const Flow = ({ selectedWorkflowRun }: { selectedWorkflowRun: any }) => {
123+
const selectedWorkflow = selectedWorkflowRun.workflow;
124+
const [nodes, setNodes, onNodesChange] = useNodesState(
125+
generateNodes(selectedWorkflow, selectedWorkflowRun),
126+
);
127+
const [edges, setEdges, onEdgesChange] = useEdgesState(
128+
generateEdges(selectedWorkflow),
129+
);
130+
131+
const reactFlowInstance = useReactFlow();
132+
133+
const workflowId = selectedWorkflow?.id;
134+
135+
// The workflow isn't updating when I switch workflows
136+
// so I do this hack:
137+
useEffect(() => {
138+
// console.log('updating workflow');
139+
setNodes(generateNodes(selectedWorkflow, selectedWorkflowRun));
140+
setEdges(generateEdges(selectedWorkflow));
141+
}, [selectedWorkflow]);
142+
143+
// Use fitView after the component mounts
144+
useEffect(() => {
145+
// Wait a moment to ensure the flow is rendered before fitting
146+
const timer = setTimeout(() => {
147+
reactFlowInstance.fitView({
148+
includeHiddenNodes: false, // Don't include hidden nodes
149+
minZoom: 0.5, // Set minimum zoom level
150+
maxZoom: 2, // Set maximum zoom level
151+
});
152+
}, 100);
153+
154+
return () => clearTimeout(timer);
155+
}, [reactFlowInstance, selectedWorkflow]);
156+
157+
return (
158+
<ReactFlow
159+
nodes={nodes}
160+
edges={edges}
161+
nodeTypes={nodeTypes}
162+
snapToGrid={true}
163+
snapGrid={[15, 15]}
164+
elementsSelectable={true}
165+
nodesDraggable={true}
166+
nodesConnectable={true}
167+
fitView
168+
zoomOnScroll={true}
169+
zoomOnPinch={false}
170+
zoomOnDoubleClick={false}
171+
panOnScroll={false}
172+
style={{
173+
backgroundColor:
174+
'color-mix(in srgb, var(--joy-palette-background-level1), white 60%)',
175+
}}
176+
>
177+
<Background color="#96ADE9" />
178+
</ReactFlow>
179+
);
180+
};
181+
182+
export default function WorkflowRunCanvas({
183+
selectedWorkflowRun,
184+
}: {
185+
selectedWorkflowRun: any;
186+
}) {
187+
if (!selectedWorkflowRun) {
188+
return null;
189+
}
190+
return (
191+
<ReactFlowProvider>
192+
<Flow selectedWorkflowRun={selectedWorkflowRun} />
193+
</ReactFlowProvider>
194+
);
195+
}

src/renderer/components/Experiment/Workflows/WorkflowRuns.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import useSWR from 'swr';
1414
import TinyCircle from 'renderer/components/Shared/TinyCircle';
1515
import { useEffect, useState } from 'react';
1616
import * as chatAPI from '../../../lib/transformerlab-api-sdk';
17+
import WorkflowRunCanvas from './WorkflowRunCanvas';
1718

1819
const fetcher = (url: any) => fetch(url).then((res) => res.json());
1920

@@ -70,9 +71,14 @@ function ShowSelectedWorkflowRun({ selectedWorkflowRun }) {
7071
if (!selectedWorkflowRun) {
7172
return <div>No workflow run selected.</div>;
7273
}
74+
75+
const { data, error, isLoading, mutate } = useSWR(
76+
chatAPI.Endpoints.Workflows.GetRun(selectedWorkflowRun.id),
77+
fetcher,
78+
);
7379
return (
7480
<Sheet variant="soft" sx={{ height: '100%', p: 2 }}>
75-
<pre>{JSON.stringify(selectedWorkflowRun, null, 2)}</pre>
81+
<WorkflowRunCanvas selectedWorkflowRun={data} />
7682
</Sheet>
7783
);
7884
}

src/renderer/lib/transformerlab-api-sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,8 @@ Endpoints.Workflows = {
11241124
RunWorkflow: (workflowId: string) =>
11251125
API_URL() + 'workflows/' + workflowId + '/start',
11261126
ListRuns: () => API_URL() + 'workflows/list_runs',
1127+
GetRun: (workflowRunID: string) =>
1128+
API_URL() + 'workflows/runs/' + workflowRunID,
11271129
};
11281130

11291131
Endpoints.Dataset = {

0 commit comments

Comments
 (0)