Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e593169
Add FailedIcon component and DurationChart failed icon plugin
imrichardwu Feb 13, 2026
963ad54
feat: add fail icon if status is failed
imrichardwu Feb 14, 2026
15d67f4
fix: adjust fail icon padding to be dynamic
imrichardwu Feb 14, 2026
b75c411
allow user to click on fail icon and redirect to task instances
imrichardwu Feb 14, 2026
80351f9
feat: add deadlines UI - DeadlineIcon, Deadlines page, i18n, and router
imrichardwu Feb 19, 2026
fe586c8
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 20, 2026
71ccaad
fix: ensure icons are positioned correctly above the rendered bar by …
imrichardwu Feb 20, 2026
d932933
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 24, 2026
af9d4d2
feat(ui): add deadline translations and refactor Deadlines component …
imrichardwu Feb 24, 2026
c851cb1
refactor(ui): consolidate imports in Bar component for cleaner code
imrichardwu Feb 25, 2026
e8099a6
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 25, 2026
d78dc22
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 25, 2026
ea0649e
refactor(ui): remove unused DeadlineIcon and FailedIcon components, r…
imrichardwu Feb 25, 2026
435cb3e
refactor(ui): simplify failed state icon rendering in Bar component
imrichardwu Feb 25, 2026
9e17bf0
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 25, 2026
d083286
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 25, 2026
d847834
Merge branch 'main' into error-ui-frontend
imrichardwu Feb 26, 2026
709a4cc
Merge branch 'main' into error-ui-frontend
imrichardwu Mar 17, 2026
7b9c20a
refactor: remove redundant BAR_HEIGHT constant from Bar component
imrichardwu Feb 27, 2026
5d348eb
Merge branch 'main' into error-ui-frontend
imrichardwu Mar 17, 2026
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 @@ -74,6 +74,8 @@
"dagRun_other": "Dag Runs",
"dagRunId": "Dag Run ID",
"dagWarnings": "Dag warnings/errors",
"deadline_one": "Deadline",
"deadline_other": "Deadlines",
"defaultToGraphView": "Default to graph view",
"defaultToGridView": "Default to grid view",
"delete": "Delete",
Expand Down
12 changes: 12 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 @@ -40,6 +40,17 @@
"parseDuration": "Parse Duration:",
"parsedAt": "Parsed at:"
},
"deadlines": {
"createdAt": "Created At",
"deadlineTime": "Deadline Time",
"description": "Description",
"missed": "Missed",
"name": "Name",
"noDeadlines": "No deadlines for this DAG run.",
"onTrack": "On Track",
"showMissedOnly": "Show missed only",
"status": "Status"
},
"extraLinks": "Extra Links",
"grid": {
"buttons": {
Expand Down Expand Up @@ -171,6 +182,7 @@
"backfills": "Backfills",
"calendar": "Calendar",
"code": "Code",
"deadlines": "Deadlines",
"details": "Details",
"logs": "Logs",
"mappedTaskInstances_one": "Task Instance [{{count}}]",
Expand Down
47 changes: 45 additions & 2 deletions airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Flex, Box } from "@chakra-ui/react";
import { useParams, useSearchParams } from "react-router-dom";
import { Flex, Box, Center, Button, Icon } from "@chakra-ui/react";
import { LuClock, LuTriangleAlert } from "react-icons/lu";
import { useParams, useSearchParams, useNavigate } from "react-router-dom";

import { RunTypeIcon } from "src/components/RunTypeIcon";
import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions";
Expand All @@ -32,6 +33,11 @@ import {
type GridRunWithVersionFlags,
} from "./useGridRunsWithVersionFlags";

const ICON_GAP_PX = 4;
const ICON_HEIGHT_PX = 16;
const BAR_PADDING_BOTTOM_PX = 2;
const BAR_MIN_HEIGHT_PX = 14;

type Props = {
readonly max: number;
readonly onClick?: () => void;
Expand All @@ -47,10 +53,24 @@ export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => {
const isSelected = runId === run.run_id;
const isHovered = hoveredRunId === run.run_id;
const search = searchParams.toString();
const isFailed = (run.state ?? "").toLowerCase() === "failed";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why that 'isFailed' based on the 'run.state' ? We do not set an icon for each possible dag run states, what is the motivation here?

Copy link
Contributor Author

@imrichardwu imrichardwu Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is to prevent the failed icon and deadline icon from overlapping

const hasMissedDeadline = Boolean(run.has_missed_deadline);
const barHeightPx = max > 0 ? (run.duration / max) * BAR_HEIGHT : 0;

const handleMouseEnter = () => setHoveredRunId(run.run_id);
const handleMouseLeave = () => setHoveredRunId(undefined);

const navigate = useNavigate();

const handleDeadlineIconClick = () => {
void navigate({ pathname: `/dags/${dagId}/runs/${run.run_id}/deadlines`, search });
};

// Account for minHeight and padding-bottom so icons always appear above the rendered bar
const effectiveBarHeightPx = Math.max(barHeightPx, BAR_MIN_HEIGHT_PX) + BAR_PADDING_BOTTOM_PX;
const failedIconBottom = effectiveBarHeightPx + ICON_GAP_PX;
const deadlineIconBottom = isFailed ? failedIconBottom + ICON_HEIGHT_PX : failedIconBottom;

Comment on lines +70 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could break alignment with the 'Gantt', can you show a picture with deadline alerts failed and the Gantt chart displayed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is 2 images

Screenshot 2026-02-25 at 11 35 38 AM Screenshot 2026-02-25 at 11 35 30 AM

return (
<Box
bg={isSelected ? "brand.emphasized" : isHovered ? "brand.muted" : undefined}
Expand All @@ -59,6 +79,29 @@ export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => {
position="relative"
transition="background-color 0.2s"
>
{hasMissedDeadline ? (
<Center bottom={`${deadlineIconBottom}px`} left={0} position="absolute" right={0} zIndex={2}>
<Button
_focusVisible={{ boxShadow: "none" }}
borderRadius={0}
h="auto"
lineHeight={1}
m={0}
minH={0}
minW={0}
onClick={handleDeadlineIconClick}
p={0}
variant="ghost"
>
<Icon as={LuClock} boxSize={3} color="warning.solid" />
</Button>
</Center>
) : undefined}
{isFailed ? (
<Center bottom={`${failedIconBottom}px`} left={0} position="absolute" right={0} zIndex={2}>
<Icon as={LuTriangleAlert} boxSize={3} color="failed.solid" />
</Center>
) : undefined}
{run.isBundleVersionChange &&
(showVersionIndicatorMode === VersionIndicatorOptions.BUNDLE_VERSION ||
showVersionIndicatorMode === VersionIndicatorOptions.ALL) ? (
Expand Down
19 changes: 15 additions & 4 deletions airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { TaskInstancesColumn } from "./TaskInstancesColumn";
import { TaskNames } from "./TaskNames";
import {
GRID_HEADER_HEIGHT_PX,
GRID_HEADER_ICON_SPACE_PX,
GRID_HEADER_PADDING_PX,
GRID_OUTER_PADDING_PX,
ROW_HEIGHT,
Expand Down Expand Up @@ -166,11 +167,21 @@ export const Grid = ({
{/* Grid header, both bgs are needed to hide elements during horizontal and vertical scroll */}
<Flex bg="bg" display="flex" position="sticky" pt={`${GRID_HEADER_PADDING_PX}px`} top={0} zIndex={2}>
<Box bg="bg" flexGrow={1} left={0} minWidth="200px" position="sticky" zIndex={1}>
<Flex flexDirection="column-reverse" height={`${GRID_HEADER_HEIGHT_PX}px`} position="relative">
<Flex
flexDirection="column-reverse"
height={`${GRID_HEADER_HEIGHT_PX + GRID_HEADER_ICON_SPACE_PX}px`}
position="relative"
>
{Boolean(gridRuns?.length) && (
<>
<DurationTick bottom={`${GRID_HEADER_HEIGHT_PX - 8}px`} duration={max} />
<DurationTick bottom={`${GRID_HEADER_HEIGHT_PX / 2 - 4}px`} duration={max / 2} />
<DurationTick
bottom={`${GRID_HEADER_HEIGHT_PX + GRID_HEADER_ICON_SPACE_PX - 8}px`}
duration={max}
/>
<DurationTick
bottom={`${GRID_HEADER_HEIGHT_PX / 2 + GRID_HEADER_ICON_SPACE_PX - 4}px`}
duration={max / 2}
/>
</>
)}
</Flex>
Expand All @@ -196,7 +207,7 @@ export const Grid = ({
<Link to={`/dags/${dagId}`}>
<IconButton
aria-label={translate("grid.buttons.resetToLatest")}
height={`${GRID_HEADER_HEIGHT_PX - 2}px`}
height={`${GRID_HEADER_HEIGHT_PX + GRID_HEADER_ICON_SPACE_PX - 2}px`}
loading={isLoading}
minW={0}
ml={1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const ROW_HEIGHT = 20;
export const GRID_OUTER_PADDING_PX = 64; // pt={16} = 16 * 4 = 64px
export const GRID_HEADER_PADDING_PX = 16; // pt={4} = 4 * 4 = 16px
export const GRID_HEADER_HEIGHT_PX = 100; // height="100px" for duration bars
// Space above bars for failed-run icon so it is not clipped
export const GRID_HEADER_ICON_SPACE_PX = 28;
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is making computation complex and easy to mess up in the Grid component.

We should probably refactor to make it more straight forward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to use flexbox before, but the ui kept breaking. Will try to figure out how to get it working without the calculation


// Gantt chart's x-axis height (time labels at top of chart)
export const GANTT_AXIS_HEIGHT_PX = 36;
Expand Down
101 changes: 101 additions & 0 deletions airflow-core/src/airflow/ui/src/pages/Run/Deadlines.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*!
* 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 { Badge } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";

import { useDeadlinesServiceGetDagRunDeadlines } from "openapi/queries";
import type { DeadlineResponse } from "openapi/requests";
import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import Time from "src/components/Time";

const columns = (translate: TFunction): Array<ColumnDef<DeadlineResponse>> => [
{
accessorKey: "alert_name",
header: translate("deadlines.name"),
id: "alert_name",
},
{
accessorKey: "deadline_time",
cell: ({ row }) => <Time datetime={row.original.deadline_time} />,
header: translate("deadlines.deadlineTime"),
id: "deadline_time",
},
{
accessorKey: "missed",
cell: ({ row }) => (
<Badge colorPalette={row.original.missed ? "red" : "green"}>
{row.original.missed ? translate("deadlines.missed") : translate("deadlines.onTrack")}
</Badge>
),
enableSorting: false,
header: translate("deadlines.status"),
id: "missed",
},
{
accessorKey: "created_at",
cell: ({ row }) => <Time datetime={row.original.created_at} />,
header: translate("deadlines.createdAt"),
id: "created_at",
},
{
accessorKey: "alert_description",
enableSorting: false,
header: translate("deadlines.description"),
id: "alert_description",
},
];

export const Deadlines = () => {
const { t: translate } = useTranslation("dag");
const { dagId = "", runId = "" } = useParams();
const { setTableURLState, tableURLState } = useTableURLState();
const { pagination, sorting } = tableURLState;
const [sort] = sorting;
const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : ["deadline_time"];

const { data, error, isLoading } = useDeadlinesServiceGetDagRunDeadlines(
{
dagId,
dagRunId: runId,
limit: pagination.pageSize,
offset: pagination.pageIndex * pagination.pageSize,
orderBy,
},
undefined,
{ placeholderData: (prev) => prev },
);

return (
<DataTable
columns={columns(translate)}
data={data?.deadlines ?? []}
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isLoading={isLoading}
modelName="common:deadline"
onStateChange={setTableURLState}
total={data?.total_entries}
/>
);
};
3 changes: 2 additions & 1 deletion airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { ReactFlowProvider } from "@xyflow/react";
import { useTranslation } from "react-i18next";
import { FiCode, FiDatabase, FiUser } from "react-icons/fi";
import { MdDetails, MdOutlineEventNote, MdOutlineTask } from "react-icons/md";
import { MdDetails, MdOutlineEventNote, MdOutlineTask, MdOutlineTimer } from "react-icons/md";
import { useParams } from "react-router-dom";

import { useDagRunServiceGetDagRun } from "openapi/queries";
Expand All @@ -44,6 +44,7 @@ export const Run = () => {
{ icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: "events" },
{ icon: <FiCode />, label: translate("tabs.code"), value: "code" },
{ icon: <MdDetails />, label: translate("tabs.details"), value: "details" },
{ icon: <MdOutlineTimer />, label: translate("tabs.deadlines"), value: "deadlines" },
...externalTabs,
];

Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/ui/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Pools } from "src/pages/Pools";
import { Providers } from "src/pages/Providers";
import { Run } from "src/pages/Run";
import { AssetEvents as DagRunAssetEvents } from "src/pages/Run/AssetEvents";
import { Deadlines as RunDeadlines } from "src/pages/Run/Deadlines";
import { Details as DagRunDetails } from "src/pages/Run/Details";
import { Security } from "src/pages/Security";
import { Task } from "src/pages/Task";
Expand Down Expand Up @@ -186,6 +187,7 @@ export const routerConfig = [
{ element: <Code />, path: "code" },
{ element: <DagRunDetails />, path: "details" },
{ element: <DagRunAssetEvents />, path: "asset_events" },
{ element: <RunDeadlines />, path: "deadlines" },
pluginRoute,
],
element: <Run />,
Expand Down
Loading