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 0fbb05751a Remove old gantt chart and redirect to grid views gantt tab
(#32908)
0fbb05751a is described below
commit 0fbb05751acf6061f9f6fa83ee53816d568055d0
Author: Brent Bovenzi <[email protected]>
AuthorDate: Sat Jul 29 04:10:55 2023 +0800
Remove old gantt chart and redirect to grid views gantt tab (#32908)
* Remove old gantt chart and redirect to grid views gantt tab
* Add ts-ignore for some reason
---
LICENSE | 1 -
airflow/www/static/css/gantt.css | 50 ---
airflow/www/static/js/api/useDatasets.ts | 1 +
.../static/js/cluster-activity/nav/FilterBar.tsx | 2 +
.../www/static/js/cluster-activity/useFilters.tsx | 3 +
airflow/www/static/js/dag/details/gantt/index.tsx | 1 +
airflow/www/static/js/dag/nav/FilterBar.tsx | 1 +
airflow/www/static/js/dag/useFilters.test.tsx | 1 +
airflow/www/static/js/dag/useFilters.tsx | 1 +
airflow/www/static/js/gantt.js | 431 ---------------------
airflow/www/templates/airflow/gantt.html | 83 ----
airflow/www/views.py | 97 +----
airflow/www/webpack.config.js | 1 -
docs/apache-airflow/img/gantt.png | Bin 70606 -> 352148 bytes
licenses/LICENSE-moment-strftime.txt | 9 -
tests/www/views/test_views_extra_links.py | 30 +-
...st_views_graph_gantt.py => test_views_graph.py} | 1 -
17 files changed, 15 insertions(+), 698 deletions(-)
diff --git a/LICENSE b/LICENSE
index 81899d0fee..b83a80d26d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -237,7 +237,6 @@ The text of each license is also included at
licenses/LICENSE-[project].txt.
(MIT License) normalize.css v3.0.2
(http://necolas.github.io/normalize.css/)
(MIT License) ElasticMock v1.3.2 (https://github.com/vrcmarcos/elasticmock)
(MIT License) MomentJS v2.24.0 (http://momentjs.com/)
- (MIT License) moment-strftime v0.5.0
(https://github.com/benjaminoakes/moment-strftime)
(MIT License) eonasdan-bootstrap-datetimepicker v4.17.49
(https://github.com/eonasdan/bootstrap-datetimepicker/)
========================================================================
diff --git a/airflow/www/static/css/gantt.css b/airflow/www/static/css/gantt.css
deleted file mode 100644
index ab076e5dc5..0000000000
--- a/airflow/www/static/css/gantt.css
+++ /dev/null
@@ -1,50 +0,0 @@
-/*!
- * 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.
- */
-
-.axis path,
-.axis line {
- fill: none;
- stroke: #000;
- shape-rendering: crispEdges;
-}
-
-rect {
- stroke: #000;
- cursor: pointer;
-}
-
-/* Creates a small triangle extender for the tooltip */
-.d3-tip::after {
- box-sizing: border-box;
- display: inline;
- width: 100%;
- line-height: 1;
- color: rgba(0, 0, 0, 0.8);
- content: "\25BC";
- pointer-events: none;
- position: absolute;
- text-align: center;
-}
-
-/* Style northward tooltips differently */
-.d3-tip.n::after {
- margin: -1px 0 0 0;
- top: 100%;
- left: 0;
-}
diff --git a/airflow/www/static/js/api/useDatasets.ts
b/airflow/www/static/js/api/useDatasets.ts
index 62d2583d90..6150f51ce3 100644
--- a/airflow/www/static/js/api/useDatasets.ts
+++ b/airflow/www/static/js/api/useDatasets.ts
@@ -58,6 +58,7 @@ export default function useDatasets({
const updatedAfterParam =
updatedAfter && updatedAfter.count && updatedAfter.unit
? {
+ // @ts-ignore
updated_after: moment()
.subtract(updatedAfter.count, updatedAfter.unit)
.toISOString(),
diff --git a/airflow/www/static/js/cluster-activity/nav/FilterBar.tsx
b/airflow/www/static/js/cluster-activity/nav/FilterBar.tsx
index 29fa456df4..10cc6813f1 100644
--- a/airflow/www/static/js/cluster-activity/nav/FilterBar.tsx
+++ b/airflow/www/static/js/cluster-activity/nav/FilterBar.tsx
@@ -35,7 +35,9 @@ const FilterBar = () => {
useFilters();
const { timezone } = useTimezone();
+ // @ts-ignore
const startDate = moment(filters.startDate);
+ // @ts-ignore
const endDate = moment(filters.endDate);
const formattedStartDate = startDate.tz(timezone).format(isoFormatWithoutTZ);
const formattedEndDate = endDate.tz(timezone).format(isoFormatWithoutTZ);
diff --git a/airflow/www/static/js/cluster-activity/useFilters.tsx
b/airflow/www/static/js/cluster-activity/useFilters.tsx
index 414745d0de..6e30683e3a 100644
--- a/airflow/www/static/js/cluster-activity/useFilters.tsx
+++ b/airflow/www/static/js/cluster-activity/useFilters.tsx
@@ -52,6 +52,7 @@ const useFilters = (): FilterHookReturn => {
const endDate = searchParams.get(END_DATE_PARAM) || now;
const startDate =
searchParams.get(START_DATE_PARAM) ||
+ // @ts-ignore
moment(endDate).subtract(1, "d").toISOString();
const makeOnChangeFn =
@@ -68,10 +69,12 @@ const useFilters = (): FilterHookReturn => {
const onStartDateChange = makeOnChangeFn(
START_DATE_PARAM,
+ // @ts-ignore
(localDate: string) => moment(localDate).utc().format()
);
const onEndDateChange = makeOnChangeFn(END_DATE_PARAM, (localDate: string) =>
+ // @ts-ignore
moment(localDate).utc().format()
);
diff --git a/airflow/www/static/js/dag/details/gantt/index.tsx
b/airflow/www/static/js/dag/details/gantt/index.tsx
index 931c48b566..d881543a0d 100644
--- a/airflow/www/static/js/dag/details/gantt/index.tsx
+++ b/airflow/www/static/js/dag/details/gantt/index.tsx
@@ -131,6 +131,7 @@ const Gantt = ({ openGroupIds, gridScrollRef,
ganttScrollRef }: Props) => {
ml={-9}
>
<Time
+ // @ts-ignore
dateTime={moment(startDate)
.add(i * intervals, "milliseconds")
.format()}
diff --git a/airflow/www/static/js/dag/nav/FilterBar.tsx
b/airflow/www/static/js/dag/nav/FilterBar.tsx
index f76e20b270..9078fe705a 100644
--- a/airflow/www/static/js/dag/nav/FilterBar.tsx
+++ b/airflow/www/static/js/dag/nav/FilterBar.tsx
@@ -46,6 +46,7 @@ const FilterBar = () => {
} = useFilters();
const { timezone } = useTimezone();
+ // @ts-ignore
const time = moment(filters.baseDate);
const formattedTime = time.tz(timezone).format(isoFormatWithoutTZ);
diff --git a/airflow/www/static/js/dag/useFilters.test.tsx
b/airflow/www/static/js/dag/useFilters.test.tsx
index 8081dad20e..ce4a03d7b3 100644
--- a/airflow/www/static/js/dag/useFilters.test.tsx
+++ b/airflow/www/static/js/dag/useFilters.test.tsx
@@ -73,6 +73,7 @@ describe("Test useFilters hook", () => {
{
fnName: "onBaseDateChange" as keyof UtilFunctions,
paramName: "baseDate" as keyof Filters,
+ // @ts-ignore
paramValue: moment.utc().format(),
},
{
diff --git a/airflow/www/static/js/dag/useFilters.tsx
b/airflow/www/static/js/dag/useFilters.tsx
index 26f97dca54..7b8b998aac 100644
--- a/airflow/www/static/js/dag/useFilters.tsx
+++ b/airflow/www/static/js/dag/useFilters.tsx
@@ -100,6 +100,7 @@ const useFilters = (): FilterHookReturn => {
const onBaseDateChange = makeOnChangeFn(
BASE_DATE_PARAM,
+ // @ts-ignore
(localDate: string) => moment(localDate).utc().format()
);
const onNumRunsChange = makeOnChangeFn(NUM_RUNS_PARAM);
diff --git a/airflow/www/static/js/gantt.js b/airflow/www/static/js/gantt.js
deleted file mode 100644
index 2b7cf10c81..0000000000
--- a/airflow/www/static/js/gantt.js
+++ /dev/null
@@ -1,431 +0,0 @@
-/*!
- * 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.
- */
-/**
- * @author Dimitry Kudrayvtsev
- * @version 2.1
- * @modifiedby Maxime Beauchemin
- */
-
-// Taken from
-//
https://github.com/benjaminoakes/moment-strftime/blob/1886cabc4b07d13e3046ae075d357e7aad92ea93/lib/moment-strftime.js
-// but I couldn't work out how to make webpack not include moment again.
-// TODO: revisit our webpack config
-//
-// -- Begin moment-strftime
-// Copyright (c) 2012 Benjamin Oakes, MIT Licensed
-
-/* global d3, document, moment, data $ */
-
-import tiTooltip, { taskQueuedStateTooltip } from "./task_instances";
-import callModal from "./callModal";
-
-const replacements = {
- a: "ddd",
- A: "dddd",
- b: "MMM",
- B: "MMMM",
- c: "lll",
- d: "DD",
- "-d": "D",
- e: "D",
- F: "YYYY-MM-DD",
- H: "HH",
- "-H": "H",
- I: "hh",
- "-I": "h",
- j: "DDDD",
- "-j": "DDD",
- k: "H",
- l: "h",
- m: "MM",
- "-m": "M",
- M: "mm",
- "-M": "m",
- p: "A",
- P: "a",
- S: "ss",
- "-S": "s",
- u: "E",
- w: "d",
- W: "WW",
- x: "ll",
- X: "LTS",
- y: "YY",
- Y: "YYYY",
- z: "ZZ",
- Z: "z",
- f: "SSS",
- "%": "%",
-};
-
-moment.fn.strftime = function formatTime(format) {
- // Break up format string based on strftime tokens
- const tokens = format.split(/(%-?.)/);
- const momentFormat = tokens
- .map((token) => {
- // Replace strftime tokens with moment formats
- if (
- token[0] === "%" &&
- !!Object.getOwnPropertyDescriptor(replacements, token.substr(1))
- ) {
- return replacements[token.substr(1)];
- }
- // Escape non-token strings to avoid accidental formatting
- return token.length > 0 ? `[${token}]` : token;
- })
- .join("");
-
- return this.format(momentFormat);
-};
-// -- End moment-strftime
-
-d3.gantt = () => {
- const FIT_TIME_DOMAIN_MODE = "fit";
- const executionTip = d3
- .tip()
- .attr("class", "tooltip d3-tip")
- .offset([-10, 0])
- .html((d) => tiTooltip(d, null, { includeTryNumber: true }));
-
- const queuedStateTip = d3
- .tip()
- .attr("class", "tooltip d3-tip")
- .offset([-10, 0])
- .html((d) => taskQueuedStateTooltip(d));
-
- let margin = {
- top: 20,
- right: 40,
- bottom: 20,
- left: 150,
- };
- const yAxisLeftOffset = 220;
- let selector = "body";
- let timeDomainStart = d3.time.day.offset(new Date(), -3);
- let timeDomainEnd = d3.time.hour.offset(new Date(), +3);
- let timeDomainMode = FIT_TIME_DOMAIN_MODE; // fixed or fit
- let taskTypes = [];
- let height = document.body.clientHeight - margin.top - margin.bottom - 5;
- let width = $(".gantt").width() - margin.right - margin.left - 5;
-
- let tickFormat = "%H:%M";
-
- const keyFunction = (d) => d.start_date + d.task_id + d.end_date;
- const filterTaskWithValidQueuedDttm = (tasks) =>
- tasks.filter((d) => !!d.queued_dttm);
-
- let x = d3.time
- .scale()
- .domain([timeDomainStart, timeDomainEnd])
- .range([0, width - yAxisLeftOffset])
- .clamp(true);
-
- let y = d3.scale
- .ordinal()
- .domain(taskTypes)
- .rangeRoundBands([0, height - margin.top - margin.bottom], 0.1);
-
- const rectTransform = (d) =>
- `translate(${x(d.start_date.valueOf()) +
yAxisLeftOffset},${y(d.task_id)})`;
- const queuedRectTransform = (d) =>
- `translate(${x(d.queued_dttm.valueOf()) + yAxisLeftOffset},${y(
- d.task_id
- )})`;
-
- // We can't use d3.time.format as that uses local time, so instead we use
- // moment as that handles our "global" timezone.
- const tickFormatter = (d) => moment(d).strftime(tickFormat);
-
- let xAxis = d3.svg
- .axis()
- .scale(x)
- .orient("bottom")
- .tickFormat(tickFormatter)
- .tickSubdivide(true)
- .tickSize(8)
- .tickPadding(8);
-
- let yAxis = d3.svg.axis().scale(y).orient("left").tickSize(0);
-
- const initTimeDomain = (tasks) => {
- if (timeDomainMode === FIT_TIME_DOMAIN_MODE) {
- if (tasks === undefined || tasks.length < 1) {
- timeDomainStart = d3.time.day.offset(new Date(), -3);
- timeDomainEnd = d3.time.hour.offset(new Date(), +3);
- return;
- }
-
- tasks.forEach((a) => {
- if (!(a.start_date instanceof moment)) {
- // eslint-disable-next-line no-param-reassign
- a.start_date = moment(a.start_date);
- }
- if (!(a.end_date instanceof moment)) {
- // eslint-disable-next-line no-param-reassign
- a.end_date = moment(a.end_date);
- }
- if (a.queued_dttm && !(a.queued_dttm instanceof moment)) {
- // eslint-disable-next-line no-param-reassign
- a.queued_dttm = moment(a.queued_dttm);
- }
- });
- timeDomainEnd = moment.max(tasks.map((a) => a.end_date)).valueOf();
- timeDomainStart = moment
- .min(
- tasks.map((a) => {
- if (a.queued_dttm) {
- return moment.min([a.queued_dttm, a.start_date]);
- }
- return a.start_date;
- })
- )
- .valueOf();
- }
- };
-
- const initAxis = () => {
- x = d3.time
- .scale()
- .domain([timeDomainStart, timeDomainEnd])
- .range([0, width - yAxisLeftOffset])
- .clamp(true);
- y = d3.scale
- .ordinal()
- .domain(taskTypes)
- .rangeRoundBands([0, height - margin.top - margin.bottom], 0.1);
- xAxis = d3.svg
- .axis()
- .scale(x)
- .orient("bottom")
- .tickFormat(tickFormatter)
- .tickSubdivide(true)
- .tickSize(8)
- .tickPadding(8);
-
- yAxis = d3.svg.axis().scale(y).orient("left").tickSize(0);
- };
-
- function gantt(tasks) {
- initTimeDomain(tasks);
- initAxis();
-
- const svg = d3
- .select(selector)
- .append("svg")
- .attr("class", "chart")
- .attr("width", width + margin.left + margin.right)
- .attr("height", height + margin.top + margin.bottom)
- .append("g")
- .attr("class", "gantt-chart")
- .attr("width", width + margin.left + margin.right)
- .attr("height", height + margin.top + margin.bottom)
- .attr("transform", `translate(${margin.left}, ${margin.top})`);
-
- // Draw all task instances with their corresponding states as boxes in
gantt chart.
- svg
- .selectAll(".chart")
- .data(tasks, keyFunction)
- .enter()
- .append("rect")
- .on("mouseover", executionTip.show)
- .on("mouseout", executionTip.hide)
- .on("click", (d) => {
- callModal({
- taskId: d.task_id,
- executionDate: d.execution_date,
- extraLinks: d.extraLinks,
- dagRunId: d.run_id,
- mapIndex: d.map_index,
- });
- })
- .attr("class", (d) => `${d.state || "null"} all-tasks`)
- .attr("y", 0)
- .attr("transform", rectTransform)
- .attr("height", () => y.rangeBand())
- .attr("width", (d) =>
- d3.max([x(d.end_date.valueOf()) - x(d.start_date.valueOf()), 1])
- );
-
- // Draw queued states of task instances with valid queued date time as
boxes in gantt chart.
- svg
- .selectAll(".chart")
- .data(filterTaskWithValidQueuedDttm(tasks), keyFunction)
- .enter()
- .append("rect")
- .on("mouseover", queuedStateTip.show)
- .on("mouseout", queuedStateTip.hide)
- .attr("class", "queued tasks-with-queued-dttm")
- .attr("y", 0)
- .attr("transform", queuedRectTransform)
- .attr("height", () => y.rangeBand())
- .attr("width", (d) =>
- d3.max([x(d.start_date.valueOf()) - x(d.queued_dttm.valueOf()), 1])
- );
-
- svg
- .append("g")
- .attr("class", "x axis")
- .attr(
- "transform",
- `translate(${yAxisLeftOffset}, ${height - margin.top - margin.bottom})`
- )
- .transition()
- .call(xAxis);
-
- svg
- .append("g")
- .attr("class", "y axis")
- .transition()
- .attr("transform", `translate(${yAxisLeftOffset}, 0)`)
- .call(yAxis);
- svg.call(executionTip);
- svg.call(queuedStateTip);
-
- return gantt;
- }
-
- gantt.redraw = (tasks) => {
- initTimeDomain(tasks);
- initAxis();
-
- const svg = d3.select(".chart");
-
- const ganttChartGroup = svg.select(".gantt-chart");
- const rect = ganttChartGroup
- .selectAll(".all-tasks")
- .data(tasks, keyFunction);
- // Redraw all task instances with their corresponding states as boxes in
gantt chart.
- rect
- .enter()
- .insert("rect", ":first-child")
- .attr("rx", 5)
- .attr("ry", 5)
- .attr("class", (d) => d.state || "null")
- .transition()
- .attr("y", 0)
- .attr("transform", rectTransform)
- .attr("height", () => y.rangeBand())
- .attr("width", (d) =>
- d3.max([x(d.end_date.valueOf()) - x(d.start_date.valueOf()), 1])
- );
-
- const queuedStateRect = ganttChartGroup
- .selectAll(".tasks-with-queued-dttm")
- .data(filterTaskWithValidQueuedDttm(tasks), keyFunction);
- // Redraw queued states of task instances with valid queued date time as
boxes in gantt chart.
- queuedStateRect
- .enter()
- .insert("rect", ":first-child")
- .attr("rx", 5)
- .attr("ry", 5)
- .attr("class", "queued")
- .transition()
- .attr("y", 0)
- .attr("transform", queuedRectTransform)
- .attr("height", () => y.rangeBand())
- .attr("width", (d) =>
- d3.max([x(d.start_date.valueOf()) - x(d.queued_dttm.valueOf()), 1])
- );
-
- rect.exit().remove();
- queuedStateRect.exit().remove();
-
- svg.select(".x").transition().call(xAxis);
- svg.select(".y").transition().call(yAxis);
-
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.margin = function (value) {
- if (!arguments.length) return margin;
- margin = value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.timeDomain = function (value) {
- if (!arguments.length) return [timeDomainStart, timeDomainEnd];
- timeDomainStart = +value[0];
- timeDomainEnd = +value[1];
- return gantt;
- };
-
- /**
- * @param {string}
- * vale The value can be "fit" - the domain fits the data or
- * "fixed" - fixed domain.
- */
- // eslint-disable-next-line func-names
- gantt.timeDomainMode = function (value) {
- if (!arguments.length) return timeDomainMode;
- timeDomainMode = value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.taskTypes = function (value) {
- if (!arguments.length) return taskTypes;
- taskTypes = value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.width = function (value) {
- if (!arguments.length) return width;
- width = +value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.height = function (value) {
- if (!arguments.length) return height;
- height = +value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.tickFormat = function (value) {
- if (!arguments.length) return tickFormat;
- tickFormat = value;
- return gantt;
- };
-
- // eslint-disable-next-line func-names
- gantt.selector = function (value) {
- if (!arguments.length) return selector;
- selector = value;
- return gantt;
- };
-
- return gantt;
-};
-
-document.addEventListener("DOMContentLoaded", () => {
- const gantt = d3
- .gantt()
- .taskTypes(data.taskNames)
- .height(data.height)
- .selector(".gantt")
- .tickFormat("%H:%M:%S");
- gantt(data.tasks);
- $("body").on("airflow.timezone-change", () => {
- gantt.redraw(data.tasks);
- });
-});
diff --git a/airflow/www/templates/airflow/gantt.html
b/airflow/www/templates/airflow/gantt.html
deleted file mode 100644
index 11e59ccb30..0000000000
--- a/airflow/www/templates/airflow/gantt.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{#
- 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.
-#}
-
-{% extends "airflow/dag.html" %}
-
-{% block page_title %}{{ dag.dag_id }} - Gantt - {{ appbuilder.app_name }}{%
endblock %}
-
-{% block head_css %}
- {{ super() }}
- <link rel="stylesheet" type="text/css" href="{{ url_for_asset('gantt.css')
}}">
- <link rel="stylesheet" type="text/css" href="{{ url_for_asset('chart.css')
}}">
- <style type="text/css">
- {% for state, state_color in state_color_mapping.items() %}
- rect.{{state}} {
- fill: {{state_color}};
- }
- {% endfor %}
- </style>
-{% endblock %}
-
-{% block content %}
- {{ super() }}
- <div class="row dag-view-tools">
- <div class="col-md-12">
- <form method="get" class="form-inline">
- <input type="hidden" value="{{ dag.dag_id }}" name="dag_id">
- <input type="hidden" name="root" value="{{ root if root else '' }}">
- <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
- <div class="form-group">
- <label class="sr-only" for="base_date">Base date</label>
- <div class="input-group">
- {{ form.base_date(class_="form-control",
disabled=not(dag.has_dag_runs())) }}
- </div>
- </div>
- <div class="form-group">
- <label class="sr-only" for="num_runs">Number of runs</label>
- <div class="input-group">
- <div class="input-group-addon">Runs</div>
- {{ form.num_runs(class_="form-control",
disabled=not(dag.has_dag_runs())) }}
- </div>
- </div>
- <div class="form-group">
- <label class="sr-only" for="execution_date">Run</label>
- <div class="input-group">
- <div class="input-group-addon">Run</div>
- {{ form.execution_date(class_="form-control",
disabled=not(dag.has_dag_runs())) }}
- </div>
- </div>
- <button type="submit" class="btn"{{' disabled' if not
dag.has_dag_runs() else ''}}>Update</button>
- {% if not dag.has_dag_runs() %}<span class="text-warning"
style="margin-left:16px;">No DAG runs yet.</span>{% endif %}
- </form>
- </div>
- </div>
- <div class="container">
- <div class="gantt"></div>
- </div>
-{% endblock %}
-
-{% block tail_js %}
- {{ super() }}
- <script src="{{ url_for_asset('d3.min.js') }}"></script>
- <script src="{{ url_for_asset('d3-tip.js') }}"></script>
- <script>
- const data = {{ data|tojson }};
- </script>
- <script src="{{ url_for_asset('gantt.js') }}"></script>
-{% endblock %}
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 636d7c0f87..fd4f8192ca 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -119,7 +119,7 @@ from airflow.utils.dag_edges import dag_edges
from airflow.utils.dates import infer_time_unit, scale_time_units
from airflow.utils.db import get_query_count
from airflow.utils.docs import get_doc_url_for_provider, get_docs_url
-from airflow.utils.helpers import alchemy_to_dict, exactly_one
+from airflow.utils.helpers import exactly_one
from airflow.utils.log import secrets_masker
from airflow.utils.log.log_reader import TaskLogReader
from airflow.utils.net import get_hostname
@@ -3625,105 +3625,16 @@ class Airflow(AirflowBaseView):
@action_logging
@provide_session
def gantt(self, dag_id: str, session: Session = NEW_SESSION):
- """Show GANTT chart."""
+ """Redirect to the replacement - grid + gantt. Kept for backwards
compatibility."""
dag = get_airflow_app().dag_bag.get_dag(dag_id, session=session)
- dag_model = DagModel.get_dagmodel(dag_id, session=session)
- if not dag:
- flash(f'DAG "{dag_id}" seems to be missing.', "error")
- return redirect(url_for("Airflow.index"))
-
- wwwutils.check_import_errors(dag.fileloc, session)
- wwwutils.check_dag_warnings(dag.dag_id, session)
-
- root = request.args.get("root")
- if root:
- dag = dag.partial_subset(task_ids_or_regex=root,
include_upstream=True, include_downstream=False)
-
dt_nr_dr_data = get_date_time_num_runs_dag_runs_form_data(request,
session, dag)
dttm = dt_nr_dr_data["dttm"]
dag_run = dag.get_dagrun(execution_date=dttm)
dag_run_id = dag_run.run_id if dag_run else None
- form = DateTimeWithNumRunsWithDagRunsForm(data=dt_nr_dr_data)
- form.execution_date.choices = dt_nr_dr_data["dr_choices"]
-
- tis = session.scalars(
- select(TaskInstance)
- .where(
- TaskInstance.dag_id == dag_id,
- TaskInstance.run_id == dag_run_id,
- TaskInstance.start_date.is_not(None),
- TaskInstance.state.is_not(None),
- )
- .order_by(TaskInstance.start_date)
- ).all()
+ kwargs = {**sanitize_args(request.args), "dag_id": dag_id, "tab":
"gantt", "dag_run_id": dag_run_id}
- ti_fails = select(TaskFail).filter_by(run_id=dag_run_id, dag_id=dag_id)
- if dag.partial:
- ti_fails = ti_fails.where(TaskFail.task_id.in_([t.task_id for t in
dag.tasks]))
-
- ti_fails = session.scalars(ti_fails)
- tasks = []
- for ti in tis:
- if not dag.has_task(ti.task_id):
- continue
- # prev_attempted_tries will reflect the currently running
try_number
- # or the try_number of the last complete run
- # https://issues.apache.org/jira/browse/AIRFLOW-2143
- try_count = ti.prev_attempted_tries if ti.prev_attempted_tries !=
0 else ti.try_number
- task_dict = alchemy_to_dict(ti) or {}
- task_dict["end_date"] = task_dict["end_date"] or timezone.utcnow()
- task_dict["extraLinks"] = dag.get_task(ti.task_id).extra_links
- task_dict["try_number"] = try_count
- task_dict["execution_date"] = dttm.isoformat()
- task_dict["run_id"] = dag_run_id
- tasks.append(task_dict)
-
- tf_count = 0
- try_count = 1
- prev_task_id = ""
- for failed_task_instance in ti_fails:
- if not dag.has_task(failed_task_instance.task_id):
- continue
- if tf_count != 0 and failed_task_instance.task_id == prev_task_id:
- try_count += 1
- else:
- try_count = 1
- prev_task_id = failed_task_instance.task_id
- tf_count += 1
- task = dag.get_task(failed_task_instance.task_id)
- task_dict = alchemy_to_dict(failed_task_instance) or {}
- end_date = task_dict["end_date"] or timezone.utcnow()
- task_dict["end_date"] = end_date
- task_dict["start_date"] = task_dict["start_date"] or end_date
- task_dict["state"] = TaskInstanceState.FAILED
- task_dict["operator"] = task.operator_name
- task_dict["try_number"] = try_count
- task_dict["extraLinks"] = task.extra_links
- task_dict["execution_date"] = dttm.isoformat()
- task_dict["run_id"] = dag_run_id
- tasks.append(task_dict)
-
- task_names = [ti.task_id for ti in tis]
- data = {
- "taskNames": task_names,
- "tasks": tasks,
- "height": len(task_names) * 25 + 25,
- }
-
- session.commit()
-
- return self.render_template(
- "airflow/gantt.html",
- dag=dag,
- dag_run_id=dag_run_id,
- execution_date=dttm.isoformat(),
- form=form,
- data=data,
- base_date="",
- root=root,
- dag_model=dag_model,
- )
+ return redirect(url_for("Airflow.grid", **kwargs))
@expose("/extra_links")
@auth.has_access(
diff --git a/airflow/www/webpack.config.js b/airflow/www/webpack.config.js
index e61947707d..98ef0bc3bf 100644
--- a/airflow/www/webpack.config.js
+++ b/airflow/www/webpack.config.js
@@ -65,7 +65,6 @@ const config = {
dagDependencies: `${JS_DIR}/dag_dependencies.js`,
dags: [`${CSS_DIR}/dags.css`, `${JS_DIR}/dags.js`],
flash: `${CSS_DIR}/flash.css`,
- gantt: [`${CSS_DIR}/gantt.css`, `${JS_DIR}/gantt.js`],
graph: [`${CSS_DIR}/graph.css`, `${JS_DIR}/graph.js`],
loadingDots: `${CSS_DIR}/loading-dots.css`,
main: [`${CSS_DIR}/main.css`, `${JS_DIR}/main.js`],
diff --git a/docs/apache-airflow/img/gantt.png
b/docs/apache-airflow/img/gantt.png
index cacad300b9..85e24891a1 100644
Binary files a/docs/apache-airflow/img/gantt.png and
b/docs/apache-airflow/img/gantt.png differ
diff --git a/licenses/LICENSE-moment-strftime.txt
b/licenses/LICENSE-moment-strftime.txt
deleted file mode 100644
index d27a0a397c..0000000000
--- a/licenses/LICENSE-moment-strftime.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-(The MIT License)
-
-Copyright (c) 2012 Benjamin Oakes
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
diff --git a/tests/www/views/test_views_extra_links.py
b/tests/www/views/test_views_extra_links.py
index db5d832ef7..9b6ec14d35 100644
--- a/tests/www/views/test_views_extra_links.py
+++ b/tests/www/views/test_views_extra_links.py
@@ -17,9 +17,7 @@
# under the License.
from __future__ import annotations
-import datetime
import json
-import re
from unittest import mock
import pytest
@@ -27,12 +25,10 @@ import pytest
from airflow.models import DAG
from airflow.models.baseoperator import BaseOperator, BaseOperatorLink
from airflow.utils import timezone
-from airflow.utils.session import create_session
-from airflow.utils.state import DagRunState, TaskInstanceState
+from airflow.utils.state import DagRunState
from airflow.utils.types import DagRunType
from tests.test_utils.db import clear_db_runs
from tests.test_utils.mock_operators import AirflowLink, Dummy2TestOperator,
Dummy3TestOperator
-from tests.test_utils.www import check_content_in_response
DEFAULT_DATE = timezone.datetime(2017, 1, 1)
@@ -158,30 +154,6 @@ def test_global_extra_links_works(dag_run, task_1,
viewer_client, session):
}
-def test_extra_link_in_gantt_view(dag, create_dag_run, viewer_client):
- exec_date = timezone.datetime(2022, 1, 1)
- start_date = timezone.datetime(2020, 4, 10, 2, 0, 0)
-
- with create_session() as session:
- dag_run = create_dag_run(execution_date=exec_date, session=session)
- for ti in dag_run.task_instances:
- ti.refresh_from_task(dag.get_task(ti.task_id))
- ti.state = TaskInstanceState.SUCCESS
- ti.start_date = start_date
- ti.end_date = start_date + datetime.timedelta(seconds=30)
- session.merge(ti)
-
- url = f"gantt?dag_id={dag.dag_id}&execution_date={exec_date}"
- resp = viewer_client.get(url, follow_redirects=True)
-
- check_content_in_response('"extraLinks":', resp)
-
- extra_links_grps = re.search(r"extraLinks\": \[(\".*?\")\]",
resp.get_data(as_text=True))
- extra_links = extra_links_grps.group(0)
- assert "airflow" in extra_links
- assert "github" in extra_links
-
-
def test_operator_extra_link_override_global_extra_link(dag_run, task_1,
viewer_client):
response = viewer_client.get(
f"{ENDPOINT}?dag_id={task_1.dag_id}&task_id={task_1.task_id}"
diff --git a/tests/www/views/test_views_graph_gantt.py
b/tests/www/views/test_views_graph.py
similarity index 99%
rename from tests/www/views/test_views_graph_gantt.py
rename to tests/www/views/test_views_graph.py
index 697658f8c9..3a49404d63 100644
--- a/tests/www/views/test_views_graph_gantt.py
+++ b/tests/www/views/test_views_graph.py
@@ -41,7 +41,6 @@ VERY_CLOSE_RUNS_DATE = timezone.datetime(2020, 1, 1, 0, 0, 0)
ENDPOINTS = [
"/graph?dag_id=dag_for_testing_dt_nr_dr_form",
- "/gantt?dag_id=dag_for_testing_dt_nr_dr_form",
]