Tasks marked as skipped in the UI (#2978)

* indicate skipped task runs correctly in the UI

* do not use useCurrentTenantId

* naming

* new hook for task skipped
This commit is contained in:
Mohammed Nafees
2026-02-09 22:07:38 +01:00
committed by GitHub
parent 8755cd5e8e
commit e448777339
6 changed files with 99 additions and 18 deletions
@@ -1,8 +1,13 @@
import { useIsTaskRunSkipped } from '../../hooks/use-is-task-run-skipped';
import { useWorkflowDetails } from '../../hooks/use-workflow-details';
import { TabOption } from './step-run-detail/step-run-detail';
import StepRunNode from './step-run-node';
import { useRefetchInterval } from '@/contexts/refetch-interval-context';
import { queries, WorkflowRunShapeItemForWorkflowRunDetails } from '@/lib/api';
import {
queries,
V1TaskEventType,
WorkflowRunShapeItemForWorkflowRunDetails,
} from '@/lib/api';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
@@ -17,7 +22,23 @@ type NodeRelationship = {
};
export const JobMiniMap = ({ onClick }: JobMiniMapProps) => {
const { shape, taskRuns: tasks, isLoading, isError } = useWorkflowDetails();
const {
shape,
taskRuns: tasks,
taskEvents,
isLoading,
isError,
} = useWorkflowDetails();
const skippedTaskIds = useMemo(() => {
const ids = new Set<string>();
for (const event of taskEvents) {
if (event.eventType === V1TaskEventType.SKIPPED) {
ids.add(event.taskId);
}
}
return ids;
}, [taskEvents]);
const taskRunRelationships: NodeRelationship[] = useMemo(() => {
if (!shape || !tasks) {
@@ -127,6 +148,9 @@ export const JobMiniMap = ({ onClick }: JobMiniMapProps) => {
onClick: () => onClick(taskRun?.metadata.id),
childWorkflowsCount: taskRun?.numSpawnedChildren || 0,
taskName: shapeItem.taskName,
isSkipped: taskRun
? skippedTaskIds.has(taskRun.metadata.id)
: false,
}}
/>
);
@@ -160,6 +184,7 @@ export const TaskRunMiniMap = ({
taskRunId,
}: JobMiniMapProps & UseTaskRunProps) => {
const { taskRun, isLoading, isError } = useTaskRun({ taskRunId });
const { isSkipped } = useIsTaskRunSkipped({ taskRunId, limit: 1 });
if (isLoading || isError || !taskRun) {
return null;
@@ -175,6 +200,7 @@ export const TaskRunMiniMap = ({
onClick: () => onClick(taskRun.metadata.id),
childWorkflowsCount: taskRun.numSpawnedChildren,
taskName: taskRun.displayName,
isSkipped,
}}
/>
</div>
@@ -1,6 +1,7 @@
import { V1RunIndicator } from '../../../components/run-statuses';
import { RunsTable } from '../../../components/runs-table';
import { RunsProvider } from '../../../hooks/runs-provider';
import { useIsTaskRunSkipped } from '../../../hooks/use-is-task-run-skipped';
import { isTerminalState } from '../../../hooks/use-workflow-details';
import { TaskRunMiniMap } from '../mini-map';
import { StepRunEvents } from '../step-run-events-for-workflow-run';
@@ -20,14 +21,13 @@ import {
TabsTrigger,
} from '@/components/v1/ui/tabs';
import { useSidePanel } from '@/hooks/use-side-panel';
import { useCurrentTenantId } from '@/hooks/use-tenant';
import { V1TaskStatus, V1TaskSummary, queries } from '@/lib/api';
import { emptyGolangUUID, formatDuration } from '@/lib/utils';
import { TaskRunActionButton } from '@/pages/main/v1/task-runs-v1/actions';
import { WorkflowDefinitionLink } from '@/pages/main/workflow-runs/$run/v2components/workflow-definition';
import { appRoutes } from '@/router';
import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { Link, useParams } from '@tanstack/react-router';
import { FullscreenIcon } from 'lucide-react';
import { useCallback, useState } from 'react';
@@ -59,13 +59,13 @@ const TaskRunPermalinkOrBacklink = ({
taskRun: V1TaskSummary;
showViewTaskRunButton: boolean;
}) => {
const { tenantId } = useCurrentTenantId();
const { tenant } = useParams({ from: appRoutes.tenantRoute.to });
if (showViewTaskRunButton) {
return (
<Link
to={appRoutes.tenantRunRoute.to}
params={{ tenant: tenantId, run: taskRun.metadata.id }}
params={{ tenant: tenant, run: taskRun.metadata.id }}
>
<Button
size={'sm'}
@@ -84,7 +84,7 @@ const TaskRunPermalinkOrBacklink = ({
return (
<Link
to={appRoutes.tenantRunRoute.to}
params={{ tenant: tenantId, run: taskRun.workflowRunExternalId }}
params={{ tenant: tenant, run: taskRun.workflowRunExternalId }}
>
<Button
size={'sm'}
@@ -133,6 +133,7 @@ export const TaskRunDetail = ({
},
});
const { isSkipped } = useIsTaskRunSkipped({ taskRunId });
const taskRun = taskRunQuery.data;
if (taskRunQuery.isLoading) {
@@ -152,7 +153,9 @@ export const TaskRunDetail = ({
<div className="flex flex-row items-center justify-between">
<div className="flex w-full flex-row items-center justify-between">
<div className="flex flex-row items-center gap-4">
{taskRun.status && <V1RunIndicator status={taskRun.status} />}
{taskRun.status && (
<V1RunIndicator status={taskRun.status} isSkipped={isSkipped} />
)}
<h3 className="flex flex-row items-center gap-4 font-mono text-lg font-semibold leading-tight tracking-tight text-foreground">
{taskRun.displayName || 'Task Run Detail'}
</h3>
@@ -162,7 +165,7 @@ export const TaskRunDetail = ({
{taskRun.parentTaskExternalId && (
<TriggeringParentWorkflowRunSection
tenantId={taskRun.tenantId}
tenant={taskRun.tenantId}
parentTaskExternalId={taskRun.parentTaskExternalId}
/>
)}
@@ -329,9 +332,9 @@ const V1StepRunSummary = ({ taskRunId }: { taskRunId: string }) => {
const taskRunQuery = useQuery({
...queries.v1Tasks.get(taskRunId),
});
const { isSkipped: hasSkippedEvent } = useIsTaskRunSkipped({ taskRunId });
const timings = [];
const data = taskRunQuery.data;
if (taskRunQuery.isLoading || !data) {
@@ -359,7 +362,7 @@ const V1StepRunSummary = ({ taskRunId }: { taskRunId: string }) => {
if (data.status === V1TaskStatus.COMPLETED && data.finishedAt) {
timings.push(
<div key="finished" className="text-sm text-muted-foreground">
{'Succeeded '}
{hasSkippedEvent ? 'Skipped ' : 'Succeeded '}
<RelativeDate date={data.finishedAt} />
</div>,
);
@@ -393,10 +396,10 @@ const V1StepRunSummary = ({ taskRunId }: { taskRunId: string }) => {
};
function TriggeringParentWorkflowRunSection({
tenantId,
tenant,
parentTaskExternalId,
}: {
tenantId: string;
tenant: string;
parentTaskExternalId: string;
}) {
// Get the parent task to find the parent workflow run
@@ -428,7 +431,7 @@ function TriggeringParentWorkflowRunSection({
<Link
to={appRoutes.tenantRunRoute.to}
params={{
tenant: tenantId,
tenant: tenant,
run: parentWorkflowRun.workflowRunExternalId,
}}
className="font-semibold text-indigo-500 hover:underline dark:text-indigo-200"
@@ -17,6 +17,7 @@ export type NodeData = {
onClick: (defaultOpenTab?: TabOption) => void;
childWorkflowsCount: number;
taskName: string;
isSkipped?: boolean;
};
// eslint-disable-next-line react/display-name
@@ -61,7 +62,10 @@ export default memo(({ data }: { data: NodeData }) => {
<span className="step-run-backdrop absolute inset-[1px] bg-background transition-colors duration-200" />
<div className="z-10 flex w-full flex-row items-center justify-between gap-4">
<div className="z-10 flex flex-row items-center justify-start gap-2">
<V1RunIndicator status={data.taskRun?.status} />
<V1RunIndicator
status={data.taskRun?.status}
isSkipped={data.isSkipped}
/>
<div className="max-w-[160px] flex-grow truncate">
{data.taskName}
</div>
@@ -1,7 +1,7 @@
import { useWorkflowDetails } from '../../hooks/use-workflow-details';
import stepRunNode, { NodeData } from './step-run-node';
import { useTheme } from '@/components/hooks/use-theme';
import { V1TaskStatus } from '@/lib/api';
import { V1TaskEventType, V1TaskStatus } from '@/lib/api';
import dagre from 'dagre';
import { useMemo } from 'react';
import ReactFlow, {
@@ -30,7 +30,18 @@ const WorkflowRunVisualizer = ({
setSelectedTaskRunId: (id: string) => void;
}) => {
const { theme } = useTheme();
const { shape, taskRuns, isLoading, isError } = useWorkflowDetails();
const { shape, taskRuns, taskEvents, isLoading, isError } =
useWorkflowDetails();
const skippedTaskIds = useMemo(() => {
const ids = new Set<string>();
for (const event of taskEvents) {
if (event.eventType === V1TaskEventType.SKIPPED) {
ids.add(event.taskId);
}
}
return ids;
}, [taskEvents]);
const edges: Edge[] = useMemo(
() =>
@@ -85,6 +96,7 @@ const WorkflowRunVisualizer = ({
onClick: () => task && setSelectedTaskRunId(task.metadata.id),
childWorkflowsCount: task?.numSpawnedChildren || 0,
taskName: shapeItem.taskName,
isSkipped: task ? skippedTaskIds.has(task.metadata.id) : false,
};
return {
@@ -95,7 +107,7 @@ const WorkflowRunVisualizer = ({
selectable: true,
};
}) || [],
[shape, taskRuns, setSelectedTaskRunId],
[shape, taskRuns, setSelectedTaskRunId, skippedTaskIds],
);
const nodeWidth = 230;
@@ -1,5 +1,6 @@
import { V1TaskStatus } from '@/lib/api';
import { cn } from '@/lib/utils';
import { CircleMinus } from 'lucide-react';
function createV2IndicatorVariant(eventType: V1TaskStatus | undefined) {
switch (eventType) {
@@ -20,9 +21,15 @@ function createV2IndicatorVariant(eventType: V1TaskStatus | undefined) {
export function V1RunIndicator({
status,
isSkipped,
}: {
status: V1TaskStatus | undefined;
isSkipped?: boolean;
}) {
if (isSkipped) {
return <CircleMinus className="h-3 w-3 text-muted-foreground" />;
}
const indicator = createV2IndicatorVariant(status);
return <div className={cn(indicator, 'h-[6px] w-[6px] rounded-full')} />;
@@ -0,0 +1,29 @@
import { queries, V1TaskEventType } from '@/lib/api';
import { appRoutes } from '@/router';
import { useQuery } from '@tanstack/react-query';
import { useParams } from '@tanstack/react-router';
type UseIsTaskRunSkippedProps = {
taskRunId: string;
limit?: number;
};
export const useIsTaskRunSkipped = ({
taskRunId,
limit = 50,
}: UseIsTaskRunSkippedProps) => {
const { tenant } = useParams({ from: appRoutes.tenantRoute.to });
const eventsQuery = useQuery({
...queries.v1TaskEvents.list(tenant, { limit, offset: 0 }, taskRunId),
});
const isSkipped =
eventsQuery.data?.rows?.some(
(event) => event.eventType === V1TaskEventType.SKIPPED,
) ?? false;
return {
isSkipped,
isLoading: eventsQuery.isLoading,
};
};