dags.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /*!
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /* global document, window, $, d3, STATE_COLOR, isoDateToTimeEl, autoRefreshInterval,
  20. localStorage */
  21. import { throttle } from "lodash";
  22. import { getMetaValue } from "./utils";
  23. import tiTooltip from "./task_instances";
  24. import { approxTimeFromNow, formatDateTime } from "./datetime_utils";
  25. import { openDatasetModal, getDatasetTooltipInfo } from "./datasetUtils";
  26. const DAGS_INDEX = getMetaValue("dags_index");
  27. const ENTER_KEY_CODE = 13;
  28. const pausedUrl = getMetaValue("paused_url");
  29. const statusFilter = getMetaValue("status_filter");
  30. const autocompleteUrl = getMetaValue("autocomplete_url");
  31. const graphUrl = getMetaValue("graph_url");
  32. const dagRunUrl = getMetaValue("dag_run_url");
  33. const taskInstanceUrl = getMetaValue("task_instance_url");
  34. const blockedUrl = getMetaValue("blocked_url");
  35. const csrfToken = getMetaValue("csrf_token");
  36. const lastDagRunsUrl = getMetaValue("last_dag_runs_url");
  37. const dagStatsUrl = getMetaValue("dag_stats_url");
  38. const taskStatsUrl = getMetaValue("task_stats_url");
  39. const gridUrl = getMetaValue("grid_url");
  40. const datasetsUrl = getMetaValue("datasets_url");
  41. const nextRunDatasetsSummaryUrl = getMetaValue("next_run_datasets_summary_url");
  42. const nextDatasets = {};
  43. let nextDatasetsError;
  44. const DAG_RUN = "dag-run";
  45. const TASK_INSTANCE = "task-instance";
  46. // auto refresh interval in milliseconds
  47. // (x2 the interval in tree/graph view since this page can take longer to refresh )
  48. const refreshIntervalMs = 2000;
  49. $("#tags_filter").select2({
  50. placeholder: "Filter DAGs by tag",
  51. allowClear: true,
  52. });
  53. $("#tags_filter").on("change", (e) => {
  54. e.preventDefault();
  55. const query = new URLSearchParams(window.location.search);
  56. const tags = $(e.target).select2("val");
  57. if (tags.length) {
  58. if (query.has("tags")) query.delete("tags");
  59. tags.forEach((value) => {
  60. query.append("tags", value);
  61. });
  62. } else {
  63. query.delete("tags");
  64. query.set("reset_tags", "reset");
  65. }
  66. if (query.has("page")) query.delete("page");
  67. window.location = `${DAGS_INDEX}?${query.toString()}`;
  68. });
  69. $("#tags_form").on("reset", (e) => {
  70. e.preventDefault();
  71. const query = new URLSearchParams(window.location.search);
  72. query.delete("tags");
  73. if (query.has("page")) query.delete("page");
  74. query.set("reset_tags", "reset");
  75. window.location = `${DAGS_INDEX}?${query.toString()}`;
  76. });
  77. $("#dag_query").on("keypress", (e) => {
  78. // check for key press on ENTER (key code 13) to trigger the search
  79. if (e.which === ENTER_KEY_CODE) {
  80. const query = new URLSearchParams(window.location.search);
  81. query.set("search", e.target.value.trim());
  82. query.delete("page");
  83. window.location = `${DAGS_INDEX}?${query.toString()}`;
  84. e.preventDefault();
  85. }
  86. });
  87. $.each($("[id^=toggle]"), function toggleId() {
  88. const $input = $(this);
  89. const dagId = $input.data("dag-id");
  90. $input.on("change", () => {
  91. const isPaused = $input.is(":checked");
  92. const url = `${pausedUrl}?is_paused=${isPaused}&dag_id=${encodeURIComponent(
  93. dagId
  94. )}`;
  95. $input.removeClass("switch-input--error");
  96. // Remove focus on element so the tooltip will go away
  97. $input.trigger("blur");
  98. $.post(url).fail(() => {
  99. setTimeout(() => {
  100. $input.prop("checked", !isPaused);
  101. $input.addClass("switch-input--error");
  102. }, 500);
  103. });
  104. });
  105. });
  106. $(".typeahead").typeahead({
  107. source(query, callback) {
  108. return $.ajax(autocompleteUrl, {
  109. data: {
  110. query: encodeURIComponent(query),
  111. status: statusFilter,
  112. },
  113. success: callback,
  114. });
  115. },
  116. displayText(value) {
  117. return value.dag_display_name || value.name;
  118. },
  119. autoSelect: false,
  120. afterSelect(value) {
  121. const query = new URLSearchParams(window.location.search);
  122. query.set("search", value.name);
  123. if (value.type === "owner") {
  124. window.location = `${DAGS_INDEX}?${query}`;
  125. }
  126. if (value.type === "dag") {
  127. window.location = `${gridUrl.replace("__DAG_ID__", value.name)}?${query}`;
  128. }
  129. },
  130. });
  131. $("#search_form").on("reset", () => {
  132. const query = new URLSearchParams(window.location.search);
  133. query.delete("search");
  134. query.delete("page");
  135. window.location = `${DAGS_INDEX}?${query}`;
  136. });
  137. $("#main_content").show(250);
  138. const diameter = 25;
  139. const circleMargin = 4;
  140. const strokeWidth = 2;
  141. const strokeWidthHover = 6;
  142. function blockedHandler(error, json) {
  143. $.each(json, function handleBlock() {
  144. const a = document.querySelector(`[data-dag-id="${this.dag_id}"]`);
  145. a.title = `${this.active_dag_run}/${this.max_active_runs} active dag runs`;
  146. if (this.active_dag_run >= this.max_active_runs) {
  147. a.style.color = "#e43921";
  148. }
  149. });
  150. }
  151. function lastDagRunsHandler(error, json) {
  152. $(".js-loading-last-run").remove();
  153. Object.keys(json).forEach((safeDagId) => {
  154. const dagId = json[safeDagId].dag_id;
  155. const executionDate = json[safeDagId].execution_date;
  156. const g = d3.select(`#last-run-${safeDagId}`);
  157. // Show last run as a link to the graph view
  158. g.selectAll("a")
  159. .attr(
  160. "href",
  161. `${graphUrl}?dag_id=${encodeURIComponent(
  162. dagId
  163. )}&execution_date=${encodeURIComponent(executionDate)}`
  164. )
  165. .html("")
  166. .insert(isoDateToTimeEl.bind(null, executionDate, { title: false }));
  167. // Only show the tooltip when we have a last run and add the json to a custom data- attribute
  168. g.selectAll("span")
  169. .style("display", null)
  170. .attr("data-lastrun", JSON.stringify(json[safeDagId]));
  171. });
  172. }
  173. // Load data-lastrun attribute data to populate the tooltip on hover
  174. d3.selectAll(".js-last-run-tooltip").on(
  175. "mouseover",
  176. function mouseoverLastRun() {
  177. const lastRunData = JSON.parse(d3.select(this).attr("data-lastrun"));
  178. d3.select(this).attr("data-original-title", tiTooltip(lastRunData));
  179. }
  180. );
  181. function formatCount(count) {
  182. if (count >= 1000000) return `${Math.floor(count / 1000000)}M`;
  183. if (count >= 1000) return `${Math.floor(count / 1000)}k`;
  184. return count;
  185. }
  186. function drawDagStats(selector, dagId, states) {
  187. const g = d3
  188. .select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
  189. .attr("height", diameter + strokeWidthHover * 2)
  190. .attr("width", states.length * (diameter + circleMargin) + circleMargin)
  191. .selectAll("g")
  192. .data(states)
  193. .enter()
  194. .append("g")
  195. .attr("transform", (d, i) => {
  196. const x = i * (diameter + circleMargin) + (diameter / 2 + circleMargin);
  197. const y = diameter / 2 + strokeWidthHover;
  198. return `translate(${x},${y})`;
  199. });
  200. g.append("svg:a")
  201. .attr("href", (d) => {
  202. const params = new URLSearchParams();
  203. params.append("_flt_3_dag_id", dagId);
  204. /* eslint no-unused-expressions: ["error", { "allowTernary": true }] */
  205. d.state
  206. ? params.append("_flt_3_state", d.state)
  207. : params.append("_flt_8_state", "");
  208. switch (selector) {
  209. case DAG_RUN:
  210. return `${dagRunUrl}?${params.toString()}`;
  211. case TASK_INSTANCE:
  212. return `${taskInstanceUrl}?${params.toString()}`;
  213. default:
  214. return "";
  215. }
  216. })
  217. .append("circle")
  218. .attr(
  219. "id",
  220. (d) => `${selector}-${dagId.replace(/\./g, "_")}-${d.state || "none"}`
  221. )
  222. .attr("class", "has-svg-tooltip")
  223. .attr("stroke-width", (d) => {
  224. if (d.count > 0) return strokeWidth;
  225. return 1;
  226. })
  227. .attr("stroke", (d) => {
  228. if (d.count > 0) return STATE_COLOR[d.state];
  229. return "gainsboro";
  230. })
  231. .attr("fill", "#fff")
  232. .attr("r", diameter / 2)
  233. .attr("title", (d) => `${d.state || "none"}: ${d.count}`)
  234. .on("mouseover", (d) => {
  235. if (d.count > 0) {
  236. d3.select(d3.event.currentTarget)
  237. .transition()
  238. .duration(400)
  239. .attr("fill", "#e2e2e2")
  240. .style("stroke-width", strokeWidthHover);
  241. }
  242. })
  243. .on("mouseout", (d) => {
  244. if (d.count > 0) {
  245. d3.select(d3.event.currentTarget)
  246. .transition()
  247. .duration(400)
  248. .attr("fill", "#fff")
  249. .style("stroke-width", strokeWidth);
  250. }
  251. })
  252. .style("opacity", 0)
  253. .transition()
  254. .duration(300)
  255. .delay((d, i) => i * 50)
  256. .style("opacity", 1);
  257. d3.select(`.js-loading-${selector}-stats`).remove();
  258. g.append("text")
  259. .attr("fill", "#51504f")
  260. .attr("text-anchor", "middle")
  261. .attr("vertical-align", "middle")
  262. .attr("font-size", 9)
  263. .attr("y", 3)
  264. .style("pointer-events", "none")
  265. .text((d) => (d.count > 0 ? formatCount(d.count) : ""));
  266. }
  267. function dagStatsHandler(selector, json) {
  268. Object.keys(json).forEach((dagId) => {
  269. const states = json[dagId];
  270. drawDagStats(selector, dagId, states);
  271. });
  272. }
  273. function nextRunDatasetsSummaryHandler(_, json) {
  274. [...document.getElementsByClassName("next-dataset-triggered")].forEach(
  275. (el) => {
  276. const dagId = $(el).attr("data-dag-id");
  277. const previousSummary = $(el).attr("data-summary");
  278. const nextDatasetsInfo = json[dagId];
  279. // Only update dags that depend on multiple datasets
  280. if (nextDatasetsInfo && !nextDatasetsInfo.uri) {
  281. const newSummary = `${nextDatasetsInfo.ready} of ${nextDatasetsInfo.total} datasets updated`;
  282. // Only update the element if the summary has changed
  283. if (previousSummary !== newSummary) {
  284. $(el).attr("data-summary", newSummary);
  285. $(el).text(newSummary);
  286. }
  287. }
  288. }
  289. );
  290. }
  291. function getDagIds({ activeDagsOnly = false } = {}) {
  292. let dagIds = $("[id^=toggle]");
  293. if (activeDagsOnly) {
  294. dagIds = dagIds.filter(":checked");
  295. }
  296. dagIds = dagIds
  297. // eslint-disable-next-line func-names
  298. .map(function () {
  299. return $(this).data("dag-id");
  300. })
  301. .get();
  302. return dagIds;
  303. }
  304. function getDagStats() {
  305. const dagIds = getDagIds();
  306. const params = new URLSearchParams();
  307. dagIds.forEach((dagId) => {
  308. params.append("dag_ids", dagId);
  309. });
  310. if (params.has("dag_ids")) {
  311. d3.json(blockedUrl)
  312. .header("X-CSRFToken", csrfToken)
  313. .post(params, blockedHandler);
  314. d3.json(lastDagRunsUrl)
  315. .header("X-CSRFToken", csrfToken)
  316. .post(params, lastDagRunsHandler);
  317. d3.json(dagStatsUrl)
  318. .header("X-CSRFToken", csrfToken)
  319. .post(params, (error, json) => dagStatsHandler(DAG_RUN, json));
  320. d3.json(taskStatsUrl)
  321. .header("X-CSRFToken", csrfToken)
  322. .post(params, (error, json) => dagStatsHandler(TASK_INSTANCE, json));
  323. } else {
  324. // no dags, hide the loading dots
  325. $(`.js-loading-${DAG_RUN}-stats`).remove();
  326. $(`.js-loading-${TASK_INSTANCE}-stats`).remove();
  327. }
  328. }
  329. function showSvgTooltip(text, circ) {
  330. const tip = $("#svg-tooltip");
  331. tip.children(".tooltip-inner").text(text);
  332. const centeringOffset = tip.width() / 2;
  333. tip.css({
  334. display: "block",
  335. left: `${circ.left + 12.5 - centeringOffset}px`, // 12.5 == half of circle width
  336. top: `${circ.top - 25}px`, // 25 == position above circle
  337. });
  338. }
  339. function hideSvgTooltip() {
  340. $("#svg-tooltip").css("display", "none");
  341. }
  342. function refreshDagStats(selector, dagId, states) {
  343. d3.select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
  344. .selectAll("circle")
  345. .data(states)
  346. .attr("stroke-width", (d) => {
  347. if (d.count > 0) return strokeWidth;
  348. return 1;
  349. })
  350. .attr("stroke", (d) => {
  351. if (d.count > 0) return STATE_COLOR[d.state];
  352. return "gainsboro";
  353. });
  354. d3.select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
  355. .selectAll("text")
  356. .data(states)
  357. .text((d) => {
  358. if (d.count > 0) {
  359. return d.count;
  360. }
  361. return "";
  362. });
  363. }
  364. let refreshInterval;
  365. function checkActiveRuns(json) {
  366. // filter latest dag runs and check if there are still running dags
  367. const activeRuns = Object.keys(json).filter((dagId) => {
  368. const dagRuns = json[dagId]
  369. .filter(({ state }) => state === "running" || state === "queued")
  370. .filter((r) => r.count > 0);
  371. return dagRuns.length > 0;
  372. });
  373. if (activeRuns.length === 0) {
  374. // in case there are no active runs increase the interval for auto refresh
  375. $("#auto_refresh").prop("checked", false);
  376. clearInterval(refreshInterval);
  377. }
  378. }
  379. function refreshDagStatsHandler(selector, json) {
  380. if (selector === DAG_RUN) checkActiveRuns(json);
  381. Object.keys(json).forEach((dagId) => {
  382. const states = json[dagId];
  383. refreshDagStats(selector, dagId, states);
  384. });
  385. }
  386. function handleRefresh({ activeDagsOnly = false } = {}) {
  387. const dagIds = getDagIds({ activeDagsOnly });
  388. const params = new URLSearchParams();
  389. dagIds.forEach((dagId) => {
  390. params.append("dag_ids", dagId);
  391. });
  392. $("#loading-dots").css("display", "inline-block");
  393. if (params.has("dag_ids")) {
  394. d3.json(lastDagRunsUrl)
  395. .header("X-CSRFToken", csrfToken)
  396. .post(params, lastDagRunsHandler);
  397. d3.json(dagStatsUrl)
  398. .header("X-CSRFToken", csrfToken)
  399. .post(params, (error, json) => refreshDagStatsHandler(DAG_RUN, json));
  400. d3.json(taskStatsUrl)
  401. .header("X-CSRFToken", csrfToken)
  402. .post(params, (error, json) =>
  403. refreshDagStatsHandler(TASK_INSTANCE, json)
  404. );
  405. d3.json(nextRunDatasetsSummaryUrl)
  406. .header("X-CSRFToken", csrfToken)
  407. .post(params, nextRunDatasetsSummaryHandler);
  408. }
  409. setTimeout(() => {
  410. $("#loading-dots").css("display", "none");
  411. }, refreshIntervalMs);
  412. }
  413. function startOrStopRefresh() {
  414. if ($("#auto_refresh").is(":checked")) {
  415. refreshInterval = setInterval(() => {
  416. handleRefresh({ activeDagsOnly: true });
  417. }, autoRefreshInterval * refreshIntervalMs);
  418. } else {
  419. clearInterval(refreshInterval);
  420. }
  421. }
  422. function initAutoRefresh() {
  423. const isDisabled = localStorage.getItem("dagsDisableAutoRefresh");
  424. $("#auto_refresh").prop("checked", !isDisabled);
  425. startOrStopRefresh();
  426. d3.select("#refresh_button").on("click", () => handleRefresh());
  427. }
  428. // pause autorefresh when the page is not active
  429. const handleVisibilityChange = () => {
  430. if (document.hidden) {
  431. clearInterval(refreshInterval);
  432. } else {
  433. initAutoRefresh();
  434. }
  435. };
  436. document.addEventListener("visibilitychange", handleVisibilityChange);
  437. $(window).on("load", () => {
  438. initAutoRefresh();
  439. $("body").on("mouseover", ".has-svg-tooltip", (e) => {
  440. const elem = e.target;
  441. const text = elem.getAttribute("title");
  442. const circ = elem.getBoundingClientRect();
  443. showSvgTooltip(text, circ);
  444. });
  445. $("body").on("mouseout", ".has-svg-tooltip", () => {
  446. hideSvgTooltip();
  447. });
  448. getDagStats();
  449. });
  450. $(".js-next-run-tooltip").each((i, run) => {
  451. $(run).on("mouseover", () => {
  452. $(run).attr("data-original-title", () => {
  453. const nextRunData = $(run).attr("data-nextrun");
  454. const [createAfter, intervalStart, intervalEnd] = nextRunData.split(",");
  455. let newTitle = "";
  456. newTitle += `<strong>Run After:</strong> ${formatDateTime(
  457. createAfter
  458. )}<br>`;
  459. newTitle += `Next Run: ${approxTimeFromNow(createAfter)}<br><br>`;
  460. newTitle += "<strong>Data Interval</strong><br>";
  461. newTitle += `Start: ${formatDateTime(intervalStart)}<br>`;
  462. newTitle += `End: ${formatDateTime(intervalEnd)}`;
  463. return newTitle;
  464. });
  465. });
  466. });
  467. $("#auto_refresh").change(() => {
  468. if ($("#auto_refresh").is(":checked")) {
  469. // Run an initial refresh before starting interval if manually turned on
  470. handleRefresh({ activeDagsOnly: true });
  471. localStorage.removeItem("dagsDisableAutoRefresh");
  472. } else {
  473. localStorage.setItem("dagsDisableAutoRefresh", "true");
  474. $("#loading-dots").css("display", "none");
  475. }
  476. startOrStopRefresh();
  477. });
  478. $(".next-dataset-triggered").on("click", (e) => {
  479. const dagId = $(e.target).data("dag-id");
  480. const summary = $(e.target).data("summary");
  481. const singleDatasetUri = $(e.target).data("uri");
  482. // If there are multiple datasets, open a modal, otherwise link directly to the dataset
  483. if (!singleDatasetUri) {
  484. if (dagId)
  485. openDatasetModal(dagId, summary, nextDatasets[dagId], nextDatasetsError);
  486. } else {
  487. window.location.href = `${datasetsUrl}?uri=${encodeURIComponent(
  488. singleDatasetUri
  489. )}`;
  490. }
  491. });
  492. const getTooltipInfo = throttle(
  493. (dagId, run, setNextDatasets) =>
  494. getDatasetTooltipInfo(dagId, run, setNextDatasets),
  495. 1000
  496. );
  497. $(".js-dataset-triggered").each((i, cell) => {
  498. $(cell).on("mouseover", () => {
  499. const run = $(cell).children();
  500. const dagId = $(run).data("dag-id");
  501. const singleDatasetUri = $(run).data("uri");
  502. const setNextDatasets = (datasets, error) => {
  503. nextDatasets[dagId] = datasets;
  504. nextDatasetsError = error;
  505. };
  506. // Only update the tooltip info if there are multiple datasets
  507. if (!singleDatasetUri) {
  508. getTooltipInfo(dagId, run, setNextDatasets);
  509. }
  510. });
  511. });