Skip to content

Commit

Permalink
feat(dashboard): handle threads (#763)
Browse files Browse the repository at this point in the history
* feat(dashboard): handle StartThread, StartMultipleThreads and WaitForThreads

* test(dashboard): add test to concatWfRunIds util
  • Loading branch information
mijailr committed Apr 29, 2024
1 parent 51a9337 commit 1c49ed2
Show file tree
Hide file tree
Showing 19 changed files with 303 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'
import { formatDate } from '@/app/utils/date'
import { concatWfRunIds } from '@/app/utils/wfRun'
import { concatWfRunIds, formatDate } from '@/app/utils'
import { WfRun } from 'littlehorse-client/dist/proto/wf_run'
import Link from 'next/link'
import { FC } from 'react'
Expand Down
10 changes: 7 additions & 3 deletions dashboard-new/src/app/(authenticated)/wfRun/[...ids]/getWfRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export type WfRunResponse = {
export const getWfRun = async ({ ids }: Props): Promise<WfRunResponse> => {
const tenantId = cookies().get('tenantId')?.value
const client = await lhClient({ tenantId })
const wfRunId = ids.reverse().reduceRight<WfRunId|undefined>((parentWfRunId, id) => ({ id, parentWfRunId }), undefined)
const wfRunId = ids
.reverse()
.reduceRight<WfRunId | undefined>((parentWfRunId, id) => ({ id, parentWfRunId }), undefined)
const wfRun = await client.getWfRun(wfRunId!)
const [wfSpec, { results: nodeRuns }] = await Promise.all([
client.getWfSpec({ ...wfRun.wfSpecId }),
client.listNodeRuns({
wfRunId
wfRunId,
}),
])

Expand All @@ -37,6 +39,8 @@ export const getWfRun = async ({ ids }: Props): Promise<WfRunResponse> => {
const mergeThreadRunsWithNodeRuns = (threadRun: ThreadRun, nodeRuns: NodeRun[]): ThreadRunWithNodeRuns => {
return {
...threadRun,
nodeRuns: nodeRuns.filter(nodeRun => nodeRun.threadSpecName === threadRun.threadSpecName),
nodeRuns: nodeRuns.filter(
nodeRun => nodeRun.threadSpecName === threadRun.threadSpecName && nodeRun.id?.threadRunNumber === threadRun.number
),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export default async function Page({ params: { ids } }: Props) {

export async function generateMetadata({ params: { ids } }: Props): Promise<Metadata> {
return {
title: `WfRun ${ids[ids.length -1]} | Littlehorse`,
title: `WfRun ${ids[ids.length - 1]} | Littlehorse`,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { NodeRun } from 'littlehorse-client/dist/proto/node_run'
import { WfRun } from 'littlehorse-client/dist/proto/wf_run'
import { WfSpec } from 'littlehorse-client/dist/proto/wf_spec'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import ReactFlow, { Controls, Panel, useEdgesState, useNodesState } from 'reactflow'
import ReactFlow, { Controls, useEdgesState, useNodesState } from 'reactflow'
import 'reactflow/dist/base.css'
import { ThreadProvider, ThreadType } from '../hooks/useThread'
import { edgeTypes } from './EdgeTypes'
import { Layouter } from './Layouter'
import nodeTypes from './NodeTypes'
import { ThreadPanel } from './ThreadPanel'
import { extractEdges } from './extractEdges'
import { extractNodes } from './extractNodes'

Expand All @@ -18,73 +20,57 @@ type Props = {
spec: WfSpec
}

export const Diagram: FC<Props> = ({ spec, wfRun, nodeRuns }) => {
export const Diagram: FC<Props> = ({ spec, wfRun }) => {
const currentThread = wfRun
? wfRun.threadRuns[wfRun.greatestThreadrunNumber].threadSpecName
: spec.entrypointThreadName
const [thread, setThread] = useState(currentThread)
const [thread, setThread] = useState<ThreadType>({ name: currentThread, number: wfRun?.greatestThreadrunNumber || 0 })

const threadSpec = useMemo(() => {
if (thread === undefined) return spec.threadSpecs[spec.entrypointThreadName]
return spec.threadSpecs[thread]
return spec.threadSpecs[thread.name]
}, [spec, thread])

const [edges, setEdges] = useEdgesState(extractEdges(threadSpec))
const [nodes, setNodes] = useNodesState(extractNodes(threadSpec))

const threadRunNumber = useMemo(() => {
const threadRun = wfRun?.threadRuns.find(threadRun => threadRun.threadSpecName === thread)
return threadRun?.number
}, [thread, wfRun?.threadRuns])
const threadNodeRuns = useMemo(() => {
if (!wfRun || threadRunNumber === undefined) return
return wfRun.threadRuns[threadRunNumber].nodeRuns
}, [threadRunNumber, wfRun])
if (!wfRun) return
return wfRun.threadRuns[thread.number].nodeRuns
}, [thread, wfRun])

const updateGraph = useCallback(() => {
const { name } = thread
const threadSpec = spec.threadSpecs[name]
const nodes = extractNodes(threadSpec)
const edges = extractEdges(threadSpec)
setNodes(nodes)
setEdges(edges)
}, [setEdges, setNodes, threadSpec])
}, [spec.threadSpecs, thread, setNodes, setEdges])

useEffect(() => {
updateGraph()
}, [updateGraph])

return (
<div className="mb-4 min-h-[800px] min-w-full rounded border-2 border-slate-100 bg-slate-50 shadow-inner">
<ReactFlow
nodes={nodes}
edges={edges}
nodesConnectable={false}
elementsSelectable
minZoom={0.3}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid={true}
className="min-h-[800px] min-w-full bg-slate-50"
>
<Panel position="top-left">
<div className="flex w-full items-center justify-between gap-2">
{Object.keys(spec.threadSpecs)
.reverse()
.map(threadName => (
<button
className={
'border-[1px] p-2 text-sm shadow ' +
(threadName === thread ? 'bg-blue-500 text-white' : 'bg-white text-black')
}
key={threadName}
onClick={() => setThread(threadName)}
>
{threadName}
</button>
))}
</div>
</Panel>
<Controls />
</ReactFlow>
<Layouter nodeRuns={threadNodeRuns} />
</div>
<ThreadProvider value={{ thread, setThread }}>
<div className="mb-4 min-h-[800px] min-w-full rounded border-2 border-slate-100 bg-slate-50 shadow-inner">
<ReactFlow
nodes={nodes}
edges={edges}
nodesConnectable={false}
elementsSelectable
minZoom={0.3}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid={true}
className="min-h-[800px] min-w-full bg-slate-50"
>
<ThreadPanel spec={spec} wfRun={wfRun} />
<Controls />
</ReactFlow>
<Layouter nodeRuns={threadNodeRuns} />
</div>
</ThreadProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getVariable } from '@/app/utils'
import { PlusIcon } from '@heroicons/react/16/solid'
import { FC, memo } from 'react'
import { Handle, Position } from 'reactflow'
import { NodeProps } from '.'
import { useThread } from '../../hooks/useThread'
import { Fade } from './Fade'
import { NodeDetails } from './NodeDetails'

const Node: FC<NodeProps> = ({ data }) => {
const { fade } = data
const { setThread } = useThread()
if (data.startMultipleThreads === undefined) return
const variables = Object.entries(data.startMultipleThreads.variables)
return (
<>
<NodeDetails>
<div className="flex items-center items-center gap-1 text-nowrap">
<h3 className="font-bold">StartMultipleThread</h3>
{data.nodeRun === undefined ? (
<button
className="block whitespace-nowrap text-blue-500 hover:underline"
onClick={() => setThread({ name: data.startMultipleThreads?.threadSpecName || '', number: 0 })}
>
{data.startMultipleThreads?.threadSpecName}
</button>
) : (
<div>{data.startMultipleThreads?.threadSpecName}</div>
)}
</div>
<div className="">
<span className="font-bold">Iterable:</span> {getVariable(data.startMultipleThreads.iterable)}
</div>
{variables.length > 0 && (
<div className="mt-2">
<h2 className="font-bold">Variables</h2>
<ul>
{variables.map(([name, value]) => (
<li key={name}>
{`{${name}}`} {getVariable(value)}
</li>
))}
</ul>
</div>
)}

{data.nodeRun && (
<div className="mt-2">
<h2 className="font-bold">Thread Runs</h2>
<ul>
{data.nodeRun.startMultipleThreads?.childThreadIds.map(number => (
<li
className="cursor-pointer text-blue-500 hover:underline"
onClick={() => {
setThread({ name: data.startMultipleThreads?.threadSpecName || '', number })
}}
key={number}
>
{data.nodeRun?.startMultipleThreads?.threadSpecName}-{number}
</li>
))}
</ul>
</div>
)}
</NodeDetails>
<Fade fade={fade} status={data.nodeRun?.status}>
<div className="relative cursor-pointer">
<div className="ml-1 flex h-6 w-6 rotate-45 items-center justify-center border-[2px] border-gray-500 bg-gray-200">
<PlusIcon className="h-5 w-5 rotate-45 fill-gray-500" />
</div>
</div>
<Handle type="source" position={Position.Right} className="bg-transparent" />
<Handle type="target" position={Position.Left} className="bg-transparent" />
</Fade>
</>
)
}

export const StartMultipleThreads = memo(Node)
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import { ClockIcon } from '@heroicons/react/24/outline'
import { Node as NodeProto } from 'littlehorse-client/dist/proto/wf_spec'
import { getVariable } from '@/app/utils'
import { PlusIcon } from '@heroicons/react/16/solid'
import { FC, memo } from 'react'
import { Handle, Position } from 'reactflow'
import { NodeProps } from '.'
import { useThread } from '../../hooks/useThread'
import { Fade } from './Fade'
import { NodeDetails } from './NodeDetails'

const Node: FC<NodeProps<NodeProto>> = ({ data }) => {
const Node: FC<NodeProps> = ({ data }) => {
const { fade } = data
const { setThread } = useThread()
if (data.startThread === undefined) return
const variables = Object.entries(data.startThread.variables)
return (
<Fade fade={fade}>
<div className="relative cursor-pointer items-center justify-center text-xs">
<div className="items-center-justify-center flex rounded-full border-[1px] border-blue-500 bg-blue-200 p-[1px] text-xs">
<div className="items-center-justify-center flex h-10 w-10 items-center justify-center rounded-full border-[1px] border-blue-500 bg-blue-200 p-2 text-xs">
<ClockIcon className="h-4 w-4 fill-transparent stroke-blue-500" />
<>
<NodeDetails>
<div className="flex items-center items-center gap-1 text-nowrap">
<h3 className="font-bold">StartThread</h3>
<button
className="whitespace-nowrap text-blue-500 hover:underline"
onClick={() => setThread({ name: data.startThread?.threadSpecName || '', number: 0 })}
>
{data.startThread?.threadSpecName}
</button>
</div>
{variables.length > 0 && (
<div className="mt-2">
<h2 className="font-bold">Variables</h2>
<ul>
{variables.map(([name, value]) => (
<li key={name}>
{`{${name}}`} = {getVariable(value)}
</li>
))}
</ul>
</div>
)}
</NodeDetails>
<Fade fade={fade} status={data.nodeRun?.status}>
<div className="relative cursor-pointer">
<div className="ml-1 flex h-6 w-6 rotate-45 items-center justify-center border-[2px] border-gray-500 bg-gray-200">
<PlusIcon className="h-5 w-5 rotate-45 fill-gray-500" />
</div>
</div>
<Handle type="source" position={Position.Right} className="bg-transparent" />
<Handle type="target" position={Position.Left} className="bg-transparent" />
<div className="absolute flex w-full items-center justify-center whitespace-nowrap text-center ">
<div className="block">{data.startThread?.threadSpecName}</div>
</div>
</div>
</Fade>
</Fade>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const StatusPin: FC<{ status?: LHStatus }> = ({ status }) => {
if (!status) return <></>
const { color, Icon } = Status[status]
return (
<div className={`absolute -right-2 -top-2 rounded-full bg-${color}-200 z-10 p-1`}>
<div className={`absolute -right-4 -top-4 rounded-full bg-${color}-200 z-10 p-1`}>
<Icon className={`h-4 w-4 stroke-${color}-500 fill-transparent`} />
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { getVariable } from '@/app/utils'
import { Cog6ToothIcon } from '@heroicons/react/16/solid'
import { Node as NodeProto } from 'littlehorse-client/dist/proto/wf_spec'
import { FC, memo } from 'react'
import { Handle, Position } from 'reactflow'
import { NodeProps } from '.'
import { Fade } from './Fade'
import { TaskDetails } from './TaskDetails'

const Node: FC<NodeProps<NodeProto>> = ({ selected, data }) => {
const Node: FC<NodeProps> = ({ selected, data }) => {
const { fade } = data
if (!data.task) return null
const { task } = data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const TaskDetails: FC<{ task?: TaskNode; nodeRun?: NodeRun }> = ({ task,
return (
<NodeDetails>
<div className="mb-2">
<div className="flex items-center items-center gap-1 text-nowrap">
<div className="flex items-center items-center gap-1 whitespace-nowrap text-nowrap">
<h3 className="font-bold">TaskDef</h3>
{nodeRun ? (
<TaskLink taskName={task.taskDefId?.name} />
Expand All @@ -28,7 +28,7 @@ export const TaskDetails: FC<{ task?: TaskNode; nodeRun?: NodeRun }> = ({ task,
</div>
</div>
{task.variables && task.variables.length > 0 && (
<div className="">
<div className="whitespace-nowrap">
<h3 className="font-bold">Inputs</h3>
<ul className="list-inside list-disc">
{task.variables.map((variable, i) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { getVariable } from '@/app/utils'
import { UserIcon } from '@heroicons/react/16/solid'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid'
import { Node as NodeProto } from 'littlehorse-client/dist/proto/wf_spec'
import Link from 'next/link'
import { FC, memo } from 'react'
import { Handle, Position } from 'reactflow'
import { NodeProps } from '.'
import { Fade } from './Fade'
import { NodeDetails } from './NodeDetails'

const Node: FC<NodeProps<NodeProto>> = ({ data, selected }) => {
const Node: FC<NodeProps> = ({ data, selected }) => {
if (!data.userTask) return null
const { fade, userTask } = data
return (
Expand Down
Loading

0 comments on commit 1c49ed2

Please sign in to comment.