This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 7c62f118c8db3f5156f249a16fbeb80c5fb76b62 Author: LI,JHE-CHEN <[email protected]> AuthorDate: Tue Sep 30 12:41:01 2025 -0400 fix(ui): modify calendar cell colors (#56161) * fix(ui): modify calendar cell colors * feat(ui): Add legend for mixed state (cherry picked from commit fa0e02c2f85e9edea6fc6e29d62115552b635fdd) --- .../src/airflow/ui/public/i18n/locales/en/dag.json | 1 + .../ui/src/pages/Dag/Calendar/CalendarCell.tsx | 39 ++++++++++- .../ui/src/pages/Dag/Calendar/CalendarLegend.tsx | 33 +++++++++- .../ui/src/pages/Dag/Calendar/calendarUtils.ts | 76 +++++++++++++++++----- .../src/airflow/ui/src/pages/Dag/Calendar/types.ts | 8 ++- 5 files changed, 136 insertions(+), 21 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index fd1eef88f95..c5293ca7dba 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -10,6 +10,7 @@ "hourly": "Hourly", "legend": { "less": "Less", + "mixed": "Mixed", "more": "More" }, "navigation": { 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 da6973810ea..4428a2b4e42 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 @@ -25,7 +25,13 @@ import { CalendarTooltip } from "./CalendarTooltip"; import type { CalendarCellData, CalendarColorMode } from "./types"; type Props = { - readonly backgroundColor: Record<string, string> | string; + readonly backgroundColor: + | Record<string, string> + | string + | { + actual: string | { _dark: string; _light: string }; + planned: string | { _dark: string; _light: string }; + }; readonly cellData: CalendarCellData | undefined; readonly index?: number; readonly marginRight?: string; @@ -52,7 +58,36 @@ export const CalendarCell = ({ const hasData = Boolean(cellData && relevantCount > 0); const hasTooltip = Boolean(cellData); - const cellBox = ( + const isMixedState = + typeof backgroundColor === "object" && "planned" in backgroundColor && "actual" in backgroundColor; + + const cellBox = isMixedState ? ( + <Box + _hover={hasData ? { transform: "scale(1.1)" } : {}} + borderRadius="2px" + cursor={hasData ? "pointer" : "default"} + height="14px" + marginRight={computedMarginRight} + overflow="hidden" + position="relative" + width="14px" + > + <Box + bg={backgroundColor.planned} + clipPath="polygon(0 100%, 100% 100%, 0 0)" + height="100%" + position="absolute" + width="100%" + /> + <Box + bg={backgroundColor.actual} + clipPath="polygon(100% 0, 100% 100%, 0 0)" + height="100%" + position="absolute" + width="100%" + /> + </Box> + ) : ( <Box _hover={hasData ? { transform: "scale(1.1)" } : {}} bg={backgroundColor} 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 86ed307f8d8..480c1a8e077 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 @@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next"; import { Tooltip } from "src/components/ui"; +import { PLANNED_COLOR } from "./calendarUtils"; import type { CalendarScale, CalendarColorMode } from "./types"; type Props = { @@ -82,16 +83,42 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => <Box> <HStack gap={4} justify="center" wrap="wrap"> + <HStack gap={2}> + <Box bg={PLANNED_COLOR} borderRadius="2px" boxShadow="sm" height="14px" width="14px" /> + <Text color="fg.muted" fontSize="xs"> + {translate("common:states.planned")} + </Text> + </HStack> <HStack gap={2}> <Box - bg={{ _dark: "stone.600", _light: "stone.200" }} borderRadius="2px" boxShadow="sm" height="14px" + overflow="hidden" + position="relative" width="14px" - /> + > + <Box + bg={PLANNED_COLOR} + clipPath="polygon(0 100%, 100% 100%, 0 0)" + height="100%" + position="absolute" + width="100%" + /> + <Box + bg={ + viewMode === "failed" + ? { _dark: "red.700", _light: "red.400" } + : { _dark: "green.700", _light: "green.400" } + } + clipPath="polygon(100% 0, 100% 100%, 0 0)" + height="100%" + position="absolute" + width="100%" + /> + </Box> <Text color="fg.muted" fontSize="xs"> - {translate("common:states.planned")} + {translate("calendar.legend.mixed")} </Text> </HStack> </HStack> 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 4ea24f89f81..3d4fbaab846 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 @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + /*! * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -34,23 +36,23 @@ import type { dayjs.extend(isSameOrBefore); // Calendar color constants +export const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.500" }; const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" }; -const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.200" }; const TOTAL_COLOR_INTENSITIES = [ EMPTY_COLOR, // 0 - { _dark: "green.300", _light: "green.200" }, - { _dark: "green.500", _light: "green.400" }, - { _dark: "green.700", _light: "green.600" }, - { _dark: "green.900", _light: "green.800" }, + { _dark: "green.900", _light: "green.200" }, + { _dark: "green.700", _light: "green.400" }, + { _dark: "green.500", _light: "green.600" }, + { _dark: "green.300", _light: "green.800" }, ]; const FAILURE_COLOR_INTENSITIES = [ EMPTY_COLOR, // 0 - { _dark: "red.300", _light: "red.200" }, - { _dark: "red.500", _light: "red.400" }, - { _dark: "red.700", _light: "red.600" }, - { _dark: "red.900", _light: "red.800" }, + { _dark: "red.900", _light: "red.200" }, + { _dark: "red.700", _light: "red.400" }, + { _dark: "red.500", _light: "red.600" }, + { _dark: "red.300", _light: "red.800" }, ]; const createDailyDataMap = (data: Array<CalendarTimeRangeResponse>) => { @@ -224,12 +226,22 @@ export const createCalendarScale = ( return { getColor: (counts: RunCounts) => { - if (counts.planned > 0) { + const actualCount = viewMode === "total" ? counts.total - counts.planned : counts.failed; + const hasPlanned = counts.planned > 0; + const hasActual = actualCount > 0; + + if (hasPlanned && hasActual) { + return { + actual: singleColor, + planned: PLANNED_COLOR, + }; + } + + if (hasPlanned && !hasActual) { return PLANNED_COLOR; } - const targetCount = viewMode === "total" ? counts.total : counts.failed; - return targetCount === 0 ? EMPTY_COLOR : singleColor; + return actualCount === 0 ? EMPTY_COLOR : singleColor; }, legendItems: [ { color: EMPTY_COLOR, label: "0" }, @@ -253,12 +265,46 @@ export const createCalendarScale = ( const uniqueThresholds = [...new Set(thresholds)].sort((first, second) => first - second); - const getColor = (counts: RunCounts): string | { _dark: string; _light: string } => { - if (counts.planned > 0) { + const getColor = ( + counts: RunCounts, + ): + | string + | { _dark: string; _light: string } + | { + actual: string | { _dark: string; _light: string }; + planned: string | { _dark: string; _light: string }; + } => { + const actualCount = viewMode === "total" ? counts.total - counts.planned : counts.failed; + const hasPlanned = counts.planned > 0; + const hasActual = actualCount > 0; + + if (hasPlanned && hasActual) { + let actualColor = colorScheme[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 (actualCount > 0 && actualColor === colorScheme[0]) { + actualColor = colorScheme[1] ?? EMPTY_COLOR; + } + + return { + actual: actualColor, + planned: PLANNED_COLOR, + }; + } + + if (hasPlanned && !hasActual) { return PLANNED_COLOR; } - const targetCount = viewMode === "total" ? counts.total : counts.failed; + const targetCount = actualCount; if (targetCount === 0) { return colorScheme[0] ?? EMPTY_COLOR; 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 147aadf7d8b..8ef78a66af4 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 @@ -65,7 +65,13 @@ export type LegendItem = { export type CalendarScaleType = "empty" | "gradient" | "single_value"; export type CalendarScale = { - readonly getColor: (counts: RunCounts) => string | { _dark: string; _light: string }; + readonly getColor: (counts: RunCounts) => + | string + | { _dark: string; _light: string } + | { + actual: string | { _dark: string; _light: string }; + planned: string | { _dark: string; _light: string }; + }; readonly legendItems: Array<LegendItem>; readonly type: CalendarScaleType; };
