| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- /*!
- * 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.
- */
- /* global document, window, $, d3, STATE_COLOR, isoDateToTimeEl, autoRefreshInterval,
- localStorage */
- import { throttle } from "lodash";
- import { getMetaValue } from "./utils";
- import tiTooltip from "./task_instances";
- import { approxTimeFromNow, formatDateTime } from "./datetime_utils";
- import { openDatasetModal, getDatasetTooltipInfo } from "./datasetUtils";
- const DAGS_INDEX = getMetaValue("dags_index");
- const ENTER_KEY_CODE = 13;
- const pausedUrl = getMetaValue("paused_url");
- const statusFilter = getMetaValue("status_filter");
- const autocompleteUrl = getMetaValue("autocomplete_url");
- const graphUrl = getMetaValue("graph_url");
- const dagRunUrl = getMetaValue("dag_run_url");
- const taskInstanceUrl = getMetaValue("task_instance_url");
- const blockedUrl = getMetaValue("blocked_url");
- const csrfToken = getMetaValue("csrf_token");
- const lastDagRunsUrl = getMetaValue("last_dag_runs_url");
- const dagStatsUrl = getMetaValue("dag_stats_url");
- const taskStatsUrl = getMetaValue("task_stats_url");
- const gridUrl = getMetaValue("grid_url");
- const datasetsUrl = getMetaValue("datasets_url");
- const nextRunDatasetsSummaryUrl = getMetaValue("next_run_datasets_summary_url");
- const nextDatasets = {};
- let nextDatasetsError;
- const DAG_RUN = "dag-run";
- const TASK_INSTANCE = "task-instance";
- // auto refresh interval in milliseconds
- // (x2 the interval in tree/graph view since this page can take longer to refresh )
- const refreshIntervalMs = 2000;
- $("#tags_filter").select2({
- placeholder: "Filter DAGs by tag",
- allowClear: true,
- });
- $("#tags_filter").on("change", (e) => {
- e.preventDefault();
- const query = new URLSearchParams(window.location.search);
- const tags = $(e.target).select2("val");
- if (tags.length) {
- if (query.has("tags")) query.delete("tags");
- tags.forEach((value) => {
- query.append("tags", value);
- });
- } else {
- query.delete("tags");
- query.set("reset_tags", "reset");
- }
- if (query.has("page")) query.delete("page");
- window.location = `${DAGS_INDEX}?${query.toString()}`;
- });
- $("#tags_form").on("reset", (e) => {
- e.preventDefault();
- const query = new URLSearchParams(window.location.search);
- query.delete("tags");
- if (query.has("page")) query.delete("page");
- query.set("reset_tags", "reset");
- window.location = `${DAGS_INDEX}?${query.toString()}`;
- });
- $("#dag_query").on("keypress", (e) => {
- // check for key press on ENTER (key code 13) to trigger the search
- if (e.which === ENTER_KEY_CODE) {
- const query = new URLSearchParams(window.location.search);
- query.set("search", e.target.value.trim());
- query.delete("page");
- window.location = `${DAGS_INDEX}?${query.toString()}`;
- e.preventDefault();
- }
- });
- $.each($("[id^=toggle]"), function toggleId() {
- const $input = $(this);
- const dagId = $input.data("dag-id");
- $input.on("change", () => {
- const isPaused = $input.is(":checked");
- const url = `${pausedUrl}?is_paused=${isPaused}&dag_id=${encodeURIComponent(
- dagId
- )}`;
- $input.removeClass("switch-input--error");
- // Remove focus on element so the tooltip will go away
- $input.trigger("blur");
- $.post(url).fail(() => {
- setTimeout(() => {
- $input.prop("checked", !isPaused);
- $input.addClass("switch-input--error");
- }, 500);
- });
- });
- });
- $(".typeahead").typeahead({
- source(query, callback) {
- return $.ajax(autocompleteUrl, {
- data: {
- query: encodeURIComponent(query),
- status: statusFilter,
- },
- success: callback,
- });
- },
- displayText(value) {
- return value.dag_display_name || value.name;
- },
- autoSelect: false,
- afterSelect(value) {
- const query = new URLSearchParams(window.location.search);
- query.set("search", value.name);
- if (value.type === "owner") {
- window.location = `${DAGS_INDEX}?${query}`;
- }
- if (value.type === "dag") {
- window.location = `${gridUrl.replace("__DAG_ID__", value.name)}?${query}`;
- }
- },
- });
- $("#search_form").on("reset", () => {
- const query = new URLSearchParams(window.location.search);
- query.delete("search");
- query.delete("page");
- window.location = `${DAGS_INDEX}?${query}`;
- });
- $("#main_content").show(250);
- const diameter = 25;
- const circleMargin = 4;
- const strokeWidth = 2;
- const strokeWidthHover = 6;
- function blockedHandler(error, json) {
- $.each(json, function handleBlock() {
- const a = document.querySelector(`[data-dag-id="${this.dag_id}"]`);
- a.title = `${this.active_dag_run}/${this.max_active_runs} active dag runs`;
- if (this.active_dag_run >= this.max_active_runs) {
- a.style.color = "#e43921";
- }
- });
- }
- function lastDagRunsHandler(error, json) {
- $(".js-loading-last-run").remove();
- Object.keys(json).forEach((safeDagId) => {
- const dagId = json[safeDagId].dag_id;
- const executionDate = json[safeDagId].execution_date;
- const g = d3.select(`#last-run-${safeDagId}`);
- // Show last run as a link to the graph view
- g.selectAll("a")
- .attr(
- "href",
- `${graphUrl}?dag_id=${encodeURIComponent(
- dagId
- )}&execution_date=${encodeURIComponent(executionDate)}`
- )
- .html("")
- .insert(isoDateToTimeEl.bind(null, executionDate, { title: false }));
- // Only show the tooltip when we have a last run and add the json to a custom data- attribute
- g.selectAll("span")
- .style("display", null)
- .attr("data-lastrun", JSON.stringify(json[safeDagId]));
- });
- }
- // Load data-lastrun attribute data to populate the tooltip on hover
- d3.selectAll(".js-last-run-tooltip").on(
- "mouseover",
- function mouseoverLastRun() {
- const lastRunData = JSON.parse(d3.select(this).attr("data-lastrun"));
- d3.select(this).attr("data-original-title", tiTooltip(lastRunData));
- }
- );
- function formatCount(count) {
- if (count >= 1000000) return `${Math.floor(count / 1000000)}M`;
- if (count >= 1000) return `${Math.floor(count / 1000)}k`;
- return count;
- }
- function drawDagStats(selector, dagId, states) {
- const g = d3
- .select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
- .attr("height", diameter + strokeWidthHover * 2)
- .attr("width", states.length * (diameter + circleMargin) + circleMargin)
- .selectAll("g")
- .data(states)
- .enter()
- .append("g")
- .attr("transform", (d, i) => {
- const x = i * (diameter + circleMargin) + (diameter / 2 + circleMargin);
- const y = diameter / 2 + strokeWidthHover;
- return `translate(${x},${y})`;
- });
- g.append("svg:a")
- .attr("href", (d) => {
- const params = new URLSearchParams();
- params.append("_flt_3_dag_id", dagId);
- /* eslint no-unused-expressions: ["error", { "allowTernary": true }] */
- d.state
- ? params.append("_flt_3_state", d.state)
- : params.append("_flt_8_state", "");
- switch (selector) {
- case DAG_RUN:
- return `${dagRunUrl}?${params.toString()}`;
- case TASK_INSTANCE:
- return `${taskInstanceUrl}?${params.toString()}`;
- default:
- return "";
- }
- })
- .append("circle")
- .attr(
- "id",
- (d) => `${selector}-${dagId.replace(/\./g, "_")}-${d.state || "none"}`
- )
- .attr("class", "has-svg-tooltip")
- .attr("stroke-width", (d) => {
- if (d.count > 0) return strokeWidth;
- return 1;
- })
- .attr("stroke", (d) => {
- if (d.count > 0) return STATE_COLOR[d.state];
- return "gainsboro";
- })
- .attr("fill", "#fff")
- .attr("r", diameter / 2)
- .attr("title", (d) => `${d.state || "none"}: ${d.count}`)
- .on("mouseover", (d) => {
- if (d.count > 0) {
- d3.select(d3.event.currentTarget)
- .transition()
- .duration(400)
- .attr("fill", "#e2e2e2")
- .style("stroke-width", strokeWidthHover);
- }
- })
- .on("mouseout", (d) => {
- if (d.count > 0) {
- d3.select(d3.event.currentTarget)
- .transition()
- .duration(400)
- .attr("fill", "#fff")
- .style("stroke-width", strokeWidth);
- }
- })
- .style("opacity", 0)
- .transition()
- .duration(300)
- .delay((d, i) => i * 50)
- .style("opacity", 1);
- d3.select(`.js-loading-${selector}-stats`).remove();
- g.append("text")
- .attr("fill", "#51504f")
- .attr("text-anchor", "middle")
- .attr("vertical-align", "middle")
- .attr("font-size", 9)
- .attr("y", 3)
- .style("pointer-events", "none")
- .text((d) => (d.count > 0 ? formatCount(d.count) : ""));
- }
- function dagStatsHandler(selector, json) {
- Object.keys(json).forEach((dagId) => {
- const states = json[dagId];
- drawDagStats(selector, dagId, states);
- });
- }
- function nextRunDatasetsSummaryHandler(_, json) {
- [...document.getElementsByClassName("next-dataset-triggered")].forEach(
- (el) => {
- const dagId = $(el).attr("data-dag-id");
- const previousSummary = $(el).attr("data-summary");
- const nextDatasetsInfo = json[dagId];
- // Only update dags that depend on multiple datasets
- if (nextDatasetsInfo && !nextDatasetsInfo.uri) {
- const newSummary = `${nextDatasetsInfo.ready} of ${nextDatasetsInfo.total} datasets updated`;
- // Only update the element if the summary has changed
- if (previousSummary !== newSummary) {
- $(el).attr("data-summary", newSummary);
- $(el).text(newSummary);
- }
- }
- }
- );
- }
- function getDagIds({ activeDagsOnly = false } = {}) {
- let dagIds = $("[id^=toggle]");
- if (activeDagsOnly) {
- dagIds = dagIds.filter(":checked");
- }
- dagIds = dagIds
- // eslint-disable-next-line func-names
- .map(function () {
- return $(this).data("dag-id");
- })
- .get();
- return dagIds;
- }
- function getDagStats() {
- const dagIds = getDagIds();
- const params = new URLSearchParams();
- dagIds.forEach((dagId) => {
- params.append("dag_ids", dagId);
- });
- if (params.has("dag_ids")) {
- d3.json(blockedUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, blockedHandler);
- d3.json(lastDagRunsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, lastDagRunsHandler);
- d3.json(dagStatsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, (error, json) => dagStatsHandler(DAG_RUN, json));
- d3.json(taskStatsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, (error, json) => dagStatsHandler(TASK_INSTANCE, json));
- } else {
- // no dags, hide the loading dots
- $(`.js-loading-${DAG_RUN}-stats`).remove();
- $(`.js-loading-${TASK_INSTANCE}-stats`).remove();
- }
- }
- function showSvgTooltip(text, circ) {
- const tip = $("#svg-tooltip");
- tip.children(".tooltip-inner").text(text);
- const centeringOffset = tip.width() / 2;
- tip.css({
- display: "block",
- left: `${circ.left + 12.5 - centeringOffset}px`, // 12.5 == half of circle width
- top: `${circ.top - 25}px`, // 25 == position above circle
- });
- }
- function hideSvgTooltip() {
- $("#svg-tooltip").css("display", "none");
- }
- function refreshDagStats(selector, dagId, states) {
- d3.select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
- .selectAll("circle")
- .data(states)
- .attr("stroke-width", (d) => {
- if (d.count > 0) return strokeWidth;
- return 1;
- })
- .attr("stroke", (d) => {
- if (d.count > 0) return STATE_COLOR[d.state];
- return "gainsboro";
- });
- d3.select(`svg#${selector}-${dagId.replace(/\./g, "__dot__")}`)
- .selectAll("text")
- .data(states)
- .text((d) => {
- if (d.count > 0) {
- return d.count;
- }
- return "";
- });
- }
- let refreshInterval;
- function checkActiveRuns(json) {
- // filter latest dag runs and check if there are still running dags
- const activeRuns = Object.keys(json).filter((dagId) => {
- const dagRuns = json[dagId]
- .filter(({ state }) => state === "running" || state === "queued")
- .filter((r) => r.count > 0);
- return dagRuns.length > 0;
- });
- if (activeRuns.length === 0) {
- // in case there are no active runs increase the interval for auto refresh
- $("#auto_refresh").prop("checked", false);
- clearInterval(refreshInterval);
- }
- }
- function refreshDagStatsHandler(selector, json) {
- if (selector === DAG_RUN) checkActiveRuns(json);
- Object.keys(json).forEach((dagId) => {
- const states = json[dagId];
- refreshDagStats(selector, dagId, states);
- });
- }
- function handleRefresh({ activeDagsOnly = false } = {}) {
- const dagIds = getDagIds({ activeDagsOnly });
- const params = new URLSearchParams();
- dagIds.forEach((dagId) => {
- params.append("dag_ids", dagId);
- });
- $("#loading-dots").css("display", "inline-block");
- if (params.has("dag_ids")) {
- d3.json(lastDagRunsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, lastDagRunsHandler);
- d3.json(dagStatsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, (error, json) => refreshDagStatsHandler(DAG_RUN, json));
- d3.json(taskStatsUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, (error, json) =>
- refreshDagStatsHandler(TASK_INSTANCE, json)
- );
- d3.json(nextRunDatasetsSummaryUrl)
- .header("X-CSRFToken", csrfToken)
- .post(params, nextRunDatasetsSummaryHandler);
- }
- setTimeout(() => {
- $("#loading-dots").css("display", "none");
- }, refreshIntervalMs);
- }
- function startOrStopRefresh() {
- if ($("#auto_refresh").is(":checked")) {
- refreshInterval = setInterval(() => {
- handleRefresh({ activeDagsOnly: true });
- }, autoRefreshInterval * refreshIntervalMs);
- } else {
- clearInterval(refreshInterval);
- }
- }
- function initAutoRefresh() {
- const isDisabled = localStorage.getItem("dagsDisableAutoRefresh");
- $("#auto_refresh").prop("checked", !isDisabled);
- startOrStopRefresh();
- d3.select("#refresh_button").on("click", () => handleRefresh());
- }
- // pause autorefresh when the page is not active
- const handleVisibilityChange = () => {
- if (document.hidden) {
- clearInterval(refreshInterval);
- } else {
- initAutoRefresh();
- }
- };
- document.addEventListener("visibilitychange", handleVisibilityChange);
- $(window).on("load", () => {
- initAutoRefresh();
- $("body").on("mouseover", ".has-svg-tooltip", (e) => {
- const elem = e.target;
- const text = elem.getAttribute("title");
- const circ = elem.getBoundingClientRect();
- showSvgTooltip(text, circ);
- });
- $("body").on("mouseout", ".has-svg-tooltip", () => {
- hideSvgTooltip();
- });
- getDagStats();
- });
- $(".js-next-run-tooltip").each((i, run) => {
- $(run).on("mouseover", () => {
- $(run).attr("data-original-title", () => {
- const nextRunData = $(run).attr("data-nextrun");
- const [createAfter, intervalStart, intervalEnd] = nextRunData.split(",");
- let newTitle = "";
- newTitle += `<strong>Run After:</strong> ${formatDateTime(
- createAfter
- )}<br>`;
- newTitle += `Next Run: ${approxTimeFromNow(createAfter)}<br><br>`;
- newTitle += "<strong>Data Interval</strong><br>";
- newTitle += `Start: ${formatDateTime(intervalStart)}<br>`;
- newTitle += `End: ${formatDateTime(intervalEnd)}`;
- return newTitle;
- });
- });
- });
- $("#auto_refresh").change(() => {
- if ($("#auto_refresh").is(":checked")) {
- // Run an initial refresh before starting interval if manually turned on
- handleRefresh({ activeDagsOnly: true });
- localStorage.removeItem("dagsDisableAutoRefresh");
- } else {
- localStorage.setItem("dagsDisableAutoRefresh", "true");
- $("#loading-dots").css("display", "none");
- }
- startOrStopRefresh();
- });
- $(".next-dataset-triggered").on("click", (e) => {
- const dagId = $(e.target).data("dag-id");
- const summary = $(e.target).data("summary");
- const singleDatasetUri = $(e.target).data("uri");
- // If there are multiple datasets, open a modal, otherwise link directly to the dataset
- if (!singleDatasetUri) {
- if (dagId)
- openDatasetModal(dagId, summary, nextDatasets[dagId], nextDatasetsError);
- } else {
- window.location.href = `${datasetsUrl}?uri=${encodeURIComponent(
- singleDatasetUri
- )}`;
- }
- });
- const getTooltipInfo = throttle(
- (dagId, run, setNextDatasets) =>
- getDatasetTooltipInfo(dagId, run, setNextDatasets),
- 1000
- );
- $(".js-dataset-triggered").each((i, cell) => {
- $(cell).on("mouseover", () => {
- const run = $(cell).children();
- const dagId = $(run).data("dag-id");
- const singleDatasetUri = $(run).data("uri");
- const setNextDatasets = (datasets, error) => {
- nextDatasets[dagId] = datasets;
- nextDatasetsError = error;
- };
- // Only update the tooltip info if there are multiple datasets
- if (!singleDatasetUri) {
- getTooltipInfo(dagId, run, setNextDatasets);
- }
- });
- });
|