mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-21 08:40:10 -06:00
Fix: UI Bug Burndown, Part I (#1774)
* fix: horrific recursive query performance * feat: start refactoring waterfall a bit * fix: more waterfall cleanup * fix: overflow * fix: recenter button for dag view * fix: button order
This commit is contained in:
@@ -16,6 +16,7 @@ import { RunDetailProvider, useRunDetail } from '@/next/hooks/use-run-detail';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
|
||||
import { Button } from '@/next/components/ui/button';
|
||||
import { Fullscreen } from 'lucide-react';
|
||||
const connectionLineStyleDark = { stroke: '#fff' };
|
||||
const connectionLineStyleLight = { stroke: '#000' };
|
||||
|
||||
@@ -228,6 +229,7 @@ function WorkflowRunVisualizerContent({
|
||||
if (!reactFlowInstance.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = layoutedNodes?.find(
|
||||
(n: Node) => n.data.taskRun?.taskExternalId === selectedTaskId,
|
||||
);
|
||||
@@ -239,6 +241,8 @@ function WorkflowRunVisualizerContent({
|
||||
duration: 800,
|
||||
});
|
||||
lastCenteredTaskId.current = selectedTaskId;
|
||||
} else {
|
||||
reactFlowInstance.current.fitView();
|
||||
}
|
||||
}, 1);
|
||||
}, [selectedTaskId, layoutedNodes]);
|
||||
@@ -311,20 +315,30 @@ function WorkflowRunVisualizerContent({
|
||||
}
|
||||
snapToGrid={true}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleExpand}
|
||||
className="absolute bottom-2 right-2 z-20"
|
||||
tooltip={isExpanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">{isExpanded ? 'Collapse' : 'Expand'}</span>
|
||||
</Button>
|
||||
<div className="flex flex-col absolute bottom-2 right-2 z-20">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={recenter}
|
||||
tooltip={'Recenter'}
|
||||
>
|
||||
<Fullscreen className="size-4" />
|
||||
<span className="sr-only">{'Recenter'}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleExpand}
|
||||
tooltip={isExpanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">{isExpanded ? 'Collapse' : 'Expand'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,10 +49,13 @@ export function RunId({
|
||||
},
|
||||
);
|
||||
|
||||
const displayNameIdPrefix = splitTime(displayName);
|
||||
const friendlyDisplayName = displayNameIdPrefix || displayName;
|
||||
|
||||
const name = isTaskRun
|
||||
? getFriendlyTaskRunId(taskRun)
|
||||
: displayName && id
|
||||
? splitTime(displayName) + '-' + id.split('-')[0]
|
||||
? friendlyDisplayName + '-' + id.split('-')[0]
|
||||
: getFriendlyWorkflowRunId(wfRun);
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, Drill, Loader } from 'lucide-react';
|
||||
|
||||
import { ChartContainer, ChartTooltipContent } from '@/components/ui/chart';
|
||||
import { V1TaskStatus, V1TaskTiming } from '@/lib/api';
|
||||
@@ -19,15 +19,19 @@ import { ROUTES } from '@/next/lib/routes';
|
||||
import { useRunDetail } from '@/next/hooks/use-run-detail';
|
||||
import { Button } from '../ui/button';
|
||||
import { RunId } from '../runs/run-id';
|
||||
import {
|
||||
BsArrowDownRightCircle,
|
||||
BsCircle,
|
||||
BsArrowUpLeftCircle,
|
||||
} from 'react-icons/bs';
|
||||
import { BsArrowUpLeftCircle } from 'react-icons/bs';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import { useCurrentTenantId } from '@/next/hooks/use-tenant';
|
||||
import {
|
||||
TooltipProvider,
|
||||
Tooltip as BaseTooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '../ui/tooltip';
|
||||
|
||||
interface ProcessedTaskData {
|
||||
id: string;
|
||||
taskExternalId: string;
|
||||
workflowRunId?: string;
|
||||
taskDisplayName: string;
|
||||
parentId?: string;
|
||||
@@ -35,8 +39,8 @@ interface ProcessedTaskData {
|
||||
depth: number;
|
||||
isExpanded: boolean;
|
||||
offset: number;
|
||||
queuedDuration: number;
|
||||
ranDuration: number;
|
||||
queuedDuration: number | null;
|
||||
ranDuration: number | null;
|
||||
status: V1TaskStatus;
|
||||
taskId: number; // Added for tie-breaking
|
||||
attempt: number;
|
||||
@@ -503,9 +507,9 @@ export function Waterfall({
|
||||
}
|
||||
|
||||
// Find the global minimum time (queuedAt or startedAt) among visible tasks
|
||||
let globalMinTime = Number.MAX_SAFE_INTEGER;
|
||||
visibleTasks.forEach((id) => {
|
||||
const globalMinTime = [...visibleTasks].reduce((acc, id) => {
|
||||
const task = taskMap.get(id);
|
||||
|
||||
if (task) {
|
||||
// Use queuedAt if available, otherwise use startedAt
|
||||
const minTime = task.queuedAt
|
||||
@@ -514,25 +518,32 @@ export function Waterfall({
|
||||
? new Date(task.startedAt).getTime()
|
||||
: null;
|
||||
|
||||
if (minTime !== null && minTime < globalMinTime) {
|
||||
globalMinTime = minTime;
|
||||
if (minTime !== null && minTime < acc) {
|
||||
return minTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
// Create the processed data for rendering
|
||||
const data = Array.from(visibleTasks)
|
||||
const data = [...visibleTasks]
|
||||
.map((id) => {
|
||||
const task = taskMap.get(id);
|
||||
if (!task || !task.startedAt) {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle missing queuedAt by defaulting to startedAt (no queue time)
|
||||
const queuedAt = task.queuedAt
|
||||
? new Date(task.queuedAt).getTime()
|
||||
: new Date(task.startedAt).getTime();
|
||||
const startedAt = new Date(task.startedAt).getTime();
|
||||
: task.startedAt
|
||||
? new Date(task.startedAt).getTime()
|
||||
: new Date(task.taskInsertedAt).getTime();
|
||||
|
||||
const startedAt = task.startedAt
|
||||
? new Date(task.startedAt).getTime()
|
||||
: null;
|
||||
|
||||
// For running tasks, always use current time as finishedAt
|
||||
const now = new Date().getTime();
|
||||
@@ -545,6 +556,7 @@ export function Waterfall({
|
||||
|
||||
return {
|
||||
id: task.metadata.id,
|
||||
taskExternalId: task.taskExternalId,
|
||||
taskDisplayName: task.taskDisplayName,
|
||||
parentId: task.parentTaskExternalId,
|
||||
hasChildren: taskHasChildrenMap.get(task.metadata.id) || false,
|
||||
@@ -554,8 +566,13 @@ export function Waterfall({
|
||||
// Chart data
|
||||
offset: (queuedAt - globalMinTime) / 1000, // in seconds
|
||||
// If queuedAt equals startedAt (due to our fallback logic), then queuedDuration will be 0
|
||||
queuedDuration: task.queuedAt ? (startedAt - queuedAt) / 1000 : 0, // in seconds
|
||||
ranDuration: (finishedAt - startedAt) / 1000, // in seconds
|
||||
queuedDuration: startedAt
|
||||
? task.queuedAt
|
||||
? (startedAt - queuedAt) / 1000
|
||||
: 0
|
||||
: null, // in seconds
|
||||
ranDuration:
|
||||
startedAt && finishedAt ? (finishedAt - startedAt) / 1000 : null, // in seconds
|
||||
status: task.status,
|
||||
taskId: task.taskId, // Add taskId for tie-breaking in sorting
|
||||
attempt: task.attempt || 1,
|
||||
@@ -583,159 +600,52 @@ export function Waterfall({
|
||||
return { data, taskPathMap: new Map() };
|
||||
}, [taskData, expandedTasks, autoExpandedInitially, taskRelationships]); // Only recompute when dependencies change
|
||||
|
||||
// Custom tick renderer with expand/collapse buttons
|
||||
const renderTick = (props: {
|
||||
x: number;
|
||||
y: number;
|
||||
payload: { value: string };
|
||||
}) => {
|
||||
const { x, y, payload } = props;
|
||||
const task = processedData.data.find(
|
||||
(t) => t.taskDisplayName === payload.value,
|
||||
);
|
||||
if (!task) {
|
||||
// Return empty element instead of null
|
||||
return <g transform={`translate(${x},${y})`}></g>;
|
||||
// Handler for bar click events
|
||||
const handleBarClick = (data: any) => {
|
||||
if (data && data.id) {
|
||||
// Handle task selection for sidebar
|
||||
if (handleTaskSelect) {
|
||||
handleTaskSelect(data.id, data.workflowRunId);
|
||||
}
|
||||
|
||||
// Handle expansion if the task has children
|
||||
if (data.hasChildren) {
|
||||
openTask(data.id, data.depth);
|
||||
}
|
||||
}
|
||||
|
||||
const indentation = task.depth * 12; // 12px indentation per level
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<foreignObject
|
||||
x={-160} // Start position (right aligned)
|
||||
y={-10} // Vertically center
|
||||
width={160}
|
||||
height={20}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: `${indentation}px`,
|
||||
height: '100%',
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
{/* Expand/collapse button */}
|
||||
<div
|
||||
style={{
|
||||
cursor: task.hasChildren ? 'pointer' : 'default',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
onClick={() =>
|
||||
task.hasChildren &&
|
||||
toggleTask(task.id, task.hasChildren, task.depth)
|
||||
}
|
||||
>
|
||||
{task.hasChildren &&
|
||||
(task.isExpanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Task label */}
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontSize: '12px',
|
||||
textAlign: 'left',
|
||||
flexGrow: 1,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
className=" flex items-center gap-2"
|
||||
onClick={() => handleBarClick(task)}
|
||||
>
|
||||
<RunId
|
||||
displayName={task.taskDisplayName}
|
||||
id={task.id}
|
||||
onClick={() => handleBarClick(task)}
|
||||
className={task.id === selectedTaskId ? 'underline' : ''}
|
||||
attempt={task.attempt}
|
||||
/>
|
||||
</div>
|
||||
{workflowRunId === task.workflowRunId ? (
|
||||
task.parentId ? (
|
||||
<Link
|
||||
to={ROUTES.runs.detailWithSheet(tenantId, task.parentId, {
|
||||
type: 'task-detail',
|
||||
props: {
|
||||
selectedWorkflowRunId: task.workflowRunId,
|
||||
selectedTaskId: task.id,
|
||||
},
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
tooltip="Scope out to parent task"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
<BsArrowUpLeftCircle className="w-4 h-4 transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
tooltip="No parent task, this is a root task"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
<BsCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<Link
|
||||
to={ROUTES.runs.detailWithSheet(
|
||||
tenantId,
|
||||
task.workflowRunId || task.id,
|
||||
{
|
||||
type: 'task-detail',
|
||||
props: {
|
||||
selectedWorkflowRunId: task.workflowRunId || task.id,
|
||||
selectedTaskId: task.id,
|
||||
},
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
tooltip="Scope into child task"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
<BsArrowDownRightCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTick = useCallback(
|
||||
(props: { x: number; y: number; payload: { value: string } }) => {
|
||||
const { x, y, payload } = props;
|
||||
|
||||
return (
|
||||
<Tick
|
||||
x={x}
|
||||
y={y}
|
||||
payload={payload}
|
||||
workflowRunId={workflowRunId}
|
||||
selectedTaskId={selectedTaskId}
|
||||
handleBarClick={handleBarClick}
|
||||
toggleTask={toggleTask}
|
||||
processedData={processedData}
|
||||
tenantId={tenantId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[workflowRunId, selectedTaskId, handleBarClick, toggleTask, processedData],
|
||||
);
|
||||
|
||||
// Handle loading or error states
|
||||
if (
|
||||
isLoading ||
|
||||
isError ||
|
||||
!processedData.data ||
|
||||
processedData.data.length === 0
|
||||
!isLoading &&
|
||||
(isError || !processedData.data || processedData.data.length === 0)
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="h-[100px] w-full" />
|
||||
</>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-[100px] w-full" />;
|
||||
}
|
||||
|
||||
// Compute dynamic chart height
|
||||
@@ -756,21 +666,6 @@ export function Waterfall({
|
||||
},
|
||||
};
|
||||
|
||||
// Handler for bar click events
|
||||
const handleBarClick = (data: any) => {
|
||||
if (data && data.id) {
|
||||
// Handle task selection for sidebar
|
||||
if (handleTaskSelect) {
|
||||
handleTaskSelect(data.id, data.workflowRunId);
|
||||
}
|
||||
|
||||
// Handle expansion if the task has children
|
||||
if (data.hasChildren) {
|
||||
openTask(data.id, data.depth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
@@ -873,3 +768,139 @@ export function Waterfall({
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const Tick = ({
|
||||
x,
|
||||
y,
|
||||
payload,
|
||||
workflowRunId,
|
||||
selectedTaskId,
|
||||
handleBarClick,
|
||||
toggleTask,
|
||||
processedData,
|
||||
tenantId,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
payload: { value: string };
|
||||
workflowRunId: string;
|
||||
selectedTaskId?: string;
|
||||
handleBarClick: (task: ProcessedTaskData) => void;
|
||||
toggleTask: (taskId: string, hasChildren: boolean, taskDepth: number) => void;
|
||||
processedData: ProcessedData;
|
||||
tenantId: string;
|
||||
}) => {
|
||||
const task = processedData.data.find(
|
||||
(t) => t.taskDisplayName === payload.value,
|
||||
);
|
||||
if (!task) {
|
||||
// Return empty element instead of null
|
||||
return <g transform={`translate(${x},${y})`}></g>;
|
||||
}
|
||||
|
||||
const indentation = task.depth * 12; // 12px indentation per level
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<foreignObject
|
||||
x={-180} // Start position (right aligned)
|
||||
y={-10} // Vertically center
|
||||
width={180}
|
||||
height={20}
|
||||
>
|
||||
<div
|
||||
className={`group flex flex-row items-center pl-[${indentation}px] size-full`}
|
||||
>
|
||||
{/* Expand/collapse button */}
|
||||
<div
|
||||
data-haschildren={task.hasChildren}
|
||||
className="data-[haschildren=true]:cursor-info flex flex-row items-center justify-between size-[20px] h-[20px] mr-[4px]"
|
||||
onClick={() =>
|
||||
task.hasChildren &&
|
||||
toggleTask(task.id, task.hasChildren, task.depth)
|
||||
}
|
||||
>
|
||||
{task.hasChildren &&
|
||||
(task.isExpanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Task label */}
|
||||
<div
|
||||
className="cursor-pointer flex flex-row justify-between w-full grow text-left text-xs overflow-auto text-ellipsis whitespace-nowrap gap-2 items-center"
|
||||
onClick={() => handleBarClick(task)}
|
||||
>
|
||||
<RunId
|
||||
displayName={task.taskDisplayName}
|
||||
id={task.id}
|
||||
onClick={() => handleBarClick(task)}
|
||||
className={task.id === selectedTaskId ? 'underline' : ''}
|
||||
attempt={task.attempt}
|
||||
/>
|
||||
</div>
|
||||
{workflowRunId === task.workflowRunId &&
|
||||
task.taskExternalId === workflowRunId &&
|
||||
task.parentId && (
|
||||
<Link
|
||||
to={ROUTES.runs.detailWithSheet(tenantId, task.parentId, {
|
||||
type: 'task-detail',
|
||||
props: {
|
||||
selectedWorkflowRunId: task.workflowRunId,
|
||||
selectedTaskId: task.id,
|
||||
},
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
tooltip="Zoom out to parent task"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
<BsArrowUpLeftCircle className="w-4 h-4 transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{task.hasChildren && (
|
||||
<Link
|
||||
to={ROUTES.runs.detailWithSheet(
|
||||
tenantId,
|
||||
task.workflowRunId || task.id,
|
||||
{
|
||||
type: 'task-detail',
|
||||
props: {
|
||||
selectedWorkflowRunId: task.workflowRunId || task.id,
|
||||
selectedTaskId: task.id,
|
||||
},
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
tooltip="Drill into child task"
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="group-hover:opacity-100 opacity-0 transition-opacity duration-200"
|
||||
>
|
||||
{' '}
|
||||
<Drill className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{task.queuedDuration === null && (
|
||||
<TooltipProvider>
|
||||
<BaseTooltip>
|
||||
<TooltipTrigger>
|
||||
<Loader className="animate-[spin_3s_linear_infinite] h-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This task has not started</TooltipContent>
|
||||
</BaseTooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ function RunDetailSheetContent() {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex flex-col relative">
|
||||
<div className="sticky top-0 z-10 bg-yellow-500 bg-slate-100 dark:bg-slate-900 px-4 pb-2">
|
||||
<div className="sticky top-0 z-10 bg-slate-100 dark:bg-slate-900 px-4 pb-2">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
@@ -119,7 +119,7 @@ function RunDetailSheetContent() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-scroll">
|
||||
<div className="flex-1 overflow-y-clip">
|
||||
<div className="bg-slate-100 dark:bg-slate-900">
|
||||
<WorkflowRunVisualizer
|
||||
workflowRunId={data?.run?.metadata.id || ''}
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
} from '@/next/components/ui/page-header';
|
||||
import { Headline } from '@/next/components/ui/page-header';
|
||||
import { Duration } from '@/next/components/ui/duration';
|
||||
import { V1TaskStatus } from '@/lib/api/generated/data-contracts';
|
||||
import {
|
||||
V1TaskStatus,
|
||||
V1WorkflowRun,
|
||||
} from '@/lib/api/generated/data-contracts';
|
||||
import { TriggerRunModal } from '@/next/components/runs/trigger-run-modal';
|
||||
import {
|
||||
Tabs,
|
||||
@@ -62,8 +65,8 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
|
||||
|
||||
const [showTriggerModal, setShowTriggerModal] = useState(false);
|
||||
|
||||
const workflow = useMemo(() => data?.run, [data]);
|
||||
const tasks = useMemo(() => data?.tasks, [data]);
|
||||
const workflow = data?.run;
|
||||
const tasks = data?.tasks;
|
||||
|
||||
const { open: openSheet, sheet } = useSideSheet();
|
||||
|
||||
@@ -203,38 +206,11 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Timing = () => {
|
||||
const timings: JSX.Element[] = [
|
||||
<span key="created" className="flex items-center gap-2">
|
||||
<span>Created</span>
|
||||
<RelativeDate date={workflow.createdAt} />
|
||||
</span>,
|
||||
<span key="started" className="flex items-center gap-2">
|
||||
<span>Started</span>
|
||||
<RelativeDate date={workflow.startedAt} />
|
||||
</span>,
|
||||
<span key="duration" className="flex items-center gap-2">
|
||||
<span>Duration</span>
|
||||
<span className="whitespace-nowrap">
|
||||
<Duration
|
||||
start={workflow.startedAt}
|
||||
end={workflow.finishedAt}
|
||||
status={workflow.status}
|
||||
/>
|
||||
</span>
|
||||
</span>,
|
||||
];
|
||||
return (
|
||||
<span className="flex flex-col items-start gap-y-2 text-sm text-muted-foreground">
|
||||
{timings}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Headline>
|
||||
<PageTitle description={<Timing />}>
|
||||
<PageTitle description={<Timing workflow={workflow} />}>
|
||||
<div className="text-2xl font-bold truncate flex items-center gap-2">
|
||||
<RunsBadge status={workflow.status} variant="xs" />
|
||||
<RunId wfRun={workflow} />
|
||||
@@ -368,3 +344,31 @@ function RunDetailPageContent({ workflowRunId }: RunDetailPageProps) {
|
||||
</BasicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const Timing = ({ workflow }: { workflow: V1WorkflowRun }) => {
|
||||
const timings: JSX.Element[] = [
|
||||
<span key="created" className="flex items-center gap-2">
|
||||
<span>Created</span>
|
||||
<RelativeDate date={workflow.createdAt} />
|
||||
</span>,
|
||||
<span key="started" className="flex items-center gap-2">
|
||||
<span>Started</span>
|
||||
<RelativeDate date={workflow.startedAt} />
|
||||
</span>,
|
||||
<span key="duration" className="flex items-center gap-2">
|
||||
<span>Duration</span>
|
||||
<span className="whitespace-nowrap">
|
||||
<Duration
|
||||
start={workflow.startedAt}
|
||||
end={workflow.finishedAt}
|
||||
status={workflow.status}
|
||||
/>
|
||||
</span>
|
||||
</span>,
|
||||
];
|
||||
return (
|
||||
<span className="flex flex-col items-start gap-y-2 text-sm text-muted-foreground">
|
||||
{timings}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1392,6 +1392,8 @@ func (r *OLAPRepositoryImpl) GetTaskTimings(ctx context.Context, tenantId string
|
||||
|
||||
// start out by getting a list of task external ids for the workflow run id
|
||||
rootTaskExternalIds := make([]pgtype.UUID, 0)
|
||||
sevenDaysAgo := time.Now().Add(-time.Hour * 24 * 7)
|
||||
minInsertedAt := time.Now()
|
||||
|
||||
rootTasks, err := r.queries.FlattenTasksByExternalIds(ctx, r.readPool, sqlcv1.FlattenTasksByExternalIdsParams{
|
||||
Externalids: []pgtype.UUID{workflowRunId},
|
||||
@@ -1404,12 +1406,24 @@ func (r *OLAPRepositoryImpl) GetTaskTimings(ctx context.Context, tenantId string
|
||||
|
||||
for _, task := range rootTasks {
|
||||
rootTaskExternalIds = append(rootTaskExternalIds, task.ExternalID)
|
||||
|
||||
if task.InsertedAt.Time.Before(minInsertedAt) {
|
||||
minInsertedAt = task.InsertedAt.Time
|
||||
}
|
||||
}
|
||||
|
||||
// Setting the maximum lookback period to 7 days
|
||||
// to prevent scanning a zillion partitions on the tasks,
|
||||
// runs, and dags tables.
|
||||
if minInsertedAt.Before(sevenDaysAgo) {
|
||||
minInsertedAt = sevenDaysAgo
|
||||
}
|
||||
|
||||
runsList, err := r.queries.GetRunsListRecursive(ctx, r.readPool, sqlcv1.GetRunsListRecursiveParams{
|
||||
Taskexternalids: rootTaskExternalIds,
|
||||
Tenantid: sqlchelpers.UUIDFromStr(tenantId),
|
||||
Depth: depth,
|
||||
Createdafter: sqlchelpers.TimestamptzFromTime(minInsertedAt),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1321,6 +1321,8 @@ WITH RECURSIVE all_runs AS (
|
||||
WHERE
|
||||
r.tenant_id = @tenantId::uuid
|
||||
AND ar.depth < @depth::int
|
||||
AND r.inserted_at >= @createdAfter::timestamptz
|
||||
AND t.inserted_at >= @createdAfter::timestamptz
|
||||
)
|
||||
SELECT
|
||||
tenant_id,
|
||||
|
||||
@@ -317,6 +317,8 @@ WITH RECURSIVE all_runs AS (
|
||||
WHERE
|
||||
r.tenant_id = $1::uuid
|
||||
AND ar.depth < $3::int
|
||||
AND r.inserted_at >= $4::timestamptz
|
||||
AND t.inserted_at >= $4::timestamptz
|
||||
)
|
||||
SELECT
|
||||
tenant_id,
|
||||
@@ -332,9 +334,10 @@ WHERE
|
||||
`
|
||||
|
||||
type GetRunsListRecursiveParams struct {
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
Taskexternalids []pgtype.UUID `json:"taskexternalids"`
|
||||
Depth int32 `json:"depth"`
|
||||
Tenantid pgtype.UUID `json:"tenantid"`
|
||||
Taskexternalids []pgtype.UUID `json:"taskexternalids"`
|
||||
Depth int32 `json:"depth"`
|
||||
Createdafter pgtype.Timestamptz `json:"createdafter"`
|
||||
}
|
||||
|
||||
type GetRunsListRecursiveRow struct {
|
||||
@@ -347,7 +350,12 @@ type GetRunsListRecursiveRow struct {
|
||||
}
|
||||
|
||||
func (q *Queries) GetRunsListRecursive(ctx context.Context, db DBTX, arg GetRunsListRecursiveParams) ([]*GetRunsListRecursiveRow, error) {
|
||||
rows, err := db.Query(ctx, getRunsListRecursive, arg.Tenantid, arg.Taskexternalids, arg.Depth)
|
||||
rows, err := db.Query(ctx, getRunsListRecursive,
|
||||
arg.Tenantid,
|
||||
arg.Taskexternalids,
|
||||
arg.Depth,
|
||||
arg.Createdafter,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user