Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(dashboard): more accurate task & noderun related data display #1276

Merged
merged 12 commits into from
Feb 5, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'
import { ReactNode } from "react"
import { DiagramDataGroupIndexer } from "./DiagramDataGroupIndexer";

export function DiagramDataGroup({ label, from, children, }: { label: string; from?: string; children?: ReactNode }) {
return <div className="relative flex flex-col justify-around min-w-36 h-fit w-fit bg-white rounded-lg">
<div className="absolute left-0 -top-5 w-fit px-3 py-1 bg-white rounded-lg font-semibold flex flex-nowrap gap-2">
{label}
</div>
{from && (
<div className="absolute left-0 -top-8 w-fit font-semibold text-gray-500 text-[8px]">
( From: {from} )
</div>
)}
<div className="flex flex-col gap-1 p-2 z-10 ">
{children}
</div>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { ChevronDown } from "lucide-react";

export function DiagramDataGroupIndexer({ index, setIndex, indexes }: { index: number, setIndex: (index: number) => void, indexes: number }) {
if (indexes <= 1) return null

return <div className="absolute right-0 -top-5">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-fit py-2 px-1 h-8 drop-shadow-none border-none">
{index} <ChevronDown className="w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-h-[300px] overflow-y-auto">
{Array.from({ length: indexes }, (_, i) => (
<DropdownMenuItem className="cursor-pointer" key={i} onClick={() => setIndex(i)}>
{i}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { formatDuration } from "@/app/utils";

export function Duration({ arrival, ended }: { arrival: string | undefined, ended: string | undefined }) {
if (!arrival) return null

const arrivalDate = new Date(arrival)
const endedDate = new Date(ended ?? Date.now())
const durationMs = endedDate.getTime() - arrivalDate.getTime()
const durationDisplay = formatDuration(durationMs)

const arrivalTime = arrivalDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) + `.${arrivalDate.getMilliseconds()}`;
const endedTime = endedDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) + `.${endedDate.getMilliseconds()}`;
const arrivalDay = arrivalDate.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' })
const endedDay = endedDate.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' })

return <div className="flex items-center justify-center gap-1">
<p className="font-light text-[10px] text-center">Arrival: <br /> {arrivalDay} ({arrivalTime})</p>
<div className="min-w-20 w-fit h-8 text-sm bg-blue-400 rounded-md items-center justify-center flex text-white">
{ended ? durationDisplay : 'N/A'}
</div>
<p className="font-light text-[10px] text-center">Ended: <br /> {ended ? `${endedDay} (${endedTime})` : 'N/A'}</p>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Separator } from "@/components/ui/separator";
import { ReactNode } from "react";

export function Entry({ label, separator, children }: { label?: string, separator?: boolean, children: ReactNode }) {
return <div className="flex flex-col">
<p className="font-light text-xs">{label}</p>
{separator && <Separator orientation="horizontal" className="my-2" />}
{children}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { OverflowText } from "@/app/(authenticated)/[tenantId]/components/OverflowText";
import { cn } from "@/components/utils";
export function ErrorMessage({ errorMessage }: { errorMessage: string | undefined }) {
return <div className={cn("text-xs w-full bg-gray-300 rounded-lg py-1 text-center border border-black", { "text-red-500 bg-red-300": !!errorMessage })}>
<OverflowText variant="error" className="py-0 px-2" text={errorMessage ?? "NO ERROR MESSAGE"} />
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { OverflowText } from "@/app/(authenticated)/[tenantId]/components/OverflowText";
import { getVariableValue } from "@/app/utils/variables";
import { cn } from "@/components/utils";
import { LHTaskError, LHTaskException, VariableValue } from "littlehorse-client/proto";
import { FC } from "react";

export function Result({ resultString, resultMessage, variant }: { resultString: string, resultMessage: string, variant?: "error" }) {
return <div className="flex gap-2 w-full rounded-lg border border-black p-1">
<div className={cn("bg-gray-300 rounded-lg py-1 text-center flex-1 text-xs flex items-center justify-center", {
"bg-red-300": variant === "error",
})}>
{resultString}
</div>
<div className={"bg-gray-300 rounded-lg text-center border border-black flex-1 max-w-32 text-nowrap px-1"} >
<OverflowText text={resultMessage} className="text-xs" variant={variant} />
</div>
</div>
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { cn } from "@/components/utils";
import { LHStatus, TaskStatus } from "littlehorse-client/proto";

export function Status({ status }: { status: LHStatus | TaskStatus }) {
const color = (statusColors as Record<string, string>)[status] ?? "bg-gray-500"

return <div className={cn("text-xs w-full bg-status-running rounded-lg py-1 text-center border border-black", color)}>
{status}
</div>
}

const statusColors: Partial<Record<LHStatus | TaskStatus, string>> = {
[LHStatus.RUNNING]: "bg-status-running",
[LHStatus.COMPLETED]: "bg-status-success",
[LHStatus.ERROR]: "bg-status-failed",
[LHStatus.EXCEPTION]: "bg-status-exception",
[LHStatus.HALTING]: "bg-status-halting",
[TaskStatus.TASK_SCHEDULED]: "bg-gray-300",
[TaskStatus.TASK_RUNNING]: "bg-status-running",
[TaskStatus.TASK_SUCCESS]: "bg-status-success",
[TaskStatus.TASK_FAILED]: "bg-status-failed",
[TaskStatus.TASK_EXCEPTION]: "bg-status-exception",
[TaskStatus.TASK_TIMEOUT]: "bg-status-halting",
[TaskStatus.TASK_PENDING]: "bg-status-running",
} as const;

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { OverflowText } from "@/app/(authenticated)/[tenantId]/components/OverflowText";
import { getVariableValue } from "@/app/utils/variables";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { VariableAssignment, VarNameAndVal } from "littlehorse-client/proto";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogClose
} from '@/components/ui/dialog';
import { EyeIcon } from "lucide-react";
import { VARIABLE_TYPES } from "@/app/constants";

export function ViewVariables({ variables }: { variables: VarNameAndVal[] }) {

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="flex items-center justify-center gap-2 text-xs m-0 p-0 px-0 py-2 h-fit w-full"><EyeIcon className="w-4 h-4" /> View Variables</Button>
</DialogTrigger>
<DialogContent>
<div className="flex flex-col gap-3 w-fit max-h-[300px] overflow-y-auto">
{variables.map((variable) => {
const variableType = VARIABLE_TYPES[Object.keys(variable.value!)[0].toUpperCase() as keyof typeof VARIABLE_TYPES]
return (
<div key={variable.varName} className="w-full flex gap-1 items-center">
<div className="rounded-lg flex items-center justify-center bg-yellow-100 p-1 text-xs font-semibold border border-black h-full">{variableType}</div>
<p className="text-xs font-bold text-purple-500 border text-center border-purple-500 rounded-lg p-2">{variable.varName}</p>
<p> = </p>
<div className={"px-2 border h-8 rounded-lg text-center max-w-96 text-nowrap min-h-5"} >
<OverflowText text={String(getVariableValue(variable.value))} className="text-xs" />
</div>
</div>
)
})}
</div>
</DialogContent>
</Dialog >
);
}

export function ViewVariableAssignments({ variables }: { variables: VariableAssignment[] }) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="flex items-center justify-center gap-2 text-xs m-0 p-0 px-0 py-2 h-fit w-full"><EyeIcon className="w-4 h-4" /> View Variables</Button>
</DialogTrigger>
<DialogContent>
<div className="flex flex-col gap-3 w-fit max-h-[300px] overflow-y-auto">
{variables.map((variable, i) => {
return (
<div key={variable.variableName} className="w-full flex gap-1 items-center">
<p className="text-xs font-bold text-purple-500 border text-center border-purple-500 rounded-lg p-2">arg{i}</p>
<p> = </p>
<div className={"px-2 border h-8 rounded-lg text-center max-w-96 text-nowrap min-h-5 flex items-center justify-center"} >
{`{${variable.variableName}}`}
</div>
</div>
)
})}
</div>
</DialogContent>
</Dialog >
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ExitNode: FC<NodeProps> = ({ data }) => {
return (
<Fade fade={fade} status={failureDef ? LHStatus.EXCEPTION : undefined}>
{failureDef && (
<NodeDetails>
<NodeDetails nodeRunList={data.nodeRunsList}>
<div className="mb-2 flex gap-1 text-nowrap">
<h3 className="font-bold">FailureDef</h3>
{failureDef.failureName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,40 @@ import { NodeDetails } from '../NodeDetails'

import { NodeRunsList } from '../../NodeRunsList'
import LinkWithTenant from '@/app/(authenticated)/[tenantId]/components/LinkWithTenant'
import { DiagramDataGroup } from '../DataGroupComponents/DiagramDataGroup'

const Node: FC<NodeProps<NodeProto>> = ({ data }) => {
if (!data.externalEvent) return null

const { fade, externalEvent: externalEventNode, nodeNeedsToBeHighlighted, nodeRun } = data
return (
<>
<NodeDetails>
<div>
<NodeDetails nodeRunList={data.nodeRunsList}>
<DiagramDataGroup label="ExternalEvent">
<div>
<div className="flex items-center gap-1 text-nowrap">
<h3 className="font-bold">ExternalEventDef</h3>
<LinkWithTenant
className="flex items-center justify-center gap-1 text-blue-500 hover:underline"
target="_blank"
href={`/externalEventDef/${externalEventNode.externalEventDefId?.name}`}
>
{externalEventNode.externalEventDefId?.name} <ExternalLinkIcon className="h-4 w-4" />
</LinkWithTenant>
</div>
{
<div className="flex gap-2 text-nowrap">
<div className="flex items-center justify-center">
Timeout:
{externalEventNode.timeoutSeconds
? formatTime(getVariableValue(externalEventNode.timeoutSeconds.literalValue) as number)
: 'N/A'}
</div>
<div>
<div className="flex gap-1 text-nowrap">
<LinkWithTenant
className="flex items-center justify-center gap-1 text-blue-500 hover:underline"
target="_blank"
href={`/externalEventDef/${externalEventNode.externalEventDefId?.name}`}
>
{externalEventNode.externalEventDefId?.name} <ExternalLinkIcon className="h-4 w-4" />
</LinkWithTenant>
</div>
}
{
<div className="flex gap-2 text-nowrap">
<div className="flex items-center justify-center">
Timeout:
{externalEventNode.timeoutSeconds
? formatTime(getVariableValue(externalEventNode.timeoutSeconds.literalValue) as number)
: 'N/A'}
</div>
</div>
}
</div>
</div>
<NodeRunsList nodeRuns={data?.nodeRunsList} />
</div>
</DiagramDataGroup>
</NodeDetails>
<Fade fade={fade} status={data?.nodeRunsList?.[data?.nodeRunsList.length - 1]?.status}>
<div className="relative cursor-pointer items-center justify-center text-xs">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { CSSProperties, FC, PropsWithChildren, useEffect, useMemo } from 'react'
'use client'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { CSSProperties, FC, PropsWithChildren, ReactNode, useEffect, useMemo, useState } from 'react'
import { internalsSymbol, useNodeId, useStore } from 'reactflow'
import { DiagramDataGroup } from './DataGroupComponents/DiagramDataGroup'
import React from 'react'
import { NodeRun, TaskRun } from 'littlehorse-client/proto'
import { Duration } from './DataGroupComponents/Duration'
import { Entry } from './DataGroupComponents/Entry'
import { ErrorMessage } from './DataGroupComponents/ErrorMessage'
import { Status } from './DataGroupComponents/Status'
import { DiagramDataGroupIndexer } from './DataGroupComponents/DiagramDataGroupIndexer'

type Props = PropsWithChildren<{}>
type Props = PropsWithChildren<{ nodeRunList: NodeRun[] | undefined, nodeRunsIndex?: number, setNodeRunsIndex?: (index: number) => void }>

export const NodeDetails: FC<Props> = ({ children, nodeRunList, nodeRunsIndex, setNodeRunsIndex }) => {
const [nodeRunsIndexInternal, setNodeRunsIndexInternal] = useState(nodeRunsIndex ?? 0);

export const NodeDetails: FC<Props> = ({ children }) => {
const contextNodeId = useNodeId()
const nodes = useStore(state => state.getNodes())
const setNodes = useStore(state => state.setNodes)
Expand All @@ -26,6 +39,12 @@ export const NodeDetails: FC<Props> = ({ children }) => {
}
}, [nodes, selectedNode, setNodes])

useEffect(() => {
if (nodeRunsIndex !== undefined && setNodeRunsIndex !== undefined) {
setNodeRunsIndex(nodeRunsIndexInternal);
}
}, [nodeRunsIndexInternal, setNodeRunsIndex]);

const zIndex: number = Math.max(...nodes.map(node => (node[internalsSymbol]?.z || 1) + 10))
if (!selectedNode) {
return null
Expand All @@ -38,12 +57,45 @@ export const NodeDetails: FC<Props> = ({ children }) => {
zIndex,
}

const diagramDataGroups = React.Children.toArray(children).flatMap(child => {
if (React.isValidElement(child)) {
if (child.type === DiagramDataGroup) {
return [child];
} else if (child.type === React.Fragment) {
return React.Children.toArray(child.props.children).filter(
fragmentChild => React.isValidElement(fragmentChild) && fragmentChild.type === DiagramDataGroup
);
}
}
return [];
}) as React.ReactElement[];


if (!nodeRunList || nodeRunList.length === 0) {
return null;
}

return (
<div style={wrapperStyle} className="flex flex-col justify-center drop-shadow">
<div className="max-w-96 rounded-md bg-white p-2 text-xs">{children}</div>
<div className="flex items-center justify-center">
<div className="transform-x-1/2 transform-y-1/2 h-4 w-4 border-[0.5rem] border-transparent border-t-white bg-transparent"></div>
</div>
<div style={wrapperStyle} className="flex gap-4 justify-center drop-shadow mb-6 items-start select-none">
{nodeRunList && (
<DiagramDataGroup label={nodeRunList.length > 1 ? `NodeRun #${nodeRunsIndexInternal}` : "NodeRun"} >
<DiagramDataGroupIndexer index={nodeRunsIndexInternal} setIndex={setNodeRunsIndexInternal} indexes={nodeRunList.length} />
<Entry label="Status:">
<Status status={nodeRunList[nodeRunsIndexInternal].status} />
</Entry>
<Entry label="Error Message:">
<ErrorMessage errorMessage={nodeRunList[nodeRunsIndexInternal].errorMessage} />
</Entry>
<Entry separator>
<Duration arrival={nodeRunList[nodeRunsIndexInternal].arrivalTime} ended={nodeRunList[nodeRunsIndexInternal].endTime} />
</Entry>
</DiagramDataGroup>
)}
{diagramDataGroups.map((element, i) => (
<span key={i}>
{element}
</span>
))}
</div>
)
}
}
Loading