Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fcec159
feat(ui): WIP - Add initial scaffold for DAG Calendar view
RoyLee1224 Aug 6, 2025
c7e47fe
feat(ui): enhance Dag Calendar with hourly/daily view
RoyLee1224 Aug 7, 2025
11e7a96
feat(ui): Implement state and Success Rate Spectrum for calendar view
RoyLee1224 Aug 8, 2025
00f89f2
feat(ui): implement debounced tooltip for calendar grid
RoyLee1224 Aug 8, 2025
ae98bd6
feat(ui): enhance calendar view UX with improved loading and button
RoyLee1224 Aug 8, 2025
839555d
feat(ui): modify Success Rate Spectrum layout in hourly calender view
RoyLee1224 Aug 8, 2025
85de630
Merge branch 'main' into calendar
RoyLee1224 Aug 8, 2025
da3ad99
perf(ui): optimize calendar data processing
RoyLee1224 Aug 9, 2025
504fd7f
feat(ui): refactor calendar legend layout and improve hourly view pos…
RoyLee1224 Aug 10, 2025
2026034
feat(ui): improve calendar cellSize controls and hourly view text dis…
RoyLee1224 Aug 10, 2025
9be6f57
feat(ui): enhance calendar dark mode support and replace hardcoded RG…
RoyLee1224 Aug 10, 2025
5037b3c
refactor(ui): optimize calendar date state management
RoyLee1224 Aug 10, 2025
f731aee
refactor(ui): Extract calendar tooltip logic into useDelayedTooltip hook
RoyLee1224 Aug 10, 2025
5a3a187
refactor(ui): reduce duplication in CalendarLegend
RoyLee1224 Aug 11, 2025
e57a071
style(ui): improve calendar UI consistency and accessibility
RoyLee1224 Aug 12, 2025
9a59262
fix(i18n): modify translations
RoyLee1224 Aug 16, 2025
cea2950
fix(ui): remove cellsize changing function
RoyLee1224 Aug 22, 2025
a509f43
feat(ui): GitHub-style heatmap, default to hourly.
RoyLee1224 Aug 22, 2025
66f64b2
fix(ui): Rename calendar colorMode to viewMode to avoid confusion wit…
RoyLee1224 Aug 22, 2025
7effbf1
style(calendar): reduce cell size to 14x14px and simplify weekday lab…
RoyLee1224 Aug 22, 2025
f65a75b
refactor(ui): consolidate calendar cells
RoyLee1224 Aug 22, 2025
d2b1718
style(ui): modify calendar cell gap and legend size
RoyLee1224 Aug 22, 2025
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 @@ -170,6 +170,7 @@
"failed": "Failed",
"no_status": "No Status",
"none": "No Status",
"planned": "Planned",
"queued": "Queued",
"removed": "Removed",
"restarting": "Restarting",
Expand Down
28 changes: 28 additions & 0 deletions airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@
"reason": "Reason",
"title": "Dependencies Blocking Task From Getting Scheduled"
},
"calendar": {
"daily": "Daily",
"hourly": "Hourly",
"legend": {
"less": "Less",
"more": "More"
},
"navigation": {
"nextMonth": "Next month",
"nextYear": "Next year",
"previousMonth": "Previous month",
"previousYear": "Previous year"
},
"noData": "No data available",
"noRuns": "No runs",
"totalRuns": "Total Runs",
"week": "Week {{weekNumber}}",
"weekdays": {
"friday": "Fri",
"monday": "Mon",
"saturday": "Sat",
"sunday": "Sun",
"thursday": "Thu",
"tuesday": "Tue",
"wednesday": "Wed"
}
},
"code": {
"bundleUrl": "Bundle Url",
"noCode": "No Code Found",
Expand Down Expand Up @@ -101,6 +128,7 @@
"assetEvents": "Asset Events",
"auditLog": "Audit Log",
"backfills": "Backfills",
"calendar": "Calendar",
"code": "Code",
"details": "Details",
"logs": "Logs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"failed": "失敗",
"no_status": "無狀態",
"none": "無狀態",
"planned": "已計劃",
"queued": "排隊中",
"removed": "已移除",
"restarting": "重啟中",
Expand Down
29 changes: 29 additions & 0 deletions airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@
"reason": "原因",
"title": "依賴 (Dependencies) 阻礙任務排程"
},
"calendar": {
"daily": "每日",
"error": "載入日曆資料時發生錯誤",
"hourly": "每小時",
"legend": {
"less": "較少",
"more": "較多"
},
"navigation": {
"nextMonth": "下一月",
"nextYear": "下一年",
"previousMonth": "上一月",
"previousYear": "上一年"
},
"noData": "沒有可用的資料",
"noRuns": "沒有執行",
"totalRuns": "總執行數",
"week": "第 {{weekNumber}} 週",
"weekdays": {
"friday": "五",
"monday": "一",
"saturday": "六",
"sunday": "日",
"thursday": "四",
"tuesday": "二",
"wednesday": "三"
}
},
"code": {
"bundleUrl": "套件包網址",
"noCode": "找不到程式碼",
Expand Down Expand Up @@ -101,6 +129,7 @@
"assetEvents": "資源事件",
"auditLog": "審計日誌",
"backfills": "回填",
"calendar": "日曆",
"code": "程式碼",
"details": "詳細資訊",
"logs": "日誌",
Expand Down
270 changes: 270 additions & 0 deletions airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*!
* 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, ButtonGroup } from "@chakra-ui/react";
import { keyframes } from "@emotion/react";
import dayjs from "dayjs";
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { 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 { CalendarLegend } from "./CalendarLegend";
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 [selectedDate, setSelectedDate] = useState(dayjs());
const [granularity, setGranularity] = useLocalStorage<"daily" | "hourly">("calendar-granularity", "hourly");
const [viewMode, setViewMode] = useLocalStorage<"failed" | "total">("calendar-view-mode", "total");

const currentDate = dayjs();

const dateRange = useMemo(() => {
if (granularity === "daily") {
const yearStart = selectedDate.startOf("year");
const yearEnd = selectedDate.endOf("year");

return {
logicalDateGte: yearStart.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
logicalDateLte: yearEnd.format("YYYY-MM-DD[T]HH:mm:ss[Z]"),
};
} else {
const monthStart = selectedDate.startOf("month");
const monthEnd = selectedDate.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]"),
};
}
}, [granularity, selectedDate]);

const { data, error, isLoading } = useCalendarServiceGetCalendar(
{
dagId,
granularity,
...dateRange,
},
undefined,
{ enabled: Boolean(dagId) },
);

if (!data && !isLoading) {
return (
<Box p={4}>
<Text>{translate("calendar.noData")}</Text>
</Box>
);
}

return (
<Box p={6}>
<ErrorAlert error={error} />
<HStack justify="space-between" mb={6}>
<HStack gap={4} mb={4}>
{granularity === "daily" ? (
<HStack gap={2}>
<IconButton
aria-label={translate("calendar.navigation.previousYear")}
onClick={() => setSelectedDate(selectedDate.subtract(1, "year"))}
size="sm"
variant="ghost"
>
<FiChevronLeft />
</IconButton>
<Text
_hover={selectedDate.year() === currentDate.year() ? {} : { textDecoration: "underline" }}
color={selectedDate.year() === currentDate.year() ? "fg.info" : "inherit"}
cursor={selectedDate.year() === currentDate.year() ? "default" : "pointer"}
fontSize="xl"
fontWeight="bold"
minWidth="120px"
onClick={() => {
if (selectedDate.year() !== currentDate.year()) {
setSelectedDate(currentDate.startOf("year"));
}
}}
textAlign="center"
>
{selectedDate.year()}
</Text>
<IconButton
aria-label={translate("calendar.navigation.nextYear")}
onClick={() => setSelectedDate(selectedDate.add(1, "year"))}
size="sm"
variant="ghost"
>
<FiChevronRight />
</IconButton>
</HStack>
) : (
<HStack gap={2}>
<IconButton
aria-label={translate("calendar.navigation.previousMonth")}
onClick={() => setSelectedDate(selectedDate.subtract(1, "month"))}
size="sm"
variant="ghost"
>
<FiChevronLeft />
</IconButton>
<Text
_hover={
selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year")
? {}
: { textDecoration: "underline" }
}
color={
selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year")
? "fg.info"
: "inherit"
}
cursor={
selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year")
? "default"
: "pointer"
}
fontSize="xl"
fontWeight="bold"
minWidth="120px"
onClick={() => {
if (
!(selectedDate.isSame(currentDate, "month") && selectedDate.isSame(currentDate, "year"))
) {
setSelectedDate(currentDate.startOf("month"));
}
}}
textAlign="center"
>
{selectedDate.format("MMM YYYY")}
</Text>
<IconButton
aria-label={translate("calendar.navigation.nextMonth")}
onClick={() => setSelectedDate(selectedDate.add(1, "month"))}
size="sm"
variant="ghost"
>
<FiChevronRight />
</IconButton>
</HStack>
)}

<ButtonGroup attached size="sm" variant="outline">
<Button
colorPalette="blue"
onClick={() => setGranularity("daily")}
variant={granularity === "daily" ? "solid" : "outline"}
>
{translate("calendar.daily")}
</Button>
<Button
colorPalette="blue"
onClick={() => setGranularity("hourly")}
variant={granularity === "hourly" ? "solid" : "outline"}
>
{translate("calendar.hourly")}
</Button>
</ButtonGroup>

<ButtonGroup attached size="sm" variant="outline">
<Button
colorPalette="blue"
onClick={() => setViewMode("total")}
variant={viewMode === "total" ? "solid" : "outline"}
>
{translate("calendar.totalRuns")}
</Button>
<Button
colorPalette="blue"
onClick={() => setViewMode("failed")}
variant={viewMode === "failed" ? "solid" : "outline"}
>
{translate("overview.buttons.failedRun_other")}
</Button>
</ButtonGroup>
</HStack>
</HStack>

<Box position="relative">
{isLoading ? (
<Box
alignItems="center"
backdropFilter="blur(2px)"
bg="bg/80"
borderRadius="md"
bottom="0"
display="flex"
justifyContent="center"
left="0"
position="absolute"
right="0"
top="0"
zIndex={10}
>
<Box textAlign="center">
<Box
animation={`${spin} 1s linear infinite`}
border="3px solid"
borderColor={{ _dark: "gray.600", _light: "blue.100" }}
borderRadius="50%"
borderTopColor="blue.500"
height="24px"
width="24px"
/>
</Box>
</Box>
) : undefined}
{granularity === "daily" ? (
<>
<DailyCalendarView
colorMode={viewMode}
data={data?.dag_runs ?? []}
selectedYear={selectedDate.year()}
/>
<CalendarLegend colorMode={viewMode} />
</>
) : (
<HStack align="start" gap={2}>
<Box>
<HourlyCalendarView
colorMode={viewMode}
data={data?.dag_runs ?? []}
selectedMonth={selectedDate.month()}
selectedYear={selectedDate.year()}
/>
</Box>
<Box display="flex" flex="1" justifyContent="center" pt={16}>
<CalendarLegend colorMode={viewMode} vertical />
</Box>
</HStack>
)}
</Box>
</Box>
);
};
Loading
Loading