This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new fa0e02c2f85 fix(ui): modify calendar cell colors (#56161)
fa0e02c2f85 is described below
commit fa0e02c2f85e9edea6fc6e29d62115552b635fdd
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
---
.../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;
};