From 24ba7dd453b9a6f68f5b8bb8dae63ceb48dff643 Mon Sep 17 00:00:00 2001 From: YeonShin Date: Fri, 15 May 2026 17:17:51 +0900 Subject: [PATCH 1/5] fix: add missing queued state color to CalendarToolTip (May 15) --- .../src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx index f5a3a50ecf96e..90ae1d8ab3681 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx @@ -32,6 +32,7 @@ type Props = { const stateColorMap = { failed: "failed.solid", planned: "stone.solid", + queued: "queued.solid", running: "running.solid", success: "success.solid", }; From c20f8dcbcc2ee4924740e1bdb85550956b0210a6 Mon Sep 17 00:00:00 2001 From: YeonShin Date: Tue, 26 May 2026 21:36:20 +0900 Subject: [PATCH 2/5] fix: calendar cell coloring to prioritize severity in Total Runs view --- .../pages/Dag/Calendar/calendarUtils.test.ts | 34 +++++ .../src/pages/Dag/Calendar/calendarUtils.ts | 117 ++++++++++++++---- 2 files changed, 129 insertions(+), 22 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts index 4cb86deec295c..3a0229852661c 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts @@ -27,6 +27,7 @@ const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" }; const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" }; const DEFAULT_TOTAL_COLOR = { _dark: "green.700", _light: "green.400" }; const DEFAULT_FAILED_COLOR = { _dark: "red.700", _light: "red.400" }; +const DEFAULT_RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" }; const EMPTY_COUNTS: RunCounts = { failed: 0, @@ -176,6 +177,39 @@ describe("createCalendarScale", () => { }); }); + it("returns the failed color for a failed-only cell in total mode", () => { + const scale = createCalendarScale([run("failed", 1)], "total", "hourly"); + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1 })).toEqual(DEFAULT_FAILED_COLOR); + }); + + it("returns a mixed red and green color for failed and success runs in total mode", () => { + const scale = createCalendarScale([run("failed", 1), run("success", 1)], "total", "hourly"); + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, success: 1, total: 2 })).toEqual({ + actual: DEFAULT_TOTAL_COLOR, + planned: DEFAULT_FAILED_COLOR, + }); + }); + + it("returns a mixed cyan and green color for running and success runs in total mode", () => { + const scale = createCalendarScale([run("running", 1), run("success", 1)], "total", "hourly"); + + expect(scale.getColor({ ...EMPTY_COUNTS, running: 1, success: 1, total: 2 })).toEqual({ + actual: DEFAULT_TOTAL_COLOR, + planned: DEFAULT_RUNNING_COLOR, + }); + }); + + it("returns a mixed cyan and red color for running and failed runs in total mode", () => { + const scale = createCalendarScale([run("running", 1), run("failed", 1)], "total", "hourly"); + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, running: 1, total: 2 })).toEqual({ + actual: DEFAULT_RUNNING_COLOR, + planned: DEFAULT_FAILED_COLOR, + }); + }); + it("uses failed counts for failed mode", () => { const scale = createCalendarScale([run("success", 5), run("failed", 1)], { granularity: "hourly", diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts index 3f57eb35030c5..239882e5480dc 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts @@ -40,6 +40,7 @@ dayjs.extend(tz); // Calendar color constants export const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" }; const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" }; +const RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" }; const TOTAL_COLOR_INTENSITIES = [ EMPTY_COLOR, // 0 @@ -267,13 +268,29 @@ export const createCalendarScale = ( return { getColor: (counts: RunCounts) => { - const actualCount = getActualRunCount(counts, viewMode); + const failedCount = counts.failed; + const runningCount = viewMode === "total" ? counts.running : 0; + const successCount = viewMode === "total" ? counts.success : 0; + const hasPending = getPendingRunCount(counts) > 0; - const hasActual = actualCount > 0; + const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0; + + const failedColor = FAILURE_COLOR_INTENSITIES[2] ?? EMPTY_COLOR; + const successColor = TOTAL_COLOR_INTENSITIES[2] ?? EMPTY_COLOR; if (hasPending && hasActual) { + let actualColor = EMPTY_COLOR; + + if (failedCount > 0) { + actualColor = failedColor; + } else if (runningCount > 0) { + actualColor = RUNNING_COLOR; + } else if (successCount > 0) { + actualColor = successColor; + } + return { - actual: singleColor, + actual: actualColor, planned: PLANNED_COLOR, }; } @@ -282,7 +299,29 @@ export const createCalendarScale = ( return PLANNED_COLOR; } - return actualCount === 0 ? EMPTY_COLOR : singleColor; + if (hasActual) { + if (failedCount > 0 && runningCount > 0) { + return { actual: RUNNING_COLOR, planned: failedColor }; + } + if (failedCount > 0 && successCount > 0) { + return { actual: successColor, planned: failedColor }; + } + if (runningCount > 0 && successCount > 0) { + return { actual: successColor, planned: RUNNING_COLOR }; + } + + if (failedCount > 0) { + return failedColor; + } + if (runningCount > 0) { + return RUNNING_COLOR; + } + if (successCount > 0) { + return successColor; + } + } + + return EMPTY_COLOR; }, legendItems: [ { color: EMPTY_COLOR, label: "0" }, @@ -315,24 +354,39 @@ export const createCalendarScale = ( actual: string | { _dark: string; _light: string }; planned: string | { _dark: string; _light: string }; } => { - const actualCount = getActualRunCount(counts, viewMode); + const failedCount = counts.failed; + const runningCount = viewMode === "total" ? counts.running : 0; + const successCount = viewMode === "total" ? counts.success : 0; + const hasPending = getPendingRunCount(counts) > 0; - const hasActual = actualCount > 0; + const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0; - if (hasPending && hasActual) { - let actualColor = colorScheme[0] ?? EMPTY_COLOR; + type ColorValue = string | { _dark: string; _light: string }; + const getIntensityColor = (count: number, scheme: Array) => { + if (count === 0) { + return scheme[0] ?? EMPTY_COLOR; + } for (let index = uniqueThresholds.length - 1; index >= 1; index -= 1) { const threshold = uniqueThresholds[index]; - if (threshold !== undefined && actualCount >= threshold) { - actualColor = colorScheme[Math.min(index, colorScheme.length - 1)] ?? EMPTY_COLOR; - break; + if (threshold !== undefined && count >= threshold) { + return scheme[Math.min(index, scheme.length - 1)] ?? EMPTY_COLOR; } } - if (actualCount > 0 && actualColor === colorScheme[0]) { - actualColor = colorScheme[1] ?? EMPTY_COLOR; + return scheme[1] ?? EMPTY_COLOR; + }; + + if (hasPending && hasActual) { + let actualColor: ColorValue = EMPTY_COLOR; + + if (failedCount > 0) { + actualColor = getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES); + } else if (runningCount > 0) { + actualColor = RUNNING_COLOR; + } else if (successCount > 0) { + actualColor = getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES); } return { @@ -345,21 +399,40 @@ export const createCalendarScale = ( return PLANNED_COLOR; } - const targetCount = actualCount; + if (hasActual) { + if (failedCount > 0 && runningCount > 0) { + return { + actual: RUNNING_COLOR, + planned: getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES), + }; + } - if (targetCount === 0) { - return colorScheme[0] ?? EMPTY_COLOR; - } + if (failedCount > 0 && successCount > 0) { + return { + actual: getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES), + planned: getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES), + }; + } - for (let index = uniqueThresholds.length - 1; index >= 1; index -= 1) { - const threshold = uniqueThresholds[index]; + if (runningCount > 0 && successCount > 0) { + return { + actual: getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES), + planned: RUNNING_COLOR, + }; + } - if (threshold !== undefined && targetCount >= threshold) { - return colorScheme[Math.min(index, colorScheme.length - 1)] ?? EMPTY_COLOR; + if (failedCount > 0) { + return getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES); + } + if (runningCount > 0) { + return RUNNING_COLOR; + } + if (successCount > 0) { + return getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES); } } - return colorScheme[1] ?? EMPTY_COLOR; + return EMPTY_COLOR; }; const legendItems: Array = []; From 78d923c356ff26c5e9ef4c73fd5bfdfbb049c229 Mon Sep 17 00:00:00 2001 From: YeonShin Date: Tue, 16 Jun 2026 15:36:35 +0900 Subject: [PATCH 3/5] refactor: clarify calendar cell color keys and extract priority resolution logic --- .../src/pages/Dag/Calendar/CalendarCell.tsx | 10 +- .../pages/Dag/Calendar/calendarUtils.test.ts | 52 +++-- .../src/pages/Dag/Calendar/calendarUtils.ts | 197 ++++++++---------- .../ui/src/pages/Dag/Calendar/types.ts | 4 +- 4 files changed, 133 insertions(+), 130 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx index ea91e34309bb0..6660194329e72 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx @@ -28,8 +28,8 @@ type Props = { | Record | string | { - actual: string | { _dark: string; _light: string }; - planned: string | { _dark: string; _light: string }; + primary: string | { _dark: string; _light: string }; + secondary: string | { _dark: string; _light: string }; }; readonly cellData: CalendarCellData | undefined; readonly index?: number; @@ -64,7 +64,7 @@ export const CalendarCell = ({ : []; const isMixedState = - typeof backgroundColor === "object" && "planned" in backgroundColor && "actual" in backgroundColor; + typeof backgroundColor === "object" && "secondary" in backgroundColor && "primary" in backgroundColor; const cellBox = isMixedState ? ( { }); expect(scale.getColor({ ...EMPTY_COUNTS, planned: 1, success: 1, total: 2 })).toEqual({ - actual: DEFAULT_TOTAL_COLOR, - planned: PLANNED_COLOR, + primary: DEFAULT_TOTAL_COLOR, + secondary: PLANNED_COLOR, }); }); @@ -172,41 +172,57 @@ describe("createCalendarScale", () => { }); expect(scale.getColor({ ...EMPTY_COUNTS, queued: 1, success: 1, total: 2 })).toEqual({ - actual: DEFAULT_TOTAL_COLOR, - planned: PLANNED_COLOR, + primary: DEFAULT_TOTAL_COLOR, + secondary: PLANNED_COLOR, }); }); it("returns the failed color for a failed-only cell in total mode", () => { - const scale = createCalendarScale([run("failed", 1)], "total", "hourly"); + const scale = createCalendarScale([run("failed", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1 })).toEqual(DEFAULT_FAILED_COLOR); }); it("returns a mixed red and green color for failed and success runs in total mode", () => { - const scale = createCalendarScale([run("failed", 1), run("success", 1)], "total", "hourly"); + const scale = createCalendarScale([run("failed", 1), run("success", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, success: 1, total: 2 })).toEqual({ - actual: DEFAULT_TOTAL_COLOR, - planned: DEFAULT_FAILED_COLOR, + primary: DEFAULT_FAILED_COLOR, + secondary: DEFAULT_TOTAL_COLOR, }); }); it("returns a mixed cyan and green color for running and success runs in total mode", () => { - const scale = createCalendarScale([run("running", 1), run("success", 1)], "total", "hourly"); + const scale = createCalendarScale([run("running", 1), run("success", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); expect(scale.getColor({ ...EMPTY_COUNTS, running: 1, success: 1, total: 2 })).toEqual({ - actual: DEFAULT_TOTAL_COLOR, - planned: DEFAULT_RUNNING_COLOR, + primary: DEFAULT_RUNNING_COLOR, + secondary: DEFAULT_TOTAL_COLOR, }); }); it("returns a mixed cyan and red color for running and failed runs in total mode", () => { - const scale = createCalendarScale([run("running", 1), run("failed", 1)], "total", "hourly"); + const scale = createCalendarScale([run("running", 1), run("failed", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, running: 1, total: 2 })).toEqual({ - actual: DEFAULT_RUNNING_COLOR, - planned: DEFAULT_FAILED_COLOR, + primary: DEFAULT_FAILED_COLOR, + secondary: DEFAULT_RUNNING_COLOR, }); }); @@ -229,8 +245,8 @@ describe("createCalendarScale", () => { }); expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, planned: 1, total: 2 })).toEqual({ - actual: DEFAULT_FAILED_COLOR, - planned: PLANNED_COLOR, + primary: DEFAULT_FAILED_COLOR, + secondary: PLANNED_COLOR, }); }); @@ -252,8 +268,8 @@ describe("createCalendarScale", () => { }); expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, queued: 1, total: 2 })).toEqual({ - actual: DEFAULT_FAILED_COLOR, - planned: PLANNED_COLOR, + primary: DEFAULT_FAILED_COLOR, + secondary: PLANNED_COLOR, }); }); }); diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts index 239882e5480dc..527ebc6389c26 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts @@ -245,6 +245,75 @@ type ScaleOptions = { viewMode: CalendarColorMode; }; +type ColorValue = string | { _dark: string; _light: string }; + +type ResolveColorParams = { + failedColor: ColorValue; + failedCount: number; + hasPending: boolean; + runningCount: number; + successColor: ColorValue; + successCount: number; +}; + +const resolveCellColor = ({ + failedColor, + failedCount, + hasPending, + runningCount, + successColor, + successCount, +}: ResolveColorParams): ColorValue | { primary: ColorValue; secondary: ColorValue } => { + const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0; + + if (hasPending && hasActual) { + let primaryColor: ColorValue = EMPTY_COLOR; + + if (failedCount > 0) { + primaryColor = failedColor; + } else if (runningCount > 0) { + primaryColor = RUNNING_COLOR; + } else if (successCount > 0) { + primaryColor = successColor; + } + + return { + primary: primaryColor, + secondary: PLANNED_COLOR, + }; + } + + if (hasPending && !hasActual) { + return PLANNED_COLOR; + } + + if (hasActual) { + if (failedCount > 0 && runningCount > 0) { + return { primary: failedColor, secondary: RUNNING_COLOR }; + } + + if (failedCount > 0 && successCount > 0) { + return { primary: failedColor, secondary: successColor }; + } + + if (runningCount > 0 && successCount > 0) { + return { primary: RUNNING_COLOR, secondary: successColor }; + } + + if (failedCount > 0) { + return failedColor; + } + if (runningCount > 0) { + return RUNNING_COLOR; + } + if (successCount > 0) { + return successColor; + } + } + + return EMPTY_COLOR; +}; + export const createCalendarScale = ( data: Array, options: ScaleOptions, @@ -273,55 +342,18 @@ export const createCalendarScale = ( const successCount = viewMode === "total" ? counts.success : 0; const hasPending = getPendingRunCount(counts) > 0; - const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0; const failedColor = FAILURE_COLOR_INTENSITIES[2] ?? EMPTY_COLOR; const successColor = TOTAL_COLOR_INTENSITIES[2] ?? EMPTY_COLOR; - if (hasPending && hasActual) { - let actualColor = EMPTY_COLOR; - - if (failedCount > 0) { - actualColor = failedColor; - } else if (runningCount > 0) { - actualColor = RUNNING_COLOR; - } else if (successCount > 0) { - actualColor = successColor; - } - - return { - actual: actualColor, - planned: PLANNED_COLOR, - }; - } - - if (hasPending && !hasActual) { - return PLANNED_COLOR; - } - - if (hasActual) { - if (failedCount > 0 && runningCount > 0) { - return { actual: RUNNING_COLOR, planned: failedColor }; - } - if (failedCount > 0 && successCount > 0) { - return { actual: successColor, planned: failedColor }; - } - if (runningCount > 0 && successCount > 0) { - return { actual: successColor, planned: RUNNING_COLOR }; - } - - if (failedCount > 0) { - return failedColor; - } - if (runningCount > 0) { - return RUNNING_COLOR; - } - if (successCount > 0) { - return successColor; - } - } - - return EMPTY_COLOR; + return resolveCellColor({ + failedColor, + failedCount, + hasPending, + runningCount, + successColor, + successCount, + }); }, legendItems: [ { color: EMPTY_COLOR, label: "0" }, @@ -351,17 +383,14 @@ export const createCalendarScale = ( | string | { _dark: string; _light: string } | { - actual: string | { _dark: string; _light: string }; - planned: string | { _dark: string; _light: string }; + primary: string | { _dark: string; _light: string }; + secondary: string | { _dark: string; _light: string }; } => { const failedCount = counts.failed; const runningCount = viewMode === "total" ? counts.running : 0; const successCount = viewMode === "total" ? counts.success : 0; const hasPending = getPendingRunCount(counts) > 0; - const hasActual = failedCount > 0 || runningCount > 0 || successCount > 0; - - type ColorValue = string | { _dark: string; _light: string }; const getIntensityColor = (count: number, scheme: Array) => { if (count === 0) { @@ -378,61 +407,19 @@ export const createCalendarScale = ( return scheme[1] ?? EMPTY_COLOR; }; - if (hasPending && hasActual) { - let actualColor: ColorValue = EMPTY_COLOR; - - if (failedCount > 0) { - actualColor = getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES); - } else if (runningCount > 0) { - actualColor = RUNNING_COLOR; - } else if (successCount > 0) { - actualColor = getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES); - } - - return { - actual: actualColor, - planned: PLANNED_COLOR, - }; - } - - if (hasPending && !hasActual) { - return PLANNED_COLOR; - } - - if (hasActual) { - if (failedCount > 0 && runningCount > 0) { - return { - actual: RUNNING_COLOR, - planned: getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES), - }; - } - - if (failedCount > 0 && successCount > 0) { - return { - actual: getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES), - planned: getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES), - }; - } - - if (runningCount > 0 && successCount > 0) { - return { - actual: getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES), - planned: RUNNING_COLOR, - }; - } - - if (failedCount > 0) { - return getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES); - } - if (runningCount > 0) { - return RUNNING_COLOR; - } - if (successCount > 0) { - return getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES); - } - } - - return EMPTY_COLOR; + const failedColor = + failedCount > 0 ? getIntensityColor(failedCount, FAILURE_COLOR_INTENSITIES) : EMPTY_COLOR; + const successColor = + successCount > 0 ? getIntensityColor(successCount, TOTAL_COLOR_INTENSITIES) : EMPTY_COLOR; + + return resolveCellColor({ + failedColor, + failedCount, + hasPending, + runningCount, + successColor, + successCount, + }); }; const legendItems: Array = []; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts index 8ef78a66af4d3..7e93c63773462 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts @@ -69,8 +69,8 @@ export type CalendarScale = { | string | { _dark: string; _light: string } | { - actual: string | { _dark: string; _light: string }; - planned: string | { _dark: string; _light: string }; + primary: string | { _dark: string; _light: string }; + secondary: string | { _dark: string; _light: string }; }; readonly legendItems: Array; readonly type: CalendarScaleType; From 4e7dc60ff7c927aeecc68ad161c149b9b67f4eeb Mon Sep 17 00:00:00 2001 From: YeonShin Date: Tue, 16 Jun 2026 15:50:01 +0900 Subject: [PATCH 4/5] test: add missing test cases for calendar scale total runs --- .../pages/Dag/Calendar/calendarUtils.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts index 41d95fa4d463b..c35225cebdc8f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts @@ -272,4 +272,72 @@ describe("createCalendarScale", () => { secondary: PLANNED_COLOR, }); }); + + it("returns the correct gradient color when runs span across different dates", () => { + const scale = createCalendarScale( + [run("failed", 1, "2026-04-08T10:00:00Z"), run("failed", 5, "2026-04-09T10:00:00Z")], + { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }, + ); + + const lowIntensityColor = { _dark: "red.900", _light: "red.200" }; + const highIntensityColor = { _dark: "red.300", _light: "red.800" }; + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1 })).toEqual(lowIntensityColor); + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 5, total: 5 })).toEqual(highIntensityColor); + }); + + it("prioritizes failed over running over success when multiple actual states coexist with pending", () => { + const scale = createCalendarScale([run("planned", 1), run("failed", 1), run("success", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, planned: 1, success: 1, total: 3 })).toEqual({ + primary: DEFAULT_FAILED_COLOR, + secondary: PLANNED_COLOR, + }); + }); + + it("returns an empty scale when no data is provided", () => { + const scale = createCalendarScale([], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); + + expect(scale.type).toBe("empty"); + expect(scale.getColor(EMPTY_COUNTS)).toEqual(EMPTY_COLOR); + expect(scale.legendItems).toEqual([{ color: EMPTY_COLOR, label: "0" }]); + }); + + it("prioritizes running and failed colors when failed, running, and success coexist without pending states", () => { + const scale = createCalendarScale([run("failed", 1), run("running", 1), run("success", 1)], { + granularity: "hourly", + timezone: "UTC", + viewMode: "total", + }); + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, running: 1, success: 1, total: 3 })).toEqual({ + primary: DEFAULT_FAILED_COLOR, + secondary: DEFAULT_RUNNING_COLOR, + }); + }); + + it("returns the correct gradient color for failed mode when failed runs span across different dates", () => { + const scale = createCalendarScale( + [run("failed", 1, "2026-04-08T10:00:00Z"), run("failed", 10, "2026-04-09T10:00:00Z")], + { granularity: "hourly", timezone: "UTC", viewMode: "failed" }, + ); + + const lowIntensityFailedColor = { _dark: "red.900", _light: "red.200" }; + const highIntensityFailedColor = { _dark: "red.300", _light: "red.800" }; + + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 1, total: 1 })).toEqual(lowIntensityFailedColor); + expect(scale.getColor({ ...EMPTY_COUNTS, failed: 10, total: 10 })).toEqual(highIntensityFailedColor); + }); }); From 26b1a8333a7128b8f8b4aae5c5b501e3370f0fdf Mon Sep 17 00:00:00 2001 From: YeonShin Date: Wed, 17 Jun 2026 14:36:13 +0900 Subject: [PATCH 5/5] fix: expand calendar legend to clarify running and failed states --- .../src/pages/Dag/Calendar/CalendarLegend.tsx | 109 +++++++++++++----- 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx index 480c1a8e07702..7f12162bb5ba8 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx @@ -30,8 +30,48 @@ type Props = { readonly viewMode: CalendarColorMode; }; +type LegendColorType = + | Record + | string + | { primary: Record | string; secondary: Record | string }; + +const LegendIcon = ({ color, cursor }: { readonly color: LegendColorType; readonly cursor?: string }) => { + const isMixedState = typeof color === "object" && "primary" in color && "secondary" in color; + + if (isMixedState) { + return ( + + + + + ); + } + + return ; +}; + export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => { - const { t: translate } = useTranslation("dag"); + const { t: translate } = useTranslation(["dag", "common"]); const legendTitle = viewMode === "failed" ? translate("overview.buttons.failedRun_other") : translate("calendar.totalRuns"); @@ -54,7 +94,9 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => {[...scale.legendItems].reverse().map(({ color, label }) => ( - + + + ))} @@ -70,7 +112,9 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => {scale.legendItems.map(({ color, label }) => ( - + + + ))} @@ -83,42 +127,49 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => + {viewMode === "total" && ( + <> + + + + {translate("common:states.success")} + + + + + + {translate("common:states.running")} + + + + )} + + + + + {translate("common:states.failed")} + + + {translate("common:states.planned")} + - - - - + : { _dark: "green.700", _light: "green.400" }, + secondary: PLANNED_COLOR, + }} + /> - {translate("calendar.legend.mixed")} + {translate("dag:calendar.legend.mixed")}