Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InlineStack } from "@/components/ui/layout";
import { Skeleton } from "@/components/ui/skeleton";
import { Paragraph } from "@/components/ui/typography";
import type { PipelineRun } from "@/types/pipelineRun";
import { formatDate, convertUTCToLocalTime } from "@/utils/date";
import { convertUTCToLocalTime, formatDate } from "@/utils/date";

import { PipelineRunStatus } from "./components/PipelineRunStatus";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CheckCircleIcon,
CircleDashedIcon,
CircleMinusIcon,
ClockIcon,
Loader2Icon,
XCircleIcon,
Expand All @@ -10,18 +11,19 @@ import type { ContainerExecutionStatus } from "@/api/types.gen";
import { Icon } from "@/components/ui/icon";
import { QuickTooltip } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { RunStatus } from "@/types/pipelineRun";
import { getExecutionStatusLabel } from "@/utils/executionStatus";

type StatusIndicatorProps = {
status: ContainerExecutionStatus | RunStatus;
status: ContainerExecutionStatus;
disabledCache?: boolean;
};

export const StatusIndicator = ({
status,
disabledCache = false,
}: StatusIndicatorProps) => {
const { style, text, icon } = getStatusMetadata(status);
const { style, icon } = getStatusMetadata(status);
const text = getExecutionStatusLabel(status);

return (
<div className="absolute -z-1 -top-5 left-0 flex items-start">
Expand All @@ -46,70 +48,55 @@ export const StatusIndicator = ({
);
};

const getStatusMetadata = (status: ContainerExecutionStatus | RunStatus) => {
const getStatusMetadata = (status: ContainerExecutionStatus) => {
switch (status) {
case "SUCCEEDED":
return {
style: "bg-emerald-500",
text: "Succeeded",
icon: <CheckCircleIcon className="w-2 h-2" />,
};
case "FAILED":
case "SYSTEM_ERROR":
case "INVALID":
return {
style: "bg-red-700",
text: "Failed",
icon: <XCircleIcon className="w-2 h-2" />,
};
case "RUNNING":
return {
style: "bg-sky-500",
text: "Running",
icon: <Loader2Icon className="w-2 h-2 animate-spin" />,
};
case "PENDING":
case "UNINITIALIZED":
return {
style: "bg-yellow-500",
text: "Pending",
icon: <ClockIcon className="w-2 h-2 animate-spin duration-2000" />,
};
case "CANCELLING":
case "CANCELLED":
return {
style: "bg-gray-800",
text: status === "CANCELLING" ? "Cancelling" : "Cancelled",
icon: <XCircleIcon className="w-2 h-2" />,
};
case "SKIPPED":
return {
style: "bg-slate-400",
text: "Skipped",
icon: <XCircleIcon className="w-2 h-2" />,
icon: <CircleMinusIcon className="w-2 h-2" />,
};
case "QUEUED":
return {
style: "bg-yellow-500",
text: "Queued",
icon: <ClockIcon className="w-2 h-2 animate-spin duration-2000" />,
};
case "WAITING_FOR_UPSTREAM":
return {
style: "bg-slate-500",
text: "Waiting for upstream",
icon: <ClockIcon className="w-2 h-2 animate-spin duration-2000" />,
};
case "WAITING":
case "UNINITIALIZED":
return {
style: "bg-yellow-500",
text: "Pending",
icon: <ClockIcon className="w-2 h-2 animate-spin duration-2000" />,
};
default:
return {
style: "bg-slate-300",
text: "Unknown",
icon: <CircleDashedIcon className="w-2 h-2" />,
};
}
Expand Down
26 changes: 16 additions & 10 deletions src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNode.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { type NodeProps } from "@xyflow/react";
import { memo, useMemo } from "react";

import type { ContainerExecutionStatus } from "@/api/types.gen";
import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider";
import { TaskNodeProvider } from "@/providers/TaskNodeProvider";
import { getRunStatus } from "@/services/executionService";
import type { TaskNodeData } from "@/types/taskNode";
import { isCacheDisabled } from "@/utils/cache";
import { deriveExecutionStatusFromStats } from "@/utils/executionStatus";

import { StatusIndicator } from "./StatusIndicator";
import { TaskNodeCard } from "./TaskNodeCard";
Expand All @@ -15,16 +16,21 @@ const TaskNode = ({ data, selected }: NodeProps) => {

const typedData = useMemo(() => data as TaskNodeData, [data]);

const status = useMemo(() => {
const status: ContainerExecutionStatus | undefined = useMemo(() => {
const taskId = typedData.taskId ?? "";
const statusCounts = executionData?.taskStatusCountsMap.get(taskId);

if (!statusCounts) {
return undefined;
}

return getRunStatus(statusCounts);
}, [executionData?.taskStatusCountsMap, typedData.taskId]);
const executionId =
executionData?.details?.child_task_execution_ids?.[taskId];
if (!executionId) return undefined;

const statusStats =
executionData?.state?.child_execution_status_stats?.[executionId];

return deriveExecutionStatusFromStats(statusStats);
}, [
executionData?.details?.child_task_execution_ids,
executionData?.state?.child_execution_status_stats,
typedData.taskId,
]);

const disabledCache = isCacheDisabled(typedData.taskSpec);

Expand Down
36 changes: 35 additions & 1 deletion src/components/shared/Status/StatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
getExecutionStatusLabel,
isContainerExecutionStatus,
} from "@/utils/executionStatus";

const StatusIcon = ({
status,
Expand All @@ -23,9 +27,11 @@ const StatusIcon = ({
tooltip?: boolean;
label?: "run" | "task" | "pipeline";
}) => {
const statusLabel = getStatusLabel(status);

if (tooltip) {
const capitalizedLabel = label.charAt(0).toUpperCase() + label.slice(1);
const tooltipText = `${capitalizedLabel} ${status?.toLowerCase() ?? "unknown"}`;
const tooltipText = `${capitalizedLabel} ${statusLabel.toLowerCase()}`;
return (
<Tooltip>
<TooltipTrigger asChild>
Expand All @@ -43,6 +49,34 @@ const StatusIcon = ({
return <Icon status={status} />;
};

const getStatusLabel = (status: string | undefined) => {
if (!status) return "Unknown";

if (isContainerExecutionStatus(status)) {
return getExecutionStatusLabel(status);
}

// Aggregate / run-level statuses (derived via getRunStatus)
switch (status) {
case "SUCCEEDED":
return "Succeeded";
case "FAILED":
return "Failed";
case "RUNNING":
return "Running";
case "WAITING":
return "Waiting";
case "CANCELLED":
return "Cancelled";
case "SKIPPED":
return "Skipped";
case "UNKNOWN":
return "Unknown";
default:
return status;
}
};

const Icon = ({ status }: { status?: string }) => {
switch (status) {
case "SUCCEEDED":
Expand Down
10 changes: 8 additions & 2 deletions src/components/shared/TaskDetails/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { useCopyToClipboard } from "@/hooks/useCopyToClip";
import useToastNotification from "@/hooks/useToastNotification";
import { cn } from "@/lib/utils";
import type { ComponentSpec } from "@/utils/componentSpec";
import {
getExecutionStatusLabel,
isContainerExecutionStatus,
} from "@/utils/executionStatus";
import {
convertGithubUrlToDirectoryUrl,
downloadYamlFromComponentText,
Expand Down Expand Up @@ -157,10 +161,12 @@ const TaskDetails = ({
{status && (
<div className="flex flex-col px-3 py-2">
<div className="shrink-0 font-medium text-sm text-gray-700 mb-1">
Run Status
Execution Status
</div>
<div className="text-xs text-gray-600 wrap-break-word whitespace-pre-wrap">
{status}
{isContainerExecutionStatus(status)
? getExecutionStatusLabel(status)
: status}
</div>
</div>
)}
Expand Down
5 changes: 2 additions & 3 deletions src/providers/TaskNodeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import useComponentFromUrl from "@/hooks/useComponentFromUrl";
import { useTaskNodeDimensions } from "@/hooks/useTaskNodeDimensions";
import useToastNotification from "@/hooks/useToastNotification";
import type { Annotations } from "@/types/annotations";
import type { RunStatus } from "@/types/pipelineRun";
import {
DEFAULT_TASK_NODE_CALLBACKS,
type TaskNodeData,
Expand Down Expand Up @@ -37,7 +36,7 @@ type TaskNodeState = Readonly<{
readOnly: boolean;
disabled: boolean;
connectable: boolean;
status?: ContainerExecutionStatus | RunStatus;
status?: ContainerExecutionStatus;
isCustomComponent: boolean;
dimensions: TaskNodeDimensions;
}>;
Expand All @@ -55,7 +54,7 @@ type TaskNodeProviderProps = {
children: ReactNode;
data: TaskNodeData;
selected: boolean;
status?: ContainerExecutionStatus | RunStatus;
status?: ContainerExecutionStatus;
};

export type TaskNodeContextType = {
Expand Down
48 changes: 48 additions & 0 deletions src/utils/executionStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";

import {
deriveExecutionStatusFromStats,
getExecutionStatusLabel,
} from "@/utils/executionStatus";

describe("getExecutionStatusLabel", () => {
it("maps WAITING_FOR_UPSTREAM to the real name", () => {
expect(getExecutionStatusLabel("WAITING_FOR_UPSTREAM")).toBe(
"Waiting for upstream",
);
});

it("maps QUEUED to the real name", () => {
expect(getExecutionStatusLabel("QUEUED")).toBe("Queued");
});

it("maps SYSTEM_ERROR to Failed (per current decision)", () => {
expect(getExecutionStatusLabel("SYSTEM_ERROR")).toBe("Failed");
});
});

describe("deriveExecutionStatusFromStats", () => {
it("returns undefined when no stats are present", () => {
expect(deriveExecutionStatusFromStats(undefined)).toBeUndefined();
});

it("returns RUNNING if any child is running", () => {
expect(deriveExecutionStatusFromStats({ RUNNING: 1, SUCCEEDED: 3 })).toBe(
"RUNNING",
);
});

it("returns WAITING_FOR_UPSTREAM when present and nothing higher priority exists", () => {
expect(deriveExecutionStatusFromStats({ WAITING_FOR_UPSTREAM: 2 })).toBe(
"WAITING_FOR_UPSTREAM",
);
});

it("returns FAILED when SYSTEM_ERROR is present", () => {
expect(deriveExecutionStatusFromStats({ SYSTEM_ERROR: 1 })).toBe("FAILED");
});

it("returns SUCCEEDED when all known nodes are succeeded", () => {
expect(deriveExecutionStatusFromStats({ SUCCEEDED: 4 })).toBe("SUCCEEDED");
});
});
Loading