You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by ag...@apache.org on 2022/10/02 15:14:31 UTC

[arrow-ballista] branch master updated: [UI] Add ability to view query plans directly in the UI (#301)

This is an automated email from the ASF dual-hosted git repository.

agrove pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-ballista.git


The following commit(s) were added to refs/heads/master by this push:
     new 2ae2d51c [UI] Add ability to view query plans directly in the UI (#301)
2ae2d51c is described below

commit 2ae2d51c93f0433627f58a277e257c7568ff222c
Author: Stefan Stanciulescu <71...@users.noreply.github.com>
AuthorDate: Sun Oct 2 08:14:25 2022 -0700

    [UI] Add ability to view query plans directly in the UI (#301)
    
    * Add support for viewing the graph in the browser. It does not work at the moment, as calling the d3.graphviz function fails due to the selector not being able to fetch the right node.
    
    * Fix formatting issues
    
    * Fix formatting issues
    
    * Cleanup - remove unused import and remove dependency that's not used
    
    * Add rust graphviz crate and generate svg directly from dot in the backend
    
    * Try some things that do not work
    
    * Add the graphviz crate to toml
    
    * Cleanup for the get_job_svg_graph API
    
    * Update the frontend part for viewing the graph as an SVG file
    
    * Make the UI look consistent with the previous version. Add an icon instead of a large button in a new column
---
 ballista/rust/scheduler/Cargo.toml                 |  1 +
 ballista/rust/scheduler/src/api/handlers.rs        | 27 +++++++++++
 ballista/rust/scheduler/src/api/mod.rs             |  8 +++-
 ballista/ui/scheduler/package.json                 |  1 +
 .../ui/scheduler/src/components/QueriesList.tsx    | 55 ++++++++++++++++++++--
 5 files changed, 87 insertions(+), 5 deletions(-)

diff --git a/ballista/rust/scheduler/Cargo.toml b/ballista/rust/scheduler/Cargo.toml
index 179240e4..954e5a48 100644
--- a/ballista/rust/scheduler/Cargo.toml
+++ b/ballista/rust/scheduler/Cargo.toml
@@ -50,6 +50,7 @@ datafusion-proto = { git = "https://github.com/apache/arrow-datafusion", rev = "
 etcd-client = { version = "0.9", optional = true }
 flatbuffers = { version = "2.1.2" }
 futures = "0.3"
+graphviz-rust = "0.3.0"
 http = "0.2"
 http-body = "0.4"
 hyper = "0.14.4"
diff --git a/ballista/rust/scheduler/src/api/handlers.rs b/ballista/rust/scheduler/src/api/handlers.rs
index 440a7ab8..fb42c22d 100644
--- a/ballista/rust/scheduler/src/api/handlers.rs
+++ b/ballista/rust/scheduler/src/api/handlers.rs
@@ -16,6 +16,9 @@ use ballista_core::serde::protobuf::job_status::Status;
 use ballista_core::serde::AsExecutionPlan;
 use ballista_core::BALLISTA_VERSION;
 use datafusion_proto::logical_plan::AsLogicalPlan;
+use graphviz_rust::cmd::{CommandArg, Format};
+use graphviz_rust::exec;
+use graphviz_rust::printer::PrinterContext;
 use warp::Rejection;
 
 #[derive(Debug, serde::Serialize)]
@@ -177,3 +180,27 @@ pub(crate) async fn get_job_dot_graph<T: AsLogicalPlan, U: AsExecutionPlan>(
         _ => Ok("Not Found".to_string()),
     }
 }
+
+/// Generate an SVG graph for the specified job id and return it as plain text
+pub(crate) async fn get_job_svg_graph<T: AsLogicalPlan, U: AsExecutionPlan>(
+    data_server: SchedulerServer<T, U>,
+    job_id: String,
+) -> Result<String, Rejection> {
+    let dot = get_job_dot_graph(data_server, job_id).await;
+    match dot {
+        Ok(dot) => {
+            let graph = graphviz_rust::parse(&dot);
+            if let Ok(graph) = graph {
+                exec(
+                    graph,
+                    &mut PrinterContext::default(),
+                    vec![CommandArg::Format(Format::Svg)],
+                )
+                .map_err(|_| warp::reject())
+            } else {
+                Ok("Cannot parse graph".to_string())
+            }
+        }
+        _ => Ok("Not Found".to_string()),
+    }
+}
diff --git a/ballista/rust/scheduler/src/api/mod.rs b/ballista/rust/scheduler/src/api/mod.rs
index 361f2861..ca621f5f 100644
--- a/ballista/rust/scheduler/src/api/mod.rs
+++ b/ballista/rust/scheduler/src/api/mod.rs
@@ -98,12 +98,16 @@ pub fn get_routes<T: AsLogicalPlan + Clone, U: 'static + AsExecutionPlan>(
         .and_then(|job_id, data_server| handlers::get_job_summary(data_server, job_id));
 
     let route_job_dot = warp::path!("api" / "job" / String / "dot")
-        .and(with_data_server(scheduler_server))
+        .and(with_data_server(scheduler_server.clone()))
         .and_then(|job_id, data_server| handlers::get_job_dot_graph(data_server, job_id));
+    let route_job_dot_svg = warp::path!("api" / "job" / String / "dot_svg")
+        .and(with_data_server(scheduler_server))
+        .and_then(|job_id, data_server| handlers::get_job_svg_graph(data_server, job_id));
 
     let routes = route_state
         .or(route_jobs)
         .or(route_job_summary)
-        .or(route_job_dot);
+        .or(route_job_dot)
+        .or(route_job_dot_svg);
     routes.boxed()
 }
diff --git a/ballista/ui/scheduler/package.json b/ballista/ui/scheduler/package.json
index a9cc3a0b..fa2462c1 100644
--- a/ballista/ui/scheduler/package.json
+++ b/ballista/ui/scheduler/package.json
@@ -19,6 +19,7 @@
     "react": "^17.0.1",
     "react-dom": "^17.0.1",
     "react-icons": "^4.2.0",
+    "react-inlinesvg": "^3.0.1",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
     "react-table": "^7.6.3",
diff --git a/ballista/ui/scheduler/src/components/QueriesList.tsx b/ballista/ui/scheduler/src/components/QueriesList.tsx
index 24069642..05a97802 100644
--- a/ballista/ui/scheduler/src/components/QueriesList.tsx
+++ b/ballista/ui/scheduler/src/components/QueriesList.tsx
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-import React from "react";
+import React, { useEffect, useState } from "react";
 import {
   CircularProgress,
   CircularProgressLabel,
@@ -25,11 +25,21 @@ import {
   Text,
   Flex,
   Box,
+  useDisclosure,
+  Button,
+  Modal,
+  ModalBody,
+  ModalCloseButton,
+  ModalContent,
+  ModalFooter,
+  ModalHeader,
+  ModalOverlay,
 } from "@chakra-ui/react";
 import { Column, DataTable, LinkCell } from "./DataTable";
 import { FaStop } from "react-icons/fa";
-import { GrDocumentDownload } from "react-icons/gr";
+import { GrDocumentDownload, GrOverview } from "react-icons/gr";
 import fileDownload from "js-file-download";
+import SVG from "react-inlinesvg";
 
 export enum QueryStatus {
   QUEUED = "QUEUED",
@@ -50,6 +60,27 @@ export interface QueriesListProps {
 }
 
 export const ActionsCell: (props: any) => React.ReactNode = (props: any) => {
+  const [dot_data, setData] = useState("");
+  const { isOpen, onOpen, onClose } = useDisclosure();
+  const ref = React.useRef<SVGElement>(null);
+
+  const dot_svg = (url: string) => {
+    fetch(url, {
+      method: "GET",
+      headers: {
+        Accept: "application/json",
+      },
+    }).then(async (res) => {
+      setData(await res.text());
+    });
+  };
+
+  useEffect(() => {
+    if (isOpen) {
+      dot_svg("/api/job/" + props.value + "/dot_svg");
+    }
+  }, [ref.current, dot_data, isOpen]);
+
   const handleDownload = (url: string, filename: string) => {
     fetch(url, {
       method: "GET",
@@ -74,6 +105,25 @@ export const ActionsCell: (props: any) => React.ReactNode = (props: any) => {
       >
         <GrDocumentDownload title={"Download DOT Plan"} />
       </button>
+      <Box mx={2}></Box>
+      <button onClick={onOpen}>
+        <GrOverview title={"View Graph"} />
+      </button>
+      <Modal isOpen={isOpen} size="small" onClose={onClose}>
+        <ModalOverlay />
+        <ModalContent>
+          <ModalHeader>Graph for {props.value} job</ModalHeader>
+          <ModalCloseButton />
+          <ModalBody margin="auto">
+            <SVG innerRef={ref} src={dot_data} width="auto" />
+          </ModalBody>
+          <ModalFooter>
+            <Button colorScheme="blue" mr={3} onClick={onClose}>
+              Close
+            </Button>
+          </ModalFooter>
+        </ModalContent>
+      </Modal>
     </Flex>
   );
 };
@@ -119,7 +169,6 @@ const getSkeletion = () => (
     <Skeleton height={5} />
     <Skeleton height={5} />
     <Skeleton height={5} />
-    <Skeleton height={5} />
   </>
 );