Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions airflow-core/newsfragments/67586.significant.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add a new **Deadlines** page under the Browse menu.

The page is accessible to any role that already has ``can_read`` and
``menu_access`` on ``DAG Runs``.
1 change: 1 addition & 0 deletions airflow-core/src/airflow/api_fastapi/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class MenuItem(Enum):
CONFIG = "Config"
CONNECTIONS = "Connections"
DAGS = "Dags"
DEADLINES = "Deadlines"
DOCS = "Docs"
JOBS = "Jobs"
PLUGINS = "Plugins"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3144,6 +3144,7 @@ components:
- Config
- Connections
- Dags
- Deadlines
- Docs
- Jobs
- Plugins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9257,7 +9257,7 @@ export const $LightGridTaskInstanceSummary = {

export const $MenuItem = {
type: 'string',
enum: ['Required Actions', 'Assets', 'Audit Log', 'Config', 'Connections', 'Dags', 'Docs', 'Jobs', 'Plugins', 'Pools', 'Providers', 'Variables', 'XComs'],
enum: ['Required Actions', 'Assets', 'Audit Log', 'Config', 'Connections', 'Dags', 'Deadlines', 'Docs', 'Jobs', 'Plugins', 'Pools', 'Providers', 'Variables', 'XComs'],
title: 'MenuItem',
description: 'Define all menu items defined in the menu.'
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2337,7 +2337,7 @@ export type LightGridTaskInstanceSummary = {
/**
* Define all menu items defined in the menu.
*/
export type MenuItem = 'Required Actions' | 'Assets' | 'Audit Log' | 'Config' | 'Connections' | 'Dags' | 'Docs' | 'Jobs' | 'Plugins' | 'Pools' | 'Providers' | 'Variables' | 'XComs';
export type MenuItem = 'Required Actions' | 'Assets' | 'Audit Log' | 'Config' | 'Connections' | 'Dags' | 'Deadlines' | 'Docs' | 'Jobs' | 'Plugins' | 'Pools' | 'Providers' | 'Variables' | 'XComs';

/**
* Menu Item Collection serializer for responses.
Expand Down
18 changes: 18 additions & 0 deletions airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@
},
"title": "Audit Log"
},
"deadlines": {
"columns": {
"alertName": "Alert Name",
"deadlineTime": "Deadline Time",
"status": "Status"
},
"deadline_one": "Deadline",
"deadline_other": "Deadlines",
"filters": {
"status": "Status",
"statusOptions": {
"all": "All",
"missed": "Missed",
"pending": "Pending"
}
},
"title": "Deadlines"
},
"xcom": {
"add": {
"error": "Failed to add XCom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"backfill_other": "Backfills",
"browse": {
"auditLog": "Audit Log",
"deadlines": "Deadlines",
"jobs": "Jobs",
"requiredActions": "Required Actions",
"xcoms": "XComs"
Expand Down
17 changes: 17 additions & 0 deletions airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ export const useFilterConfigs = () => {
min: 1,
type: FilterTypes.NUMBER,
},
[SearchParamsKeys.DEADLINE_TIME_RANGE]: {
endKey: SearchParamsKeys.DEADLINE_TIME_LTE,
icon: <MdDateRange />,
label: translate("browse:deadlines.columns.deadlineTime"),
startKey: SearchParamsKeys.DEADLINE_TIME_GTE,
type: FilterTypes.DATERANGE,
},
[SearchParamsKeys.DURATION_GTE]: {
icon: <MdHourglassEmpty />,
label: translate("common:filters.durationFrom"),
Expand Down Expand Up @@ -209,6 +216,16 @@ export const useFilterConfigs = () => {
min: -1,
type: FilterTypes.NUMBER,
},
[SearchParamsKeys.MISSED]: {
icon: <MdCheckCircle />,
label: translate("browse:deadlines.filters.status"),
options: [
{ label: translate("browse:deadlines.filters.statusOptions.all"), value: "" },
{ label: translate("browse:deadlines.filters.statusOptions.pending"), value: "false" },
{ label: translate("browse:deadlines.filters.statusOptions.missed"), value: "true" },
],
type: FilterTypes.SELECT,
},
[SearchParamsKeys.NAME_PATTERN]: {
hotkeyDisabled: true,
icon: <TaskIcon />,
Expand Down
4 changes: 4 additions & 0 deletions airflow-core/src/airflow/ui/src/constants/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export enum SearchParamsKeys {
DAG_ID_PATTERN = "dag_id_pattern",
DAG_VERSION = "dag_version",
DAG_VIEW = "view",
DEADLINE_TIME_GTE = "deadline_time_gte",
DEADLINE_TIME_LTE = "deadline_time_lte",
DEADLINE_TIME_RANGE = "deadline_time_range",
DEPENDENCIES = "dependencies",
DURATION_GTE = "duration_gte",
DURATION_LTE = "duration_lte",
Expand Down Expand Up @@ -64,6 +67,7 @@ export enum SearchParamsKeys {
LOGICAL_DATE_RANGE = "logical_date_range",
MAP_INDEX = "map_index",
MAPPED = "mapped",
MISSED = "missed",
NAME_PATTERN = "name_pattern",
NEEDS_REVIEW = "needs_review",
OFFSET = "offset",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const links = [
key: "auditLog",
title: "Audit Log",
},
{
href: "/deadlines",
key: "deadlines",
title: "Deadlines",
},
{
href: "/jobs",
key: "jobs",
Expand Down
156 changes: 156 additions & 0 deletions airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*!
* 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, Box, Heading, Link, VStack } from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
import type { TFunction } from "i18next";
import { useTranslation } from "react-i18next";
import { Link as RouterLink, useSearchParams } from "react-router-dom";

import { useDeadlinesServiceGetDeadlines } from "openapi/queries";
import type { DeadlineResponse } from "openapi/requests/types.gen";
import { DataTable } from "src/components/DataTable";
import { useTableURLState } from "src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import { FilterBar } from "src/components/FilterBar";
import Time from "src/components/Time";
import { TruncatedText } from "src/components/TruncatedText";
import { SearchParamsKeys } from "src/constants/searchParams";
import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";

type DeadlineRow = { row: { original: DeadlineResponse } };

const createColumns = (translate: TFunction): Array<ColumnDef<DeadlineResponse>> => [
{
accessorKey: "dag_id",
cell: ({ row: { original } }: DeadlineRow) => (
<Link asChild color="fg.info">
<RouterLink to={`/dags/${original.dag_id}`}>
<TruncatedText text={original.dag_id} />
</RouterLink>
</Link>
),
header: translate("common:dagId"),
},
{
accessorKey: "dag_run_id",
cell: ({ row: { original } }: DeadlineRow) => (
<Link asChild color="fg.info">
<RouterLink to={`/dags/${original.dag_id}/runs/${original.dag_run_id}`}>
<TruncatedText text={original.dag_run_id} />
</RouterLink>
</Link>
),
enableSorting: false,
header: translate("common:dagRunId"),
},
{
accessorKey: "deadline_time",
cell: ({ row: { original } }: DeadlineRow) => <Time datetime={original.deadline_time} />,
header: translate("browse:deadlines.columns.deadlineTime"),
},
{
accessorKey: "missed",
cell: ({
row: {
original: { missed },
},
}) => (
<Badge colorPalette={missed ? "red" : "blue"} size="sm" variant="solid">
{missed
? translate("browse:deadlines.filters.statusOptions.missed")
: translate("browse:deadlines.filters.statusOptions.pending")}
</Badge>
),
header: translate("browse:deadlines.columns.status"),
},
{
accessorKey: "alert_name",
cell: ({ row: { original } }) => original.alert_name ?? "",
enableSorting: false,
header: translate("browse:deadlines.columns.alertName"),
},
{
accessorKey: "created_at",
cell: ({ row: { original } }: DeadlineRow) => <Time datetime={original.created_at} />,
header: translate("common:table.createdAt"),
},
];

const deadlinesFilterKeys: Array<FilterableSearchParamsKeys> = [
SearchParamsKeys.DAG_ID,
SearchParamsKeys.DEADLINE_TIME_RANGE,
SearchParamsKeys.MISSED,
];

export const Deadlines = () => {
const { t: translate } = useTranslation(["browse", "common"]);
const { setTableURLState, tableURLState } = useTableURLState();
const [searchParams] = useSearchParams();

const { filterConfigs, handleFiltersChange, initialValues } = useFiltersHandler(deadlinesFilterKeys);

const columns = createColumns(translate);

const { pagination, sorting } = tableURLState;
const [sort] = sorting;
const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : ["-deadline_time"];

const filteredDagId = searchParams.get(SearchParamsKeys.DAG_ID);
const filteredMissed = searchParams.get(SearchParamsKeys.MISSED);
const deadlineTimeGte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_GTE);
const deadlineTimeLte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_LTE);

const missedFilter = filteredMissed === "true" ? true : filteredMissed === "false" ? false : undefined;

const { data, error, isFetching, isLoading } = useDeadlinesServiceGetDeadlines({
dagId: filteredDagId !== null && filteredDagId !== "" ? filteredDagId : "~",
dagRunId: "~",
deadlineTimeGte: deadlineTimeGte ?? undefined,
deadlineTimeLte: deadlineTimeLte ?? undefined,
limit: pagination.pageSize,
missed: missedFilter,
offset: pagination.pageIndex * pagination.pageSize,
orderBy,
});

return (
<Box p={2}>
<Heading>{translate("browse:deadlines.title")}</Heading>
<VStack align="start" gap={4} paddingY="4px">
<FilterBar
configs={filterConfigs}
initialValues={initialValues}
onFiltersChange={handleFiltersChange}
/>
</VStack>
<DataTable
columns={columns}
data={data?.deadlines ?? []}
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isFetching={isFetching}
isLoading={isLoading}
modelName="browse:deadlines.deadline"
onStateChange={setTableURLState}
Comment thread
imrichardwu marked this conversation as resolved.
showRowCountHeading={false}
total={data?.total_entries}
/>
</Box>
);
};
5 changes: 5 additions & 0 deletions airflow-core/src/airflow/ui/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Tasks } from "src/pages/Dag/Tasks";
import { DagRuns } from "src/pages/DagRuns";
import { DagsList } from "src/pages/DagsList";
import { Dashboard } from "src/pages/Dashboard";
import { Deadlines } from "src/pages/Deadlines";
import { ErrorPage } from "src/pages/Error";
import { Events } from "src/pages/Events";
import { ExternalView } from "src/pages/ExternalView";
Expand Down Expand Up @@ -125,6 +126,10 @@ export const routerConfig = [
element: <Asset />,
path: "assets/:assetId",
},
{
element: <Deadlines />,
path: "deadlines",
},
{
element: <Events />,
path: "events",
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.DAG_ID
| SearchParamsKeys.DAG_ID_PATTERN
| SearchParamsKeys.DAG_VERSION
| SearchParamsKeys.DEADLINE_TIME_RANGE
| SearchParamsKeys.DURATION_GTE
| SearchParamsKeys.DURATION_LTE
| SearchParamsKeys.END_DATE_RANGE
Expand All @@ -78,6 +79,7 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.KEY_PATTERN
| SearchParamsKeys.LOGICAL_DATE_RANGE
| SearchParamsKeys.MAP_INDEX
| SearchParamsKeys.MISSED
| SearchParamsKeys.NAME_PATTERN
| SearchParamsKeys.OPERATOR_NAME_PATTERN
| SearchParamsKeys.PARTITION_KEY_PATTERN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
_MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE[MenuItem.REQUIRED_ACTIONS] = RESOURCE_HITL_DETAIL
_MAP_DAG_ACCESS_ENTITY_TO_FAB_RESOURCE_TYPE[DagAccessEntity.HITL_DETAIL] = (RESOURCE_HITL_DETAIL,)

if hasattr(MenuItem, "DEADLINES"):
_MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE[MenuItem.DEADLINES] = RESOURCE_DAG_RUN


class FabAuthManager(BaseAuthManager[User]):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,17 @@ def test_is_authorized_custom_view(
[(ACTION_CAN_ACCESS_MENU, RESOURCE_AUDIT_LOG), (ACTION_CAN_READ, RESOURCE_VARIABLE)],
[MenuItem.AUDIT_LOG],
),
*(
[
(
[MenuItem.DEADLINES],
[(ACTION_CAN_ACCESS_MENU, RESOURCE_DAG_RUN)],
[MenuItem.DEADLINES],
)
]
if hasattr(MenuItem, "DEADLINES")
else []
),
(
[],
[],
Expand Down
Loading