Skip to content

Commit

Permalink
feat(dashboard): interactive nodes (#775)
Browse files Browse the repository at this point in the history
* feat(dashboard): add taskRun details

* feat(wfRun): improve thread selector
  • Loading branch information
mijailr authored Apr 30, 2024
1 parent 1c49ed2 commit 39db0cc
Show file tree
Hide file tree
Showing 56 changed files with 348 additions and 107 deletions.
1 change: 1 addition & 0 deletions dashboard-new/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion dashboard-new/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "next start",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "jest"
"test": "jest",
"preinstall": "cd ../sdk-js && npm i && npm run build"
},
"dependencies": {
"@headlessui/react": "^1.7.18",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
'use client'
import { ThreadRunWithNodeRuns } from '@/app/(authenticated)/wfRun/[...ids]/getWfRun'
import { ThreadRunWithNodeRuns } from '@/app/(authenticated)/(diagram)/wfRun/[...ids]/getWfRun'
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, useEdgesState, useNodesState } from 'reactflow'
import 'reactflow/dist/base.css'
import { ThreadProvider, ThreadType } from '../hooks/useThread'
import { ThreadProvider, ThreadType } from '../context'
import { edgeTypes } from './EdgeTypes'
import { extractEdges } from './EdgeTypes/extractEdges'
import { Layouter } from './Layouter'
import nodeTypes from './NodeTypes'
import { extractNodes } from './NodeTypes/extractNodes'
import { ThreadPanel } from './ThreadPanel'
import { extractEdges } from './extractEdges'
import { extractNodes } from './extractNodes'

type Props = {
wfRun?: WfRun & { threadRuns: ThreadRunWithNodeRuns[] }
Expand Down Expand Up @@ -54,6 +54,7 @@ export const Diagram: FC<Props> = ({ spec, wfRun }) => {

return (
<ThreadProvider value={{ thread, setThread }}>
<ThreadPanel spec={spec} wfRun={wfRun} />
<div className="mb-4 min-h-[800px] min-w-full rounded border-2 border-slate-100 bg-slate-50 shadow-inner">
<ReactFlow
nodes={nodes}
Expand All @@ -66,7 +67,6 @@ export const Diagram: FC<Props> = ({ spec, wfRun }) => {
snapToGrid={true}
className="min-h-[800px] min-w-full bg-slate-50"
>
<ThreadPanel spec={spec} wfRun={wfRun} />
<Controls />
</ReactFlow>
<Layouter nodeRuns={threadNodeRuns} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { FC } from 'react'
import { ModalComponents } from '.'
import { useModal } from '../../hooks/useModal'

export const Modals: FC = () => {
const { modal } = useModal()
if (!modal) return null
const Component = ModalComponents[modal.type]
return <Component {...modal} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { getVariableValue } from '@/app/utils'
import { Dialog } from '@headlessui/react'
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
import { FC, useMemo, useState } from 'react'
import { Modal } from '../../context'
import { useModal } from '../../hooks/useModal'

export const TaskRun: FC<Modal> = ({ data }) => {
const { showModal, setShowModal } = useModal()
const [attemptIndex, setAttemptIndex] = useState(data.attempts.length - 1)
const attempt = useMemo(() => data.attempts[attemptIndex], [attemptIndex, data.attempts])
return (
<Dialog open={showModal} className="relative z-50" onClose={() => setShowModal(false)}>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-1/3 min-w-fit rounded bg-white p-2">
<Dialog.Title className="mb-2 flex items-center justify-between">
<h2 className="text-lg font-bold">TaskRun</h2>
<div className="item-center flex gap-1 bg-gray-200 px-2 py-1">
<span className="font-mono text-sm">{data.id?.taskGuid}</span>
<ClipboardDocumentIcon className="h-4 w-4 fill-transparent stroke-blue-500" />
</div>
</Dialog.Title>
<Dialog.Description>
<div className="">
<div className="flex items-center justify-between bg-green-200 p-2">
<div className="flex items-center gap-1">
<div className="flex gap-1">
{data.attempts.reverse().map((_, i) => {
const index = data.attempts.length - i - 1
return (
<button
key={`attempt-${index}`}
className={`border-2 border-blue-500 px-2 ${attemptIndex === index ? 'text-blue-500' : 'bg-blue-500 text-white'}`}
onClick={() => setAttemptIndex(index)}
>
{index}
</button>
)
})}
</div>
</div>
<div className="">
<div className="">{attempt.status}</div>
</div>
</div>
<div className="p-2">
<div className="flex items-center gap-2">
<div className="font-bold">startTime:</div>
<div className="">{attempt.startTime}</div>
</div>
<div className="flex items-center gap-2">
<div className="font-bold">endTime:</div>
<div className="">{attempt.endTime}</div>
</div>
</div>
{attempt.output && (
<div className="mt-2 flex flex-col rounded bg-zinc-500 p-1 text-white">
<h3 className="font-bold">Output</h3>
<pre className="overflow-x-auto">{getVariableValue(attempt.output)}</pre>
</div>
)}
</div>
</Dialog.Description>
</Dialog.Panel>
</div>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FC } from 'react'
import { Modal } from '../../context'
import { TaskRun } from './TaskRun'

export * from './Modals'

export type ModalComponent = FC<Modal>

export const ModalComponents = {
taskRun: TaskRun,
} as const

export type ModalType = keyof typeof ModalComponents
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { getVariable } from '@/app/utils'
import { Cog6ToothIcon } from '@heroicons/react/16/solid'
import { FC, memo } from 'react'
import { Handle, Position } from 'reactflow'
import { NodeProps } from '.'
import { Fade } from './Fade'
import { NodeProps } from '..'
import { Fade } from '../Fade'
import { TaskDetails } from './TaskDetails'

const Node: FC<NodeProps> = ({ selected, data }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { getVariable } from '@/app/utils'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid'
import { ArrowTopRightOnSquareIcon, EyeIcon } from '@heroicons/react/24/solid'
import { useQuery } from '@tanstack/react-query'
import { TaskNode } from 'littlehorse-client/dist/proto/common_wfspec'
import { NodeRun } from 'littlehorse-client/dist/proto/node_run'
import Link from 'next/link'
import { FC } from 'react'
import { NodeDetails } from './NodeDetails'
import { FC, useCallback } from 'react'
import { useModal } from '../../../hooks/useModal'
import { NodeDetails } from '../NodeDetails'
import { getTaskRun } from './getTaskRun'

export const TaskDetails: FC<{ task?: TaskNode; nodeRun?: NodeRun }> = ({ task, nodeRun }) => {
const { data } = useQuery({
queryKey: ['taskRun', nodeRun],
queryFn: async () => {
if (nodeRun?.task?.taskRunId) return await getTaskRun(nodeRun.task.taskRunId)
},
})

const { setModal, setShowModal } = useModal()

const onClick = useCallback(() => {
if (data) {
setModal({ type: 'taskRun', data: data })
setShowModal(true)
}
}, [data, setModal, setShowModal])

if (!task) return null
return (
<NodeDetails>
Expand All @@ -30,11 +49,15 @@ export const TaskDetails: FC<{ task?: TaskNode; nodeRun?: NodeRun }> = ({ task,
{task.variables && task.variables.length > 0 && (
<div className="whitespace-nowrap">
<h3 className="font-bold">Inputs</h3>
<ul className="list-inside list-disc">
<ol className="list-inside list-decimal">
{task.variables.map((variable, i) => (
<li key={`variable.${i}`}>{getVariable(variable)}</li>
<li className="mb-1 flex gap-1" key={`variable.${i}`}>
<div className="bg-gray-200 px-2 font-mono text-fuchsia-500">arg{i}</div>
<div> = </div>
<div className="truncate">{getVariable(variable)}</div>
</li>
))}
</ul>
</ol>
</div>
)}
{nodeRun && nodeRun.errorMessage && (
Expand All @@ -43,6 +66,14 @@ export const TaskDetails: FC<{ task?: TaskNode; nodeRun?: NodeRun }> = ({ task,
<pre className="overflow-x-auto">{nodeRun.errorMessage}</pre>
</div>
)}
{nodeRun && (
<div className="mt-2 flex justify-center">
<button className="flex items-center gap-1 p-1 text-blue-500 hover:bg-gray-200" onClick={onClick}>
<EyeIcon className="h-4 w-4" />
Inspect TaskRun
</button>
</div>
)}
</NodeDetails>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use server'
import { lhClient } from '@/app/lhClient'
import { WithTenant } from '@/types'
import { TaskRunId } from 'littlehorse-client/dist/proto/object_id'

export type TaskRunRequestProps = TaskRunId & WithTenant
export const getTaskRun = async ({ tenantId, ...req }: TaskRunRequestProps) => {
const client = await lhClient({ tenantId })
return client.getTaskRun(req)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Task'
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Node: FC<NodeProps> = ({ data, selected }) => {
{userTask.notes && (
<div className="rounded bg-gray-200 p-1">
<h3 className="mb-1 font-bold">Notes</h3>
<pre>{getVariable(userTask.notes)}</pre>
<pre className="overflow-x-auto">{getVariable(userTask.notes)}</pre>
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { NodeRun } from 'littlehorse-client/dist/proto/node_run'
import { Node } from 'littlehorse-client/dist/proto/wf_spec'
import { ComponentType } from 'react'
import { NodeProps as NodeFlow } from 'reactflow'
import { NodeType } from '../extractNodes'
import { Entrypoint } from './Entrypoint'
import { Exit } from './Exit'
import { ExternalEvent } from './ExternalEvent'
import { Nop } from './Nop'
import { Sleep } from './Sleep'
import { StartMultipleThreads } from './StartMultipleThreads'
import { StartThread } from './StartThread'
import { Task } from './Task'
import { ExternalEvent } from './ExternalEvent'
import { UserTask } from './UserTask'
import { WaitForThreads } from './WaitForThreads'
import { StartThread } from './StartThread'
import { Sleep } from './Sleep'
import { NodeRun } from 'littlehorse-client/dist/proto/node_run'
import { Node } from 'littlehorse-client/dist/proto/wf_spec'
import { StartMultipleThreads } from './StartMultipleThreads'
import { NodeType } from './extractNodes'

const nodeTypes: Record<NodeType, ComponentType<NodeProps>> = {
ENTRYPOINT: Entrypoint,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/solid'
import { WfRun } from 'littlehorse-client/dist/proto/wf_run'
import { WfSpec } from 'littlehorse-client/dist/proto/wf_spec'
import { FC, useEffect, useMemo } from 'react'
import { useScrollbar } from '../hooks/useScrollbar'
import { useThread } from '../hooks/useThread'

export const ThreadPanel: FC<{ spec: WfSpec; wfRun?: WfRun }> = ({ spec, wfRun }) => {
const { thread, setThread } = useThread()
const threads = useMemo(() => extractThreads(spec, wfRun), [spec, wfRun])
const { scroll, itemsRef, containerRef, maxScroll, scrollLeft, scrollRight } = useScrollbar()

return (
<div className="relative mb-2 flex items-center">
<div
className={`absolute left-0 top-0 z-10 flex after:h-[38px] after:w-[50px] after:bg-gradient-to-r after:from-white after:to-transparent after:content-[''] ${scroll === 0 ? 'hidden' : ''}`}
>
<button className="bg-white" onClick={() => scrollLeft()}>
<ChevronLeftIcon className="h-6 w-6" />
</button>
</div>
<div className="flex touch-pan-y items-center overflow-hidden text-nowrap" ref={containerRef}>
<div
className="flex gap-2 duration-[15ms] ease-[cubic-bezier(.05,0,0,1)] will-change-transform "
style={{ transform: `translateX(${scroll}px)` }}
ref={itemsRef}
>
{threads.map(({ name, number }) => (
<button
className={
'border-[1px] p-2 text-sm shadow ' +
(name === thread.name && number !== undefined && number === thread.number
? 'bg-blue-500 text-white'
: 'bg-white text-black')
}
key={`${name}-${number}`}
onClick={() => {
setThread(prev => {
const current = number === undefined ? { name } : { name, number }
return {
...prev,
...current,
}
})
}}
>
{`${name}${number !== undefined ? `-${number}` : ''}`}
</button>
))}
</div>
</div>
<div
className={`absolute right-0 top-0 flex before:h-[38px] before:w-[50px] before:bg-gradient-to-l before:from-white before:to-transparent before:content-[''] ${!maxScroll || -scroll >= maxScroll ? 'hidden' : ''}`}
>
<button className="bg-white" onClick={() => scrollRight()}>
<ChevronRightIcon className="h-6 w-6" />
</button>
</div>
</div>
)
}

const extractThreads = (spec: WfSpec, wfRun?: WfRun): { name: string; number?: number }[] => {
if (wfRun) {
return wfRun.threadRuns.map(threadRun => ({
name: threadRun.threadSpecName,
number: threadRun.number,
}))
}
return Object.keys(spec.threadSpecs).map(name => ({ name }))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TaskRun } from 'littlehorse-client/dist/proto/task_run'
import { Dispatch, SetStateAction, createContext } from 'react'
import { ModalType } from '../components/Modals'

export type Modal = {
type: ModalType
data: TaskRun
}

type ModalContextType = {
modal: Modal | null
setModal: Dispatch<SetStateAction<Modal | null>>
showModal: boolean
setShowModal: Dispatch<SetStateAction<boolean>>
}
export const ModalContext = createContext<ModalContextType>({
modal: null,
setModal: () => {},
showModal: false,
setShowModal: () => {},
})

export const ModalProvider = ModalContext.Provider
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Dispatch, SetStateAction, createContext } from 'react'

export type ThreadType = {
name: string
number: number
}
type ThreadContextType = {
thread: ThreadType
setThread: Dispatch<SetStateAction<ThreadType>>
}
export const ThreadContext = createContext<ThreadContextType>({ thread: { name: '', number: 0 }, setThread: () => {} })

export const ThreadProvider = ThreadContext.Provider
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ModalContext'
export * from './ThreadContext'
Loading

0 comments on commit 39db0cc

Please sign in to comment.