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 02d94c5f0e Migration of all DAG details to existing grid view dag
details panel (#31690)
02d94c5f0e is described below
commit 02d94c5f0e2ca4571cdfbde2a8624dd4569876c7
Author: Akash Sharma <[email protected]>
AuthorDate: Tue Jun 6 22:03:27 2023 +0530
Migration of all DAG details to existing grid view dag details panel
(#31690)
* dag details added in grid view
* dag details data integrated and all links added
* deleted old dag_details and all refs
* reverted view.py and bug resolution on object parsing
* resolved bug while combining dag and dagDetails
* failing tests fixed
* Update airflow/www/static/js/dag/details/Dag.tsx
Co-authored-by: Brent Bovenzi <[email protected]>
* Update airflow/www/static/js/components/ViewScheduleInterval.tsx
Co-authored-by: Brent Bovenzi <[email protected]>
* code clean and static checks fixed
---------
Co-authored-by: Brent Bovenzi <[email protected]>
---
airflow/www/static/js/api/index.ts | 2 +
airflow/www/static/js/api/useDagDetails.ts | 53 ++++++++
.../static/js/components/ViewScheduleInterval.tsx | 61 +++++++++
airflow/www/static/js/dag/details/Dag.tsx | 148 +++++++++++++++++++--
airflow/www/static/js/utils/index.ts | 9 ++
airflow/www/templates/airflow/dag.html | 3 +-
airflow/www/templates/airflow/dags.html | 4 -
7 files changed, 267 insertions(+), 13 deletions(-)
diff --git a/airflow/www/static/js/api/index.ts
b/airflow/www/static/js/api/index.ts
index cd7b9dd5af..94ec52e863 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -42,6 +42,7 @@ import useUpstreamDatasetEvents from
"./useUpstreamDatasetEvents";
import useTaskInstance from "./useTaskInstance";
import useDag from "./useDag";
import useDagCode from "./useDagCode";
+import useDagDetails from "./useDagDetails";
import useHealth from "./useHealth";
import usePools from "./usePools";
import useDags from "./useDags";
@@ -59,6 +60,7 @@ export {
useClearTask,
useDag,
useDagCode,
+ useDagDetails,
useDagRuns,
useDags,
useDataset,
diff --git a/airflow/www/static/js/api/useDagDetails.ts
b/airflow/www/static/js/api/useDagDetails.ts
new file mode 100644
index 0000000000..16c8e4397c
--- /dev/null
+++ b/airflow/www/static/js/api/useDagDetails.ts
@@ -0,0 +1,53 @@
+/*!
+ * 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 axios, { AxiosResponse } from "axios";
+import { useQuery } from "react-query";
+
+import { getMetaValue } from "src/utils";
+import type { API } from "src/types";
+import type { DAG, DAGDetail } from "src/types/api-generated";
+import useDag from "./useDag";
+
+const dagDetailsApiUrl = getMetaValue("dag_details_api");
+
+const combineResults = (
+ dagData: DAG,
+ dagDetailsData: DAGDetail
+): Omit<DAGDetail, "defaultView"> => ({ ...dagData, ...dagDetailsData });
+
+const useDagDetails = () => {
+ const { data: dagData } = useDag();
+ const dagDetailsResult = useQuery(
+ ["dagDetailsQuery"],
+ () => axios.get<AxiosResponse, API.DAGDetail>(dagDetailsApiUrl),
+ {
+ enabled: !!dagData,
+ }
+ );
+ return {
+ ...dagDetailsResult,
+ data: combineResults(
+ dagData || {},
+ dagDetailsResult.data ? dagDetailsResult.data : {}
+ ),
+ };
+};
+
+export default useDagDetails;
diff --git a/airflow/www/static/js/components/ViewScheduleInterval.tsx
b/airflow/www/static/js/components/ViewScheduleInterval.tsx
new file mode 100644
index 0000000000..68bc5cf1f5
--- /dev/null
+++ b/airflow/www/static/js/components/ViewScheduleInterval.tsx
@@ -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 React from "react";
+import { Flex, Text } from "@chakra-ui/react";
+
+interface Props {
+ data: Record<string, number | null>;
+}
+
+const ViewScheduleInterval = ({ data }: Props) => {
+ const genericTimeDeltaUnits = [
+ "days",
+ "day",
+ "seconds",
+ "microseconds",
+ "years",
+ "year",
+ "months",
+ "month",
+ "leapdays",
+ "hours",
+ "hour",
+ "minutes",
+ "minute",
+ "second",
+ "microsecond",
+ ];
+
+ return (
+ <Flex flexWrap="wrap">
+ {!!data &&
+ genericTimeDeltaUnits.map(
+ (unit) =>
+ !!data[unit] && (
+ <Text mr={1} key={unit}>
+ {data[unit]} {unit}
+ </Text>
+ )
+ )}
+ </Flex>
+ );
+};
+
+export default ViewScheduleInterval;
diff --git a/airflow/www/static/js/dag/details/Dag.tsx
b/airflow/www/static/js/dag/details/Dag.tsx
index 098eb47dbd..836c8960b3 100644
--- a/airflow/www/static/js/dag/details/Dag.tsx
+++ b/airflow/www/static/js/dag/details/Dag.tsx
@@ -29,23 +29,32 @@ import {
Heading,
Text,
Box,
+ Badge,
+ Code,
} from "@chakra-ui/react";
-import { mean } from "lodash";
+import { mean, omit } from "lodash";
import { getDuration, formatDuration } from "src/datetime_utils";
import {
+ appendSearchParams,
finalStatesMap,
getMetaValue,
getTaskSummary,
+ toSentenceCase,
useOffsetTop,
} from "src/utils";
-import { useGridData } from "src/api";
+import { useGridData, useDagDetails } from "src/api";
import Time from "src/components/Time";
+import ViewScheduleInterval from "src/components/ViewScheduleInterval";
import type { TaskState } from "src/types";
+import type { DAG, DAGDetail } from "src/types/api-generated";
+import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
import { SimpleStatus } from "../StatusBox";
-const dagDetailsUrl = getMetaValue("dag_details_url");
+const dagId = getMetaValue("dag_id");
+const tagIndexUrl = getMetaValue("tag_index_url");
+const taskInstancesUrl = getMetaValue("task_instances_list_url");
const Dag = () => {
const {
@@ -54,9 +63,32 @@ const Dag = () => {
const detailsRef = useRef<HTMLDivElement>(null);
const offsetTop = useOffsetTop(detailsRef);
+ const { data: dagDetailsData, isLoading: isLoadingDagDetails } =
+ useDagDetails();
+
+ // fields to exclude from "dagDetailsData" since handled seprately or not
required
+ const dagDataExcludeFields = [
+ "defaultView",
+ "fileToken",
+ "scheduleInterval",
+ "tags",
+ "owners",
+ "params",
+ ];
+
+ const listParams = new URLSearchParamsWrapper({
+ _flt_3_dag_id: dagId,
+ });
+
+ const getRedirectUri = (state: string): string => {
+ listParams.set("_flt_3_state", state);
+ return appendSearchParams(taskInstancesUrl, listParams);
+ };
+
const taskSummary = getTaskSummary({ task: groups });
const numMap = finalStatesMap();
const durations: number[] = [];
+
dagRuns.forEach((dagRun) => {
durations.push(getDuration(dagRun.startDate, dagRun.endDate));
const stateKey = dagRun.state == null ? "no_status" : dagRun.state;
@@ -72,8 +104,15 @@ const Dag = () => {
<Tr key={val}>
<Td>
<Flex alignItems="center">
- <SimpleStatus state={val as TaskState} mr={2} />
- <Text>Total {val}</Text>
+ <SimpleStatus state={val as TaskState} />
+ <Link
+ href={getRedirectUri(val)}
+ title={`View all ${val} DAGS`}
+ color="blue"
+ ml="5px"
+ >
+ Total {val}
+ </Link>
</Flex>
</Td>
<Td>{key}</Td>
@@ -89,6 +128,28 @@ const Dag = () => {
const firstStart = dagRuns[0]?.startDate;
const lastStart = dagRuns[dagRuns.length - 1]?.startDate;
+ // parse value for each key if date or not
+ const parseStringData = (value: string) =>
+ Number.isNaN(Date.parse(value)) ? value : <Time dateTime={value} />;
+
+ // render dag and dag_details data
+ const renderDagDetailsData = (
+ data: DAG | DAGDetail,
+ excludekeys: Array<string>
+ ) => (
+ <>
+ {Object.entries(data).map(
+ ([key, value]) =>
+ !excludekeys.includes(key) && (
+ <Tr key={key}>
+ <Td>{toSentenceCase(key)}</Td>
+ <Td>{parseStringData(String(value))}</Td>
+ </Tr>
+ )
+ )}
+ </>
+ );
+
return (
<Box
height="100%"
@@ -96,9 +157,6 @@ const Dag = () => {
ref={detailsRef}
overflowY="auto"
>
- <Button as={Link} variant="ghost" colorScheme="blue"
href={dagDetailsUrl}>
- More Details
- </Button>
<Table variant="striped">
<Tbody>
{durations.length > 0 && (
@@ -169,6 +227,80 @@ const Dag = () => {
<Td>{value}</Td>
</Tr>
))}
+ {!isLoadingDagDetails && !!dagDetailsData && (
+ <>
+ <Tr borderBottomWidth={2} borderBottomColor="gray.300">
+ <Td>
+ <Heading size="sm">DAG Details</Heading>
+ </Td>
+ <Td />
+ </Tr>
+ {renderDagDetailsData(dagDetailsData, dagDataExcludeFields)}
+ <Tr>
+ <Td>Owners</Td>
+ <Td>
+ <Flex flexWrap="wrap">
+ {dagDetailsData.owners?.map((owner) => (
+ <Badge key={owner} colorScheme="blue">
+ {owner}
+ </Badge>
+ ))}
+ </Flex>
+ </Td>
+ </Tr>
+ <Tr>
+ <Td>Tags</Td>
+ <Td>
+ {!!dagDetailsData.tags && dagDetailsData.tags?.length > 0 ? (
+ <Flex flexWrap="wrap">
+ {dagDetailsData.tags?.map((tag) => (
+ <Button
+ key={tag.name}
+ as={Link}
+ colorScheme="teal"
+ size="xs"
+ href={tagIndexUrl.replace(
+ "_TAG_NAME_",
+ tag?.name || ""
+ )}
+ mr={3}
+ >
+ {tag.name}
+ </Button>
+ ))}
+ </Flex>
+ ) : (
+ "No tags"
+ )}
+ </Td>
+ </Tr>
+ <Tr>
+ <Td>Schedule interval</Td>
+ <Td>
+ {dagDetailsData.scheduleInterval?.type ===
+ "CronExpression" ? (
+ <Text>{dagDetailsData.scheduleInterval?.value}</Text>
+ ) : (
+ // for TimeDelta and RelativeDelta
+ <ViewScheduleInterval
+ data={omit(dagDetailsData.scheduleInterval, [
+ "type",
+ "value",
+ ])}
+ />
+ )}
+ </Td>
+ </Tr>
+ <Tr>
+ <Td>Params</Td>
+ <Td>
+ <Code width="100%">
+ <pre>{JSON.stringify(dagDetailsData.params, null, 2)}</pre>
+ </Code>
+ </Td>
+ </Tr>
+ </>
+ )}
</Tbody>
</Table>
</Box>
diff --git a/airflow/www/static/js/utils/index.ts
b/airflow/www/static/js/utils/index.ts
index 7526adfff7..37dc5cb022 100644
--- a/airflow/www/static/js/utils/index.ts
+++ b/airflow/www/static/js/utils/index.ts
@@ -177,6 +177,14 @@ const getStatusBackgroundColor = (color: string, hasNote:
boolean) =>
? `linear-gradient(-135deg, ${Color(color).hex()}60 5px, ${color} 0);`
: color;
+const toSentenceCase = (camelCase: string): string => {
+ if (camelCase) {
+ const result = camelCase.replace(/([A-Z])/g, " $1");
+ return result[0].toUpperCase() + result.substring(1).toLowerCase();
+ }
+ return "";
+};
+
export {
hoverDelay,
finalStatesMap,
@@ -188,4 +196,5 @@ export {
getDagRunLabel,
getStatusBackgroundColor,
useOffsetTop,
+ toSentenceCase,
};
diff --git a/airflow/www/templates/airflow/dag.html
b/airflow/www/templates/airflow/dag.html
index 3a2c448f20..66531c607e 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -60,7 +60,6 @@
<meta name="grid_url" content="{{ url_for('Airflow.grid', dag_id=dag.dag_id)
}}">
<meta name="datasets_url" content="{{ url_for('Airflow.datasets') }}">
<meta name="grid_url_no_root" content="{{ url_for('Airflow.grid',
dag_id=dag.dag_id, num_runs=num_runs_arg, base_date=base_date_arg) }}">
- <meta name="dag_details_url" content="{{ url_for('Airflow.dag_details',
dag_id=dag.dag_id) }}">
<meta name="graph_url" content="{{ url_for('Airflow.graph',
dag_id=dag.dag_id, root=root) }}">
<meta name="task_url" content="{{ url_for('Airflow.task', dag_id=dag.dag_id)
}}">
<meta name="log_url" content="{{ url_for('Airflow.log', dag_id=dag.dag_id)
}}">
@@ -68,6 +67,7 @@
<meta name="rendered_templates_url" content="{{
url_for('Airflow.rendered_templates', dag_id=dag.dag_id) }}">
<meta name="rendered_k8s_url" content="{{ url_for('Airflow.rendered_k8s',
dag_id=dag.dag_id) }}">
<meta name="task_instances_list_url" content="{{
url_for('TaskInstanceModelView.list') }}">
+ <meta name="tag_index_url" content="{{ url_for('Airflow.index',
tags='_TAG_NAME_') }}">
<meta name="mapped_instances_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_mapped_task_instances',
dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_') }}">
<meta name="task_log_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_log_endpoint_get_log',
dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_',
task_try_number='-1') }}">
<meta name="upstream_dataset_events_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_dag_run_endpoint_get_upstream_dataset_events',
dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_') }}">
@@ -77,6 +77,7 @@
<meta name="set_dag_run_note" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_dag_run_endpoint_set_dag_run_note',
dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_') }}">
<meta name="dag_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_dag_endpoint_get_dag',
dag_id=dag.dag_id) }}">
<meta name="dag_source_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_dag_source_endpoint_get_dag_source',
file_token='_FILE_TOKEN_') }}">
+ <meta name="dag_details_api" content="{{
url_for('/api/v1.airflow_api_connexion_endpoints_dag_endpoint_get_dag_details',
dag_id=dag.dag_id) }}">
<!-- End Urls -->
<meta name="is_paused" content="{{ dag_is_paused }}">
diff --git a/airflow/www/templates/airflow/dags.html
b/airflow/www/templates/airflow/dags.html
index 393d511f19..80b37f6a42 100644
--- a/airflow/www/templates/airflow/dags.html
+++ b/airflow/www/templates/airflow/dags.html
@@ -387,10 +387,6 @@
<span class="material-icons"
aria-hidden="true">code</span>
Code
</a>
- <a href="{{ url_for('Airflow.dag_details',
dag_id=dag.dag_id) }}" class="dags-table-more__link">
- <span class="material-icons"
aria-hidden="true">details</span>
- Details
- </a>
<a href="{{ url_for('Airflow.gantt', dag_id=dag.dag_id)
}}" class="dags-table-more__link">
<span class="material-icons"
aria-hidden="true">vertical_distribute</span>
Gantt