jscheffl commented on code in PR #54252:
URL: https://github.com/apache/airflow/pull/54252#discussion_r2264862119
##########
airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json:
##########
@@ -5,6 +5,31 @@
"reason": "原因",
"title": "依賴 (Dependencies) 阻礙任務排程"
},
+ "calendar": {
+ "cellSize": "格子大小",
+ "daily": "每日",
+ "decreaseSize": "縮小尺寸",
+ "error": "載入日曆資料時發生錯誤",
+ "hourly": "每小時",
+ "increaseSize": "增大尺寸",
+ "legend": {
+ "successRateSpectrum": "成功率光譜",
+ "tooltips": {
+ "failed": "成功率低於20%",
+ "success100": "100%成功",
+ "successRate20": "20%以上成功率",
+ "successRate40": "40%以上成功率",
+ "successRate60": "60%以上成功率",
+ "successRate80": "80%以上成功率"
+ }
+ },
+ "less": "較少",
+ "loading": "載入中...",
+ "more": "較多",
+ "noData": "沒有可用的資料",
+ "px": "像素",
+ "week": "第 {{weekNumber}} 週"
Review Comment:
The translation seems to be inconsistent to EN - can you fix this up?
##########
airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx:
##########
@@ -0,0 +1,355 @@
+/* eslint-disable max-lines */
+
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, HStack, Text, IconButton, Button } from "@chakra-ui/react";
+import { keyframes } from "@emotion/react";
+import dayjs from "dayjs";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiMinus, FiPlus, FiChevronLeft, FiChevronRight } from
"react-icons/fi";
+import { useParams } from "react-router-dom";
+import { useLocalStorage } from "usehooks-ts";
+
+import { useCalendarServiceGetCalendar } from "openapi/queries";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import { Tooltip } from "src/components/ui";
+
+import { DailyCalendarView } from "./DailyCalendarView";
+import { HourlyCalendarView } from "./HourlyCalendarView";
+
+const spin = keyframes`
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+`;
+
+export const Calendar = () => {
+ const { dagId = "" } = useParams();
+ const { t: translate } = useTranslation("dag");
+ const [cellSize, setCellSize] = useLocalStorage("calendar-cell-size", 18);
+ const [selectedYear, setSelectedYear] = useState(dayjs().year());
+ const [selectedMonth, setSelectedMonth] = useState(dayjs().month());
+ const [granularity, setGranularity] = useLocalStorage<"daily" |
"hourly">("calendar-granularity", "daily");
+
+ const currentYear = dayjs().year();
+ const currentMonth = dayjs().month();
+
+ const getDateRange = () => {
+ if (granularity === "daily") {
+ return {
+ logicalDateGte: `${selectedYear}-01-01T00:00:00Z`,
+ logicalDateLte: `${selectedYear}-12-31T23:59:59Z`,
+ };
+ } else {
+ const monthStart =
dayjs().year(selectedYear).month(selectedMonth).startOf("month");
+ const monthEnd =
dayjs().year(selectedYear).month(selectedMonth).endOf("month");
+
+ return {
+ logicalDateGte: monthStart.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
+ logicalDateLte: monthEnd.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
+ };
+ }
+ };
+
+ const { data, error, isLoading } = useCalendarServiceGetCalendar(
+ {
+ dagId,
+ granularity,
+ ...getDateRange(),
+ },
+ undefined,
+ { enabled: Boolean(dagId) },
+ );
+
+ const renderLegend = () => (
+ <Box>
+ {/* Success Rate Spectrum */}
+ <Box mb={4}>
+ <Text color="gray.700" fontSize="sm" fontWeight="medium" mb={3}
textAlign="center">
+ {translate("calendar.legend.successRateSpectrum")}
+ </Text>
+ <HStack gap={3} justify="center">
+ <Text color="gray.600" fontSize="xs">
+ {translate("common:states.success")}
+ </Text>
+ <HStack borderRadius="full" boxShadow="sm" gap={0} overflow="hidden">
+ <Tooltip
content={translate("calendar.legend.tooltips.success100")} openDelay={300}>
+ <Box bg="#008000" cursor="pointer" height="20px" width="32px" />
+ </Tooltip>
+ <Tooltip
content={translate("calendar.legend.tooltips.successRate80")} openDelay={300}>
+ <Box bg="#16A34A" cursor="pointer" height="20px" width="24px" />
+ </Tooltip>
+ <Tooltip
content={translate("calendar.legend.tooltips.successRate60")} openDelay={300}>
+ <Box bg="#22C55E" cursor="pointer" height="20px" width="24px" />
+ </Tooltip>
+ <Tooltip
content={translate("calendar.legend.tooltips.successRate40")} openDelay={300}>
+ <Box bg="#EAB308" cursor="pointer" height="20px" width="24px" />
+ </Tooltip>
+ <Tooltip
content={translate("calendar.legend.tooltips.successRate20")} openDelay={300}>
+ <Box bg="#F97316" cursor="pointer" height="20px" width="24px" />
+ </Tooltip>
+ <Tooltip content={translate("calendar.legend.tooltips.failed")}
openDelay={300}>
+ <Box bg="#DC2626" cursor="pointer" height="20px" width="32px" />
+ </Tooltip>
+ </HStack>
+ <Text color="gray.600" fontSize="xs">
+ {translate("common:states.failed")}
+ </Text>
+ </HStack>
+ </Box>
+
+ {/* Single State Colors */}
+ <Box>
+ <Text color="gray.700" fontSize="sm" fontWeight="medium" mb={3}
textAlign="center">
+ {translate("common:state")}
+ </Text>
+ <HStack gap={4} justify="center" wrap="wrap">
+ <HStack gap={2}>
+ <Box bg="#3182CE" borderRadius="full" boxShadow="sm" height="16px"
width="16px" />
+ <Text color="gray.600" fontSize="sm">
+ {translate("common:states.running")}
+ </Text>
+ </HStack>
+ <HStack gap={2}>
+ <Box bg="#F1E7DA" borderRadius="full" boxShadow="sm" height="16px"
width="16px" />
+ <Text color="gray.600" fontSize="sm">
+ {translate("common:states.planned")}
+ </Text>
+ </HStack>
+ <HStack gap={2}>
+ <Box bg="#808080" borderRadius="full" boxShadow="sm" height="16px"
width="16px" />
+ <Text color="gray.600" fontSize="sm">
+ {translate("common:states.queued")}
+ </Text>
+ </HStack>
+ <HStack gap={2}>
+ <Box bg="#ebedf0" borderRadius="full" boxShadow="sm" height="16px"
width="16px" />
+ <Text color="gray.600" fontSize="sm">
+ {translate("common:states.no_status")}
+ </Text>
+ </HStack>
+ </HStack>
+ </Box>
+ </Box>
+ );
+
+ const renderCalendarContent = () => (
+ <Box>
+ {granularity === "daily" ? (
+ <>
+ <DailyCalendarView cellSize={cellSize} data={data?.dag_runs ?? []}
selectedYear={selectedYear} />
+ {renderLegend()}
Review Comment:
Why are you adding a fucntion to generate the element tree and not a
component to integrate as legend?
##########
airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx:
##########
@@ -0,0 +1,355 @@
+/* eslint-disable max-lines */
+
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, HStack, Text, IconButton, Button } from "@chakra-ui/react";
+import { keyframes } from "@emotion/react";
+import dayjs from "dayjs";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiMinus, FiPlus, FiChevronLeft, FiChevronRight } from
"react-icons/fi";
+import { useParams } from "react-router-dom";
+import { useLocalStorage } from "usehooks-ts";
+
+import { useCalendarServiceGetCalendar } from "openapi/queries";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import { Tooltip } from "src/components/ui";
+
+import { DailyCalendarView } from "./DailyCalendarView";
+import { HourlyCalendarView } from "./HourlyCalendarView";
+
+const spin = keyframes`
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+`;
+
+export const Calendar = () => {
+ const { dagId = "" } = useParams();
+ const { t: translate } = useTranslation("dag");
+ const [cellSize, setCellSize] = useLocalStorage("calendar-cell-size", 18);
+ const [selectedYear, setSelectedYear] = useState(dayjs().year());
+ const [selectedMonth, setSelectedMonth] = useState(dayjs().month());
+ const [granularity, setGranularity] = useLocalStorage<"daily" |
"hourly">("calendar-granularity", "daily");
+
+ const currentYear = dayjs().year();
+ const currentMonth = dayjs().month();
+
+ const getDateRange = () => {
+ if (granularity === "daily") {
+ return {
+ logicalDateGte: `${selectedYear}-01-01T00:00:00Z`,
+ logicalDateLte: `${selectedYear}-12-31T23:59:59Z`,
+ };
+ } else {
+ const monthStart =
dayjs().year(selectedYear).month(selectedMonth).startOf("month");
+ const monthEnd =
dayjs().year(selectedYear).month(selectedMonth).endOf("month");
+
+ return {
+ logicalDateGte: monthStart.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
+ logicalDateLte: monthEnd.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
+ };
+ }
+ };
+
+ const { data, error, isLoading } = useCalendarServiceGetCalendar(
+ {
+ dagId,
+ granularity,
+ ...getDateRange(),
+ },
+ undefined,
+ { enabled: Boolean(dagId) },
+ );
+
+ const renderLegend = () => (
+ <Box>
+ {/* Success Rate Spectrum */}
+ <Box mb={4}>
+ <Text color="gray.700" fontSize="sm" fontWeight="medium" mb={3}
textAlign="center">
+ {translate("calendar.legend.successRateSpectrum")}
+ </Text>
+ <HStack gap={3} justify="center">
+ <Text color="gray.600" fontSize="xs">
+ {translate("common:states.success")}
+ </Text>
+ <HStack borderRadius="full" boxShadow="sm" gap={0} overflow="hidden">
+ <Tooltip
content={translate("calendar.legend.tooltips.success100")} openDelay={300}>
+ <Box bg="#008000" cursor="pointer" height="20px" width="32px" />
+ </Tooltip>
+ <Tooltip
content={translate("calendar.legend.tooltips.successRate80")} openDelay={300}>
+ <Box bg="#16A34A" cursor="pointer" height="20px" width="24px" />
Review Comment:
Colors should use logical colors and not hard coded RGB values.
##########
airflow-core/src/airflow/ui/src/pages/Dag/Calendar/constants.ts:
##########
@@ -0,0 +1,61 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { DagRunState } from "./types";
+
+export const CALENDAR_STATE_COLORS = {
+ empty: "#ebedf0",
+ failed: {
+ medium: "#EF4444",
+ pure: "#DC2626",
+ },
+ mixed: {
+ moderate: "#EAB308",
+ poor: "#F97316",
+ },
+ other: "#9CA3AF",
+ planned: {
+ pure: "#F1E7DA",
Review Comment:
As above: Please do not use hard coded RGB.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]