From fd3b06c96f91cb626faee17368bf0955d3b9b1c1 Mon Sep 17 00:00:00 2001 From: Jonathan Eagles Date: Wed, 25 Aug 2021 09:40:45 -0500 Subject: [PATCH] TEZ-4330. Import external tez component em-tgraph Co-authored-by: Sreenath Somarajapuram --- .../src/main/resources/META-INF/LICENSE.txt | 1 - .../main/webapp/app/components/em-tgraph.js | 101 ++ .../webapp/app/controllers/dag/swimlane.js | 2 +- tez-ui/src/main/webapp/app/styles/app.less | 1 + .../src/main/webapp/app/styles/em-tgraph.less | 393 +++++++ .../main/webapp/app/styles/em-tooltip.less | 1 + .../app/templates/components/em-tgraph.hbs | 111 ++ .../src/main/webapp/app/utils/fullscreen.js | 56 + .../webapp/app/utils/graph-data-processor.js | 761 ++++++++++++ .../src/main/webapp/app/utils/graph-view.js | 1024 +++++++++++++++++ tez-ui/src/main/webapp/app/utils/tip.js | 188 +++ tez-ui/src/main/webapp/package.json | 1 - .../integration/components/em-tgraph-test.js | 41 + .../tests/unit/utils/fullscreen-test.js | 30 + .../unit/utils/graph-data-processor-test.js | 30 + .../tests/unit/utils/graph-view-test.js | 32 + .../main/webapp/tests/unit/utils/tip-test.js | 32 + tez-ui/src/main/webapp/yarn.lock | 10 - 18 files changed, 2802 insertions(+), 13 deletions(-) create mode 100644 tez-ui/src/main/webapp/app/components/em-tgraph.js create mode 100644 tez-ui/src/main/webapp/app/styles/em-tgraph.less create mode 100644 tez-ui/src/main/webapp/app/templates/components/em-tgraph.hbs create mode 100644 tez-ui/src/main/webapp/app/utils/fullscreen.js create mode 100644 tez-ui/src/main/webapp/app/utils/graph-data-processor.js create mode 100644 tez-ui/src/main/webapp/app/utils/graph-view.js create mode 100644 tez-ui/src/main/webapp/app/utils/tip.js create mode 100644 tez-ui/src/main/webapp/tests/integration/components/em-tgraph-test.js create mode 100644 tez-ui/src/main/webapp/tests/unit/utils/fullscreen-test.js create mode 100644 tez-ui/src/main/webapp/tests/unit/utils/graph-data-processor-test.js create mode 100644 tez-ui/src/main/webapp/tests/unit/utils/graph-view-test.js create mode 100644 tez-ui/src/main/webapp/tests/unit/utils/tip-test.js diff --git a/tez-ui/src/main/resources/META-INF/LICENSE.txt b/tez-ui/src/main/resources/META-INF/LICENSE.txt index 608dc614c8..eea1ebc24b 100644 --- a/tez-ui/src/main/resources/META-INF/LICENSE.txt +++ b/tez-ui/src/main/resources/META-INF/LICENSE.txt @@ -231,7 +231,6 @@ The Apache TEZ tez-ui bundles the following files under the MIT License: - ember-bootstrap v0.5.1 (https://github.com/kaliber5/ember-bootstrap) - Copyright 2015 kaliber5 GmbH. - more-js v0.8.2 (https://github.com/sreenaths/snippet-ss) - snippet-ss v1.11.0 (https://github.com/sreenaths/snippet-ss) - - em-tgraph v0.0.4 (https://github.com/sreenaths/em-tgraph) - ember-cli-app-version v1.0.0 (https://github.com/EmberSherpa/ember-cli-app-version) - Authored by Taras Mankovski - ember-cli-auto-register v1.1.0 (https://github.com/williamsbdev/ember-cli-auto-register) - Copyright © 2015 Brandon Williams http://williamsbdev.com - ember-cli-content-security-policy v0.4.0 (https://github.com/rwjblue/ember-cli-content-security-policy) diff --git a/tez-ui/src/main/webapp/app/components/em-tgraph.js b/tez-ui/src/main/webapp/app/components/em-tgraph.js new file mode 100644 index 0000000000..5904637f7f --- /dev/null +++ b/tez-ui/src/main/webapp/app/components/em-tgraph.js @@ -0,0 +1,101 @@ +/** + * 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 Ember from 'ember'; + +import layout from '../templates/components/em-tgraph'; + +import fullscreen from '../utils/fullscreen'; +import GraphView from '../utils/graph-view'; +import GraphDataProcessor from '../utils/graph-data-processor'; + +export default Ember.Component.extend({ + + layout: layout, + + classNames: ['dag-view-container'], + + graphView: null, + + errMessage: null, + + isHorizontal: false, + hideAdditionals: false, + isFullscreen: false, + + styles: Ember.computed(function () { + var pathname = window.location.pathname, + safe = Ember.String.htmlSafe; + return { + vertex: safe(`fill: url(${pathname}#vertex-grad); filter: url(${pathname}#grey-glow)`), + input: safe(`fill: url(${pathname}#input-grad); filter: url(${pathname}#grey-glow)`), + output: safe(`fill: url(${pathname}#output-grad); filter: url(${pathname}#grey-glow)`), + task: safe(`fill: url(${pathname}#task-grad); filter: url(${pathname}#grey-glow)`), + io: safe(`fill: url(${pathname}#input-grad); filter: url(${pathname}#grey-glow)`), + group: safe(`fill: url(${pathname}#group-grad); filter: url(${pathname}#grey-glow)`), + }; + }), + + _onOrientationChange: function () { + }.observes('isHorizontal'), + + _onTglAdditionals: function () { + this.graphView.additionalDisplay(this.get('hideAdditionals')); + }.observes('hideAdditionals'), + + _onTglFullScreen: function () { + fullscreen.toggle(this.get('element')); + }.observes('isFullscreen'), + + actions: { + tglOrientation: function() { + var isTopBottom = this.graphView.toggleLayouts(); + this.set('isHorizontal', !isTopBottom); + }, + tglAdditionals: function() { + this.set('hideAdditionals', !this.get('hideAdditionals')); + }, + fullscreen: function () { + this.set('isFullscreen', !this.get('isFullscreen')); + }, + fitGraph: function () { + this.graphView.fitGraph(); + }, + configure: function () { + this.sendAction('configure'); + } + }, + + didInsertElement: function () { + var result = GraphDataProcessor.graphifyData(this.get('data')); + + this.graphView = GraphView.createNewGraphView(); + + if(typeof result === "string") { + this.set('errMessage', result); + } + else { + this.graphView.create( + this, + this.get('element'), + result + ); + } + } + +}); diff --git a/tez-ui/src/main/webapp/app/controllers/dag/swimlane.js b/tez-ui/src/main/webapp/app/controllers/dag/swimlane.js index 1fe2988858..9dfe883843 100644 --- a/tez-ui/src/main/webapp/app/controllers/dag/swimlane.js +++ b/tez-ui/src/main/webapp/app/controllers/dag/swimlane.js @@ -22,7 +22,7 @@ import MultiTableController from '../multi-table'; import ColumnDefinition from '../../utils/column-definition'; import VertexProcess from '../../utils/vertex-process'; -import fullscreen from 'em-tgraph/utils/fullscreen'; +import fullscreen from '../../utils/fullscreen'; export default MultiTableController.extend({ diff --git a/tez-ui/src/main/webapp/app/styles/app.less b/tez-ui/src/main/webapp/app/styles/app.less index 44bfb836e0..fc73918c70 100644 --- a/tez-ui/src/main/webapp/app/styles/app.less +++ b/tez-ui/src/main/webapp/app/styles/app.less @@ -43,6 +43,7 @@ @import "em-swimlane-vertex-name"; @import "em-table.less"; @import "em-table-status-cell"; +@import "em-tgraph"; @import "query-timeline"; @import "home-table-controls"; @import "em-progress"; diff --git a/tez-ui/src/main/webapp/app/styles/em-tgraph.less b/tez-ui/src/main/webapp/app/styles/em-tgraph.less new file mode 100644 index 0000000000..07a82a1240 --- /dev/null +++ b/tez-ui/src/main/webapp/app/styles/em-tgraph.less @@ -0,0 +1,393 @@ +/** + * 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 "bower_components/bootstrap/less/bootstrap"; +@import "bower_components/font-awesome/less/font-awesome"; + +@import "bower_components/snippet-ss/less/no"; + +@import "colors"; + +.fa-icon(@name) { + @content: "fa-var-@{name}"; + &:before {content: @@content} +} + +// -- HTML styles --- +.dag-view-container { + .no-select; + .well; + + min-height: 500px; + + background-color: @bg-liter; + + position: relative; + + padding: 0px; + width:100%; + height: 100%; + + .message { + text-align: center; + } + + .svg-container { + overflow:hidden; + + position: absolute; + left: 0px; + top: 0px; + right: 0px; + bottom: 0px; + + cursor: -moz-grab; + cursor: -webkit-grab; + cursor: grab; + + &.panning { + cursor: -moz-grabbing; + cursor: -webkit-grabbing; + cursor: grabbing; + } + + svg { + width: 100%; + height: 100%; + } + } + + .button-panel { + .no-select; + + position: absolute; + top: 10px; + right: 10px; + + // Toggle buttons + .tgl-orientation, .tgl-additionals, .config, .timeline, .fit-graph, .tgl-fullscreen { + .fa; + .fa-border; + font-size: 20px; + border-radius: 5px; + + cursor: pointer; + + border-color: @border-lite; + background-color: @bg-lite; + + &:hover { + color: @bg-lite; + background-color: @text-color; + } + } + .tgl-orientation { + .fa-icon(share-alt); + } + .tgl-additionals { + .fa-icon(circle); + + &.hide-additionals { + .fa-icon(circle-o); + } + } + .config { + .fa-icon(cog); + } + .timeline { + .fa-icon(clock-o); + } + .fit-graph { + .fa-icon(arrows-alt); + } + .tgl-fullscreen { + .fa-icon(expand); + } + + .seperator { + display: inline-block; + border-left: 1px dotted @text-color; + height: 13px; + } + } + + .fullscreen { + height: 100%; + margin: 0px; + .svg-container, svg { + height: 100%; + } + + .tgl-fullscreen { + .fa-icon(compress); + } + } + + &:-webkit-full-screen { + .fullscreen; + } + &:fullscreen { + .fullscreen; + } + &:-moz-full-screen { + .fullscreen; + } +} + +// -- SVG styles --- + +.grey-glow { + stroke: grey; +} + +.vertex-node-bg { + .grey-glow; +} + +.input-node-bg { + .grey-glow; +} + +.output-node-bg { + .grey-glow; +} + +.task-bubble-bg { + .grey-glow; +} + +.group-bubble-bg { + .grey-glow; +} + +.node { + cursor: pointer; + + text { + .no-select; + + pointer-events: none; + font: 11px sans-serif; + text-anchor: middle; + + // Ensure to manually change the transforms in graph-view.js for IE compatibility + -webkit-transform: translate(0px, 4px); // For safari + -moz-transform: translate(0px, 4px); + transform: translate(0px, 4px); + } +} + +.vertex { + text.title { + // Ensure to manually change the transforms in graph-view.js for IE compatibility + -webkit-transform: translate(0px, 3px); // For safari + transform: translate(0px, -1px); + } + + .task-bubble { + // Ensure to manually change the transforms in graph-view.js for IE compatibility + -webkit-transform: translate(38px, -15px); + transform: translate(38px, -15px); + text { + letter-spacing: -1px; + text-anchor: middle; + } + } + + .io-bubble { + // Ensure to manually change the transforms in graph-view.js for IE compatibility + -webkit-transform: translate(-38px, -15px); + transform: translate(-38px, -15px); + opacity: 0; + //pointer-events: none; + + -moz-transition: opacity .5s ease-in-out; + -webkit-transition: opacity .5s ease-in-out; + transition: opacity .5s ease-in-out; + + text { + text-anchor: middle; + } + } + + .group-bubble { + // Ensure to manually change the transforms in graph-view.js for IE compatibility + -webkit-transform: translate(38px, 15px); + transform: translate(38px, 15px); + } + + .status-bar { + pointer-events: none; + + .status { + -webkit-transform: translate(-35px, 2px); + transform: translate(-35px, 2px); + font: 8px Helvetica; + text-align: center; + + .msg-container { + border-radius: 5px; + padding: 2px 3px 0px 1px; + background-color: rgba(255, 255, 255, 0.3); + + .task-status { + -webkit-animation: none !important; + font-size: 11px; + margin-top: 3px; + background-color: rgba(255, 255, 255, 0.8); + border-radius: 6px; + vertical-align: -1px; + + width: 8px; + height: 7px; + + &.running { + font-size: 11px; + + width: 9px; + height: 7px + } + } + } + } + } +} + +.hide-io { + .vertex { + .io-bubble { + opacity: 1; + pointer-events: auto; + } + } +} + +.link { + fill: none; + stroke: #ccc; + stroke-width: 3px; + + cursor: default; + + &.broadcast { + stroke: #ccbb8f; + } +} + +// -- Tooltip style --- +.tool-tip { + .no-select; + + position: fixed; + pointer-events: none; + display: none; + + max-width: 820px; + + .sub { + font-size: 10px; + } + + .bubble { + margin-left: -11px; // Border radious + arrow margin-left + + position: relative; + padding: 10px; + + font-family: helvetica; + background: rgba(0, 0, 0, 0.8); + color: #fff; + border-radius: 5px; + + .tip-title { + text-align: center; + font-size: 1.1em; + } + .tip-text { + border-top: 1px solid rgba(255, 255, 255, 0.4); + text-align: center; + margin-bottom: -1px; + } + .tip-list { + table { + table-layout:fixed; + background: transparent; + + border-top: 1px solid rgba(255, 255, 255, 0.4); + + td { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 400px; + } + td:nth-child(1) { + padding-right: 10px; + } + td:nth-child(2) { + text-align: right; + padding-left: 10px; + border-left: 1px solid rgba(255, 255, 255, 0.4); + } + } + } + } + + &.show { + display: inline-block; + } + + &.below:after, &.above:before { + display: inline; + box-sizing: border-box; + + font-size: 12px; + line-height: 9px; + + color: rgba(0, 0, 0, 0.8); + margin-left: -6px; // Half of font size + } + + &.above { + margin-top: 10px; + .bubble { + margin-top: -5px; + } + + &:before { + content: "\25B2"; + } + } + + &.below { + margin-top: -12px; + .bubble { + margin-bottom: -7px; + } + + &:after { + content: "\25BC"; + } + } +} + +.dag-view-legend { + margin-top: -20px; + font-size: .7em; + text-align: right; +} diff --git a/tez-ui/src/main/webapp/app/styles/em-tooltip.less b/tez-ui/src/main/webapp/app/styles/em-tooltip.less index 3f7ed87c7b..1cbb836001 100644 --- a/tez-ui/src/main/webapp/app/styles/em-tooltip.less +++ b/tez-ui/src/main/webapp/app/styles/em-tooltip.less @@ -63,6 +63,7 @@ table { display: inline; table-layout:fixed; + background-color:transparent; td { overflow: hidden; diff --git a/tez-ui/src/main/webapp/app/templates/components/em-tgraph.hbs b/tez-ui/src/main/webapp/app/templates/components/em-tgraph.hbs new file mode 100644 index 0000000000..73be06e6c7 --- /dev/null +++ b/tez-ui/src/main/webapp/app/templates/components/em-tgraph.hbs @@ -0,0 +1,111 @@ +{{! + * 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. +}} + +{{#if errMessage}} +
+

Rendering failed!

{{errMessage}}
+
+{{else}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + +
+
+
+
Title
+
+
+
+
+
+{{/if}} diff --git a/tez-ui/src/main/webapp/app/utils/fullscreen.js b/tez-ui/src/main/webapp/app/utils/fullscreen.js new file mode 100644 index 0000000000..ff2b2a0f46 --- /dev/null +++ b/tez-ui/src/main/webapp/app/utils/fullscreen.js @@ -0,0 +1,56 @@ +/** + * 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. + */ + +function inFullscreenMode () { + return document.fullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + document.msFullscreenElement; +} + +export default { + + inFullscreenMode: inFullscreenMode, + + toggle: function (element) { + if (inFullscreenMode()) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } else { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } + } + + return inFullscreenMode(); + } + +}; diff --git a/tez-ui/src/main/webapp/app/utils/graph-data-processor.js b/tez-ui/src/main/webapp/app/utils/graph-data-processor.js new file mode 100644 index 0000000000..849c84eb39 --- /dev/null +++ b/tez-ui/src/main/webapp/app/utils/graph-data-processor.js @@ -0,0 +1,761 @@ +/** + * 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 Ember from 'ember'; + +/** + * The data processing part of Dag View. + * + * Converts raw DAG-plan into an internal data representation as shown below. + * Data processor exposes just a functions and an enum to the outside world, everything else + * happens inside the main closure: + * - types (Enum of node types) + * - graphifyData + * + * Links, Edges: + * -------------- + * d3 layout & graph-view uses the term links, and dag-plan uses edges. Hence you would + * see both getting used in this file. + * + * Graphify Data + * ------------- + * graphifyData function is a translator that translates the dagPlan object send by timeline server + * into another object that graph-view and in turn d3.layout.tree can digest. + * + * Input object(dag-plan as it is from the timeline server): + * { + * dagName, version, + * vertices: [ // Array of vertex objects with following properties + * { + * vertexName, processorClass, outEdgeIds {Array}, additionalInputs {Array} + * } + * ], + * edges: [ // Array of edge objects with following properties + * { + * edgeId, inputVertexName, outputVertexName, dataMovementType, dataSourceType + * schedulingType, edgeSourceClass, edgeDestinationClass + * } + * ], + * vertexGroups: [ // Array of vectorGroups objects with following properties + * { + * groupName, groupMembers {Array}, edgeMergedInputs {Array} + * } + * ] + * } + * + * Output object: + * We are having a graph that must be displayed like a tree. Hence data processor was created + * to make a tree structure out of the available data. The tree structure is made by creating + * DataNodes instances and populating their children array with respective child DataNodes + * - tree: Represents the tree structure with each node being a DataNodes instance + * - links: Represents the connections between the nodes to create the graph + * { + * tree: { // This object points to the RootDataNode instance + * children {Array} // Array of DataNodes under the node, as expected by d3.layout.tree + * + Other custom properties including data that needs to be displayed + * } + * links: [ // An array of all links in the tree + * { + * sourceId // Source vertex name + * targetId // Target vertex name + * + Other custom properties including data to be displayed + * } + * ] + * maxDepth, leafCount + * } + * + * Data Nodes: + * ----------- + * To make the implementation simpler each node in the graph will be represented as an + * instance of any of the 4 inherited classes of Data Node abstract class. + * DataNode + * |-- RootDataNode + * |-- VertexDataNode + * |-- InputDataNode + * +-- OutputDataNode + * + * Extra Nodes: + * ------------ + * Root Node (Invisible): + * Dag view support very complex DAGs, even those without interconnections and backward links. + * Hence to fit it into a tree layout I have inserted an invisible root node. + * + * Dummy Node (Invisible): + * Sinks of a vertex are added at the same level of its parent node, Hence to ensure that all + * nodes come under the root, a dummy node was added as the child of the root. The visible tree + * would be added as child of dummy node. + * Dummy also ensures the view symmetry when multiple outputs are present at the dummy level. + * + * Sample Structure, inverted tree representation: + * + * As in the view + * + * Source_m1 + * | + * Source_m2 M1----------+ + * | | | + * +-----------M2 Sink_M1 + * | + * +-----------R1----------+ + * | | + * Sink1_R1 Sink2_R1 + * + * + * Internal representation + * + * Source_m1 + * | + * Source_m2 M1 + * | | + * +-----------M2 Sink_M1 + * | | + * R1----------+ + * | + * Sink1_R1 Dummy Sink2_R1 + * | | | + * +-----------+-----------+ + * | + * Root + * + * Internal data representation + * + * Root + * | + * +-- children[Sink1_R1, Dummy, Sink2_R1] + * | + * +-- children[R1] + * | + * +-- children[M2, Sink_M1] + * | + * +-- children[Source_m2, M1] + * | + * +-- children[Source_m1] + * + * Steps: + * ------ + * The job is done in 4 steps, and is modularized using 4 separate recursive functions. + * 1. _treefyData : Get the tree structure in place with vertices and inputs/sources + * 2. _addOutputs : Add outputs/sinks. A separate step had to be created as outputs + * are represented in the opposite direction of inputs. + * 3. _cacheChildren : Make a cache of children in allChildren property for later use + * 4. _getGraphDetails : Get a graph object with all the required details + * + */ + +/** + * Enum of various node types + */ +var types = { + ROOT: 'root', + DUMMY: 'dummy', + VERTEX: 'vertex', + INPUT: 'input', + OUTPUT: 'output' +}; + +/** + * Iterates the array in a symmetric order, from middle to outwards + * @param array {Array} Array to be iterated + * @param callback {Function} Function to be called for each item + * @return A new array created with value returned by callback + */ +function centericMap(array, callback) { + var retArray = [], + length, + left, right; + + if(array) { + length = array.length - 1; + left = length >> 1; + + while(left >= 0) { + retArray[left] = callback(array[left]); + right = length - left; + if(right !== left) { + retArray[right] = callback(array[right]); + } + left--; + } + } + return retArray; +} + +/** + * Abstract class for all types of data nodes + */ +var DataNode = Ember.Object.extend({ + init: function (data) { + this._super(data); + this._init(data); + }, + _init: function () { + // Initialize data members + this.setProperties({ + /** + * Children that would be displayed in the view, to hide a child it would be removed from this array. + * Not making this a computed property because - No d3 support, low performance. + */ + children: null, + allChildren: null, // All children under this node + treeParent: null, // Direct parent DataNode in our tree structure + }); + }, + + /** + * Private function. + * Set the child array as it is. Created because of performance reasons. + * @param children {Array} Array to be set + */ + _setChildren: function (children) { + this.set('children', children && children.length > 0 ? children : null); + }, + /** + * Public function. + * Set the child array after filtering + * @param children {Array} Array of DataNodes to be set + */ + setChildren: function (children) { + var allChildren = this.get('allChildren'); + if(allChildren) { + this._setChildren(allChildren.filter(function (child) { + return children.indexOf(child) !== -1; // true if child is in children + })); + } + }, + /** + * Filter out the given children from the children array. + * @param childrenToRemove {Array} Array of DataNodes to be removed + */ + removeChildren: function (childrenToRemove) { + var children = this.get('children'); + if(children) { + children = children.filter(function (child) { + return childrenToRemove.indexOf(child) === -1; // false if child is in children + }); + this._setChildren(children); + } + }, + + /** + * Return true if this DataNode is same as or the ancestor of vertex + * @param vertex {DataNode} + */ + isSelfOrAncestor: function (vertex) { + while(vertex) { + if(vertex === this){ + return true; + } + vertex = vertex.treeParent; + } + return false; + }, + + /** + * If the property is available, expects it to be an array and iterate over + * its elements using the callback. + * @param vertex {DataNode} + * @param callback {Function} + * @param thisArg {} Will be value of this inside the callback + */ + ifForEach: function (property, callback, thisArg) { + if(this.get(property)) { + this.get(property).forEach(callback, thisArg); + } + }, + /** + * Recursively call the function specified in all children + * its elements using the callback. + * @param functionName {String} Name of the function to be called + */ + recursivelyCall: function (functionName) { + if(this[functionName]) { + this[functionName](); + } + this.ifForEach('children', function (child) { + child.recursivelyCall(functionName); + }); + } + }), + RootDataNode = DataNode.extend({ + type: types.ROOT, + vertexName: 'root', + dummy: null, // Dummy node used in the tree, check top comments for explanation + depth: 0, // Depth of the node in the tree structure + + _init: function () { + this._setChildren([this.get('dummy')]); + } + }), + VertexDataNode = DataNode.extend({ + type: types.VERTEX, + + _additionalsIncluded: true, + + _init: function () { + this._super(); + + // Initialize data members + this.setProperties({ + id: this.get('vertexName'), + inputs: [], // Array of sources + outputs: [] // Array of sinks + }); + + this.ifForEach('additionalInputs', function (input) { + this.inputs.push(InputDataNode.instantiate(this, input)); + }, this); + + this.ifForEach('additionalOutputs', function (output) { + this.outputs.push(OutputDataNode.instantiate(this, output)); + }, this); + }, + + /** + * Sets depth of the vertex and all its input children + * @param depth {Number} + */ + setDepth: function (depth) { + this.set('depth', depth); + + depth++; + this.get('inputs').forEach(function (input) { + input.set('depth', depth); + }); + }, + + /** + * Sets vertex tree parents + * @param parent {DataNode} + */ + setParent: function (parent) { + this.set('treeParent', parent); + }, + + /** + * Include sources and sinks in the children list, so that they are displayed + */ + includeAdditionals: function() { + this.setChildren(this.get('inputs').concat(this.get('children') || [])); + + var ancestor = this.get('parent.parent'); + if(ancestor) { + ancestor.setChildren(this.get('outputs').concat(ancestor.get('children') || [])); + } + this.set('_additionalsIncluded', true); + }, + /** + * Exclude sources and sinks in the children list, so that they are hidden + */ + excludeAdditionals: function() { + this.removeChildren(this.get('inputs')); + + var ancestor = this.get('parent.parent'); + if(ancestor) { + ancestor.removeChildren(this.get('outputs')); + } + this.set('_additionalsIncluded', false); + }, + /** + * Toggle inclusion/display of sources and sinks. + */ + toggleAdditionalInclusion: function () { + var include = !this.get('_additionalsIncluded'); + this.set('_additionalsIncluded', include); + + if(include) { + this.includeAdditionals(); + } + else { + this.excludeAdditionals(); + } + } + }), + InputDataNode = Ember.$.extend(DataNode.extend({ + type: types.INPUT, + vertex: null, // The vertex DataNode to which this node is linked + + _init: function () { + var vertex = this.get('vertex'); + this._super(); + + // Initialize data members + this.setProperties({ + id: vertex.get('vertexName') + this.get('name'), + depth: vertex.get('depth') + 1 + }); + } + }), { + /** + * Initiate an InputDataNode + * @param vertex {DataNode} + * @param data {Object} + */ + instantiate: function (vertex, data) { + return InputDataNode.create(Ember.$.extend(data, { + treeParent: vertex, + vertex: vertex + })); + } + }), + OutputDataNode = Ember.$.extend(DataNode.extend({ + type: types.OUTPUT, + vertex: null, // The vertex DataNode to which this node is linked + + _init: function (/*data*/) { + this._super(); + + // Initialize data members + this.setProperties({ + id: this.get('vertex.vertexName') + this.get('name') + }); + } + }), { + /** + * Initiate an OutputDataNode + * @param vertex {DataNode} + * @param data {Object} + */ + instantiate: function (vertex, data) { + /** + * We will have an idea about the treeParent & depth only after creating the + * tree structure. + */ + return OutputDataNode.create(Ember.$.extend(data, { + vertex: vertex + })); + } + }); + +var _data = null; // Raw dag plan data + +/** + * Step 1: Recursive + * Creates primary skeletal structure with vertices and inputs as nodes, + * All child vertices & inputs will be added to an array property named children + * As we are trying to treefy graph data, nodes might reoccur. Reject if its in + * the ancestral chain, and if the new depth is lower than the old + * reposition the node. + * + * @param vertex {VertexDataNode} Root vertex of current sub tree + * @param depth {Number} Depth of the passed vertex + * @param vertex {VertexDataNode} + */ +function _treefyData(vertex, depth) { + var children, + parentChildren; + + depth++; + + children = centericMap(vertex.get('inEdgeIds'), function (edgeId) { + var child = _data.vertices.get(_data.edges.get(edgeId).get('inputVertexName')); + if(!child.isSelfOrAncestor(vertex)) { + if(child.depth) { + var siblings = child.get('outEdgeIds'); + var shouldCompress = siblings ? siblings.length <= 2 : true; + var shouldDecompress = siblings ? siblings.length > 2 : false; + if((shouldCompress && child.depth > (depth + 1)) || (shouldDecompress && child.depth < (depth + 1))) { + parentChildren = child.get('treeParent.children'); + if(parentChildren) { + parentChildren.removeObject(child); + } + } + else { + return child; + } + } + child.setParent(vertex); + return _treefyData(child, depth); + } + }); + + // Remove undefined entries + children = children.filter(function (child) { + return child; + }); + + vertex.setDepth(depth); + + // Adds a dummy child to intermediate inputs so that they + // gets equal relevance as adjacent nodes on plotting the tree! + if(children.length) { + vertex.ifForEach('inputs', function (input) { + input._setChildren([DataNode.create()]); + }); + } + + children.push.apply(children, vertex.get('inputs')); + + vertex._setChildren(children); + return vertex; +} + +/** + * Part of step 1 + * To remove recurring vertices in the tree + * @param vertex {Object} root vertex + */ +function _normalizeVertexTree(vertex) { + var children = vertex.get('children'); + + if(children) { + children = children.filter(function (child) { + _normalizeVertexTree(child); + return child.get('type') !== 'vertex' || child.get('treeParent') === vertex; + }); + + vertex._setChildren(children); + } + + return vertex; +} + +/** + * Step 2: Recursive awesomeness + * Attaches outputs into the primary structure created in step 1. As outputs must be represented + * in the same level of the vertex's parent. They are added as children of its parent's parent. + * + * The algorithm is designed to get a symmetric display of output nodes. + * A call to the function will iterate through all its children, and inserts output nodes at the + * position that best fits the expected symmetry. + * + * @param vertex {VertexDataNode} + * @return {Object} Nodes that would come to the left and right of the vertex. + */ +function _addOutputs(vertex) { + var childVertices = vertex.get('children'), + childrenWithOutputs = [], + left = [], + right = []; + + // For a symmetric display of output nodes + if(childVertices && childVertices.length) { + var middleChildIndex = Math.floor((childVertices.length - 1) / 2); + + childVertices.forEach(function (child, index) { + var additionals = _addOutputs(child); + var downstream = child.get('outEdgeIds'); + var outputs = child.get('outputs'); + + if (!(outputs && outputs.length) || downstream) { + childrenWithOutputs.push.apply(childrenWithOutputs, additionals.left); + childrenWithOutputs.push(child); + childrenWithOutputs.push.apply(childrenWithOutputs, additionals.right); + } + if(outputs && outputs.length) { + var middleOutputIndex = Math.floor((outputs.length - 1) / 2); + if (downstream) { + if(index < middleChildIndex) { + left.push.apply(left, outputs); + } + else if(index > middleChildIndex) { + right.push.apply(right, outputs); + } + else { + left.push.apply(left, outputs.slice(0, middleOutputIndex + 1)); + right.push.apply(right, outputs.slice(middleOutputIndex + 1)); + } + } + else { + outputs.forEach(function (output, index) { + output.depth = vertex.depth; + if (index === middleOutputIndex) { + var outputChildren = []; + outputChildren.push.apply(outputChildren, additionals.left); + outputChildren.push(child); + outputChildren.push.apply(outputChildren, additionals.right); + output._setChildren(outputChildren); + } + childrenWithOutputs.push(output); + }); + } + } + }); + + vertex._setChildren(childrenWithOutputs); + } + return { + left: left, + right: right + }; +} + +/** + * Step 3: Recursive + * Create a copy of all possible children in allChildren for later use + * @param node {DataNode} + */ +function _cacheChildren(node) { + var children = node.get('children'); + if(children) { + node.set('allChildren', children); + children.forEach(_cacheChildren); + } +} + +/** + * Return an array of the incoming edges/links and input-output source-sink edges of the node. + * @param node {DataNode} + * @return links {Array} Array of all incoming and input-output edges of the node + */ +function _getLinks(node) { + var links = []; + + node.ifForEach('inEdgeIds', function (inEdge) { + var edge = _data.edges.get(inEdge); + edge.setProperties({ + sourceId: edge.get('inputVertexName'), + targetId: edge.get('outputVertexName') + }); + links.push(edge); + }); + + if(node.type === types.INPUT) { + links.push(Ember.Object.create({ + sourceId: node.get('id'), + targetId: node.get('vertex.id') + })); + } + else if(node.type === types.OUTPUT) { + links.push(Ember.Object.create({ + sourceId: node.get('vertex.id'), + targetId: node.get('id') + })); + } + + return links; +} + +/** + * Step 4: Recursive + * Create a graph based on the given tree structure and edges in _data object. + * @param tree {DataNode} + * @param details {Object} Object with values tree, links, maxDepth & maxHeight + */ +function _getGraphDetails(tree) { + var maxDepth = 0, + leafCount = 0, + + links = _getLinks(tree); + + tree.ifForEach('children', function (child) { + var details = _getGraphDetails(child); + maxDepth = Math.max(maxDepth, details.maxDepth); + leafCount += details.leafCount; + + links.push.apply(links, details.links); + }); + + if(!tree.get('children')) { + leafCount++; + } + + return { + tree: tree, + links: links, + maxDepth: maxDepth + 1, + leafCount: leafCount + }; +} + +/** + * Converts vertices & edges into hashes for easy access. + * Makes vertexGroup a property of the respective vertices. + * @param data {Object} + * @return {Object} An object with vertices hash, edges hash and array of root vertices. + */ +function _normalizeRawData(data) { + var EmObj = Ember.Object, + vertices, // Hash of vertices + edges, // Hash of edges + rootVertices = []; // Vertices without out-edges are considered root vertices + + vertices = data.vertices.reduce(function (obj, vertex) { + vertex = VertexDataNode.create(vertex); + if(!vertex.outEdgeIds) { + rootVertices.push(vertex); + } + obj[vertex.vertexName] = vertex; + return obj; + }, {}); + + edges = !data.edges ? [] : data.edges.reduce(function (obj, edge) { + obj[edge.edgeId] = EmObj.create(edge); + return obj; + }, {}); + + if(data.vertexGroups) { + data.vertexGroups.forEach(function (group) { + group.groupMembers.forEach(function (vertex) { + vertices[vertex].vertexGroup = EmObj.create(group); + }); + }); + } + + return { + vertices: EmObj.create(vertices), + edges: EmObj.create(edges), + rootVertices: rootVertices + }; +} + +var GraphDataProcessor = { + // Types enum + types: types, + + /** + * Converts raw DAG-plan into an internal data representation that graph-view, + * and in turn d3.layout.tree can digest. + * @param data {Object} Dag-plan data + * @return {Object/String} + * - Object with values tree, links, maxDepth & maxHeight + * - Error message if the data was not as expected. + */ + graphifyData: function (data) { + var dummy = DataNode.create({ + type: types.DUMMY, + vertexName: 'dummy', + depth: 1 + }), + root = RootDataNode.create({ + dummy: dummy + }); + + if(!data.vertices) { + return "Vertices not found!"; + } + + _data = _normalizeRawData(data); + + if(!_data.rootVertices.length) { + return "Sink vertex not found!"; + } + + dummy._setChildren(centericMap(_data.rootVertices, function (vertex) { + return _normalizeVertexTree(_treefyData(vertex, 2)); + })); + + _addOutputs(root); + + _cacheChildren(root); + + return _getGraphDetails(root); + } +}; + +// TODO - Convert to pure ES6 style export without using an object +export default GraphDataProcessor; diff --git a/tez-ui/src/main/webapp/app/utils/graph-view.js b/tez-ui/src/main/webapp/app/utils/graph-view.js new file mode 100644 index 0000000000..7f1b08dede --- /dev/null +++ b/tez-ui/src/main/webapp/app/utils/graph-view.js @@ -0,0 +1,1024 @@ +/** + * 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 d3*/ + +import Ember from 'ember'; +import moment from 'moment'; + +import GraphDataProcessor from './graph-data-processor'; +import Tip from './tip'; + +var isIE = navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0; + +/** + * The view part of Dag View. + * + * Displays TEZ DAG graph in a tree layout. (Uses d3.layout.tree) + * Graph view exposes just 4 functions to the outside world, everything else + * happens inside the main closure: + * 1. create + * 2. fitGraph + * 3. additionalDisplay + * 4. toggleLayouts + * + * Links, Paths: + * -------------- + * d3 layout uses the term links, and SVG uses path. Hence you would see both getting used + * in this file. You can consider link to be a JavaScript data object, and path to be a visible + * SVG DOM element on the screen. + * + * Extra Nodes: + * ------------ + * Root Node (Invisible): + * Dag view support very complex DAGs, even DAGs without interconnections and backward links. + * Hence to fit it into a tree layout I have inserted an invisible root node. + * + * Dummy Node (Invisible): + * Sinks of a vertex are added at the same level of its parent node, Hence to ensure that all + * nodes come under the root, a dummy node was added as the child of the root. The visible tree + * would be added as child of dummy node. + * Dummy also ensures the view symmetry when multiple outputs are present at the dummy level. + * + * Sample Structure, inverted tree representation: + * + * As in the view + * + * Source_m1 + * | + * Source_m2 M1----------+ + * | | | + * +-----------M2 Sink_M1 + * | + * +-----------R1----------+ + * | | + * Sink1_R1 Sink2_R1 + * + * + * Internal representation + * + * Source_m1 + * | + * Source_m2 M1 + * | | + * +-----------M2 Sink_M1 + * | | + * R1----------+ + * | + * Sink1_R1 Dummy Sink2_R1 + * | | | + * +-----------+-----------+ + * | + * Root + * + */ + +function createNewGraphView() { +var PADDING = 30, // Adding to be applied on the svg view + + LAYOUTS = { // The view supports two layouts - left to right and top to bottom. + leftToRight: { + hSpacing: 180, // Horizontal spacing between nodes + vSpacing: 70, // Vertical spacing between nodes + depthSpacing: 180, // In leftToRight depthSpacing = hSpacing + linkDelta: 30, // Used for links starting and ending at the same point + projector: function (x, y) { // Converts coordinate based on current orientation + return {x: y, y: x}; + }, + // Defines how path between nodes are drawn + pathFormatter: function (mx, my, q1x1, q1y1, q1x, q1y, q2x1, q2y1, q2x, q2y ) { + return `M ${mx} ${my} Q ${q1x1} ${q1y1} ${q1x} ${q1y} Q ${q2x1} ${q2y1} ${q2x} ${q2y}`; + } + }, + topToBottom: { + hSpacing: 120, + vSpacing: 100, + depthSpacing: 100, // In topToBottom depthSpacing = vSpacing + linkDelta: 15, + projector: function (x, y) { + return {x: x, y: y}; + }, + pathFormatter: function (mx, my, q1x1, q1y1, q1x, q1y, q2x1, q2y1, q2x, q2y ) { + return `M ${my} ${mx} Q ${q1y1} ${q1x1} ${q1y} ${q1x} Q ${q2y1} ${q2x1} ${q2y} ${q2x}`; + } + } + }, + + DURATION = 750, // Animation duration + + HREF_TYPE_HASH = { // Used to assess the entity type from an event target + "#task-bubble": "task", + "#vertex-bg": "vertex", + "#input-bg": "input", + "#output-bg": "output", + "#io-bubble": "io", + "#group-bubble": "group" + }; + +var _width = 0, + _height = 0, + + _component = null, // The parent ember component + _data = null, // Data object created by data processor + _treeData = null, // Points to root data node of the tree structure + _treeLayout = null, // Instance of d3 tree layout helper + _layout = null, // Current layout, one of the values defined in LAYOUTS object + + _svg = null, // jQuery instance of svg DOM element + _g = null, // For pan and zoom: Svg DOM group element that encloses all the displayed items + + _idCounter = 0, // To create a fresh id for all displayed nodes + _scheduledClickId = 0, // Id of scheduled click, used for double click. + + _tip, // Instance of tip.js + + _panZoomValues, // Temporary storage of pan zoom values for fit toggle + _panZoom; // A closure returned by _attachPanZoom to reset/modify pan and zoom values + +/** + * Texts grater than maxLength will be trimmed. + * @param text {String} Text to trim + * @param maxLength {Number} + * @return Trimmed text + */ +function _trimText(text, maxLength) { + if(text) { + text = text.toString(); + if(text.length > maxLength) { + text = text.substr(0, maxLength - 1) + '..'; + } + } + return text; +} + +/** + * IE 11 does not support css transforms on svg elements. So manually set the same. + * please keep the transform parameters in sync with the ones in dag-view.less + * See https://connect.microsoft.com/IE/feedbackdetail/view/920928 + * + * This can be removed once the bug is fixed in all supported IE versions + * @param value + */ +function translateIfIE(element, x, y) { + // Todo - pass it as option + if(isIE) { + element.attr('transform', `translate(${x}, ${y})`); + } +} + +/** + * Add task bubble to a vertex node + * @param node {SVG DOM element} Vertex node + * @param d {VertexDataNode} + */ +function _addTaskBubble(node, d) { + var group = node.append('g'); + group.attr('class', 'task-bubble'); + group.append('use').attr('xlink:href', '#task-bubble'); + translateIfIE(group.append('text') + .text(_trimText(d.get('data.totalTasks') || 0, 3)), 0, 4); + + translateIfIE(group, 38, -15); +} +/** + * Add IO(source/sink) bubble to a vertex node + * @param node {SVG DOM element} Vertex node + * @param d {VertexDataNode} + */ +function _addIOBubble(node, d) { + var group, + inputs = d.get('inputs.length'), + outputs = d.get('outputs.length'); + + if(inputs || outputs) { + group = node.append('g'); + group.attr('class', 'io-bubble'); + group.append('use').attr('xlink:href', '#io-bubble'); + translateIfIE(group.append('text') + .text(_trimText(`${inputs}/${outputs}`, 3)), 0, 4); + + translateIfIE(group, -38, -15); + } +} +/** + * Add vertex group bubble to a vertex node + * @param node {SVG DOM element} Vertex node + * @param d {VertexDataNode} + */ +function _addVertexGroupBubble(node, d) { + var group; + + if(d.vertexGroup) { + group = node.append('g'); + group.attr('class', 'group-bubble'); + group.append('use').attr('xlink:href', '#group-bubble'); + translateIfIE(group.append('text') + .text(_trimText(d.get('vertexGroup.groupMembers.length'), 2)), 0, 4); + + translateIfIE(group, 38, 15); + } +} +/** + * Add status bar to a vertex node + * @param node {SVG DOM element} Vertex node + * @param d {VertexDataNode} + */ +function _addStatusBar(node, d) { + var group = node.append('g'); + group.attr('class', 'status-bar'); + + group.append('foreignObject') + .attr("class", "status") + .attr("width", 70) + .attr("height", 15) + .html('' + + d.get('data.status') + + '' + ); +} +/** + * Creates a base SVG DOM node, with bg and title based on the type of DataNode + * @param node {SVG DOM element} Vertex node + * @param d {DataNode} + * @param titleProperty {String} d's property who's value is the title to be displayed. + * By default 'name'. + * @param maxTitleLength {Number} Title would be trimmed beyond maxTitleLength. By default 3 chars + */ +function _addBasicContents(node, d, titleProperty, maxTitleLength) { + var className = d.type; + + node.attr('class', `node ${className}`); + node.append('use').attr('xlink:href', `#${className}-bg`); + translateIfIE(node.append('text') + .attr('class', 'title') + .text(_trimText(d.get(titleProperty || 'name'), maxTitleLength || 12)), 0, 4); +} +/** + * Populates the calling node with the required content. + * @param s {DataNode} + */ +function _addContent(d) { + var node = d3.select(this); + + switch(d.type) { + case 'vertex': + _addBasicContents(node, d, 'vertexName'); + _addStatusBar(node, d); + _addTaskBubble(node, d); + _addIOBubble(node, d); + _addVertexGroupBubble(node, d); + break; + case 'input': + case 'output': + _addBasicContents(node, d); + break; + } +} + +/** + * Create a list of all links connecting nodes in the given array. + * @param nodes {Array} A list of d3 nodes created by tree layout + * @return links {Array} All links between nodes in the current DAG + */ +function _getLinks(nodes) { + var links = [], + nodeHash; + + nodeHash = nodes.reduce(function (obj, node) { + obj[node.id] = node; + return obj; + }, {}); + + _data.links.forEach(function (link) { + var source = nodeHash[link.sourceId], + target = nodeHash[link.targetId]; + if(source && target) { + link.setProperties({ + source: source, + target: target, + isBackwardLink: source.isSelfOrAncestor(target) + }); + links.push(link); + } + }); + + return links; +} + +/** + * Apply proper depth spacing and remove the space occupied by dummy node + * if the number of other nodes are odd. + * @param nodes {Array} A list of d3 nodes created by tree layout + */ +function _normalize(nodes) { + // Set layout + var farthestY = 0; + nodes.forEach(function (d) { + d.y = d.depth * -_layout.depthSpacing; + if(d.y < farthestY) { + farthestY = d.y; + } + }); + + farthestY -= PADDING; + nodes.forEach(function (d) { + d.y -= farthestY; + }); + + //Remove space occupied by dummy + var rootChildren = _treeData.get('children'), + rootChildCount = rootChildren.length, + dummyIndex, + i; + + if(rootChildCount % 2 === 0) { + dummyIndex = rootChildren.indexOf(_treeData.get('dummy')); + if(dummyIndex >= rootChildCount / 2) { + for(i = 0; i < dummyIndex; i++) { + rootChildren[i].x = rootChildren[i + 1].x; + rootChildren[i].y = rootChildren[i + 1].y; + } + } + else { + for(i = rootChildCount - 1; i > dummyIndex; i--) { + rootChildren[i].x = rootChildren[i - 1].x; + rootChildren[i].y = rootChildren[i - 1].y; + } + } + } + + // Put all single vertex outputs in-line with the vertex node + // So that they are directly below the respective vertex in vertical layout + nodes.forEach(function (node) { + if(node.type === GraphDataProcessor.types.OUTPUT && + node.get('vertex.outputs.length') === 1 && + !node.get('vertex.outEdgeIds.length') && + node.get('treeParent.x') !== node.get('x') + ) { + node.x = node.get('vertex.x'); + } + }); +} + +function _getType(node) { + if(node.tagName === 'path') { + return 'path'; + } + return HREF_TYPE_HASH[Ember.$(node).attr('href')]; +} + +function _getEndName(fullName) { + return fullName.substr(fullName.lastIndexOf('.') + 1); +} + +/** + * Mouse over handler for all displayed SVG DOM elements. + * Later the implementation will be refactored and moved into the respective DataNode. + * d {DataNode} Contains data to be displayed + */ +function _onMouseOver(d) { + var event = d3.event, + node = event.target, + tooltipData = {}; // Will be populated with {title/text/kvList}. + + node = node.correspondingUseElement || node; + + switch(_getType(node)) { + case "vertex": + var list = {}, + vertex = d.get('data'); + + _component.get('vertexProperties').forEach(function (property) { + var value = {}; + + if(vertex && property.getCellContent) { + value = property.getCellContent(vertex); + if(value && value.text !== undefined) { + value = value.text; + } + } + else if(property.contentPath) { + value = d.get('data.' + property.contentPath); + } + + if(property.cellComponentName === "date-formatter") { + value = moment(value).format("DD MMM YYYY HH:mm:ss:SSS"); + } + + if(property.get("id") === "progress" && value) { + value = Math.round(value * 100) + "%"; + } + else if(property.get("id") === "duration" && value) { + value = value + " ms"; + } + + if(typeof value !== 'object') { + list[property.get('headerTitle')] = value; + } + }); + + tooltipData = { + title: d.get("vertexName"), + kvList: list + }; + break; + case "input": + list = { + "Class": _getEndName(d.get("class")), + "Initializer": _getEndName(d.get("initializer")), + "Configurations": d.get("configs.length") || 0 + }; + tooltipData = { + title: d.get("name"), + kvList: list + }; + break; + case "output": + list = { + "Class": _getEndName(d.get("class")), + "Configurations": d.get("configs.length") || 0 + }; + tooltipData = { + title: d.get("name"), + kvList: list + }; + break; + case "task": + var totalTasks = d.get('data.totalTasks') || 0; + tooltipData.title = totalTasks > 1 ? `${totalTasks} Tasks` : `${totalTasks} Task`; + + if(!isIE) { + node = d3.event.target; + } + break; + case "io": + var inputs = d.get('inputs.length'), + outputs = d.get('outputs.length'), + title = ""; + title += inputs > 1 ? `${inputs} Sources` : `${inputs} Source`; + title += " & "; + title += outputs > 1 ? `${outputs} Sinks` : `${outputs} Sink`; + tooltipData.title = title; + + if(!isIE) { + node = d3.event.target; + } + break; + case "group": + tooltipData = { + title: d.get("vertexGroup.groupName"), + text: d.get("vertexGroup.groupMembers").join(", ") + }; + break; + case "path": + let sourceName = d.get('source.name') || d.get('source.vertexName'), + targetName = d.get('target.name') || d.get('target.vertexName'); + + tooltipData = { + position: { + x: event.clientX, + y: event.clientY + }, + title: `${sourceName} - ${targetName}` + }; + if(d.get("edgeId")) { + tooltipData.kvList = { + "Edge Id": d.get("edgeId"), + "Data Movement Type": d.get("dataMovementType"), + "Data Source Type": d.get("dataSourceType"), + "Scheduling Type": d.get("schedulingType"), + "Edge Source Class": _getEndName(d.get("edgeSourceClass")), + "Edge Destination Class": _getEndName(d.get("edgeDestinationClass")) + }; + } + else { + tooltipData.text = d.get('source.type') === "input" ? "Source link" : "Sink link"; + } + break; + } + + if(tooltipData.kvList) { + let kvList = tooltipData.kvList, + newKVList = {}; + + Object.keys(kvList).forEach(function (key) { + if(kvList[key]) { + newKVList[key] = kvList[key]; + } + }); + + tooltipData.kvList = newKVList; + } + + _tip.show(node, tooltipData, event); +} + +/** + * onclick handler scheduled using setTimeout + * @params d {DataNode} data of the clicked element + * @param node {D3 element} Element that was clicked + */ +function _scheduledClick(d, node) { + node = node.correspondingUseElement || node; + + _component.sendAction('entityClicked', { + type: _getType(node), + d: d + }); + + _tip.hide(); + _scheduledClickId = 0; +} + +/** + * Schedules an onclick handler. If double click event is not triggered the handler + * will be called in 200ms. + * @param d {DataNode} Data of the clicked element + */ +function _onClick(d) { + if(!_scheduledClickId) { + _scheduledClickId = setTimeout(_scheduledClick.bind(this, d, d3.event.target), 200); + } +} + +/** + * Callback for mousedown & mousemove interactions. To disable click on drag + * @param d {DataNode} Data of the clicked element + */ +function _onMouse(/*d*/) { + d3.select(this).on('click', d3.event.type === 'mousedown' ? _onClick : null); +} + +/** + * Double click event handler. + * @param d {DataNode} Data of the clicked element + */ +function _onDblclick(d) { + var event = d3.event, + node = event.target; + + node = node.correspondingUseElement || node; + + if(_scheduledClickId) { + clearTimeout(_scheduledClickId); + _scheduledClickId = 0; + } + + switch(_getType(node)) { + case "io": + d.toggleAdditionalInclusion(); + _update(); + break; + } +} + +/** + * Creates a path data string for the given link. Google SVG path data to learn what it is. + * @param d {Object} Must contain source and target properties with the start and end positions. + * @return pathData {String} Path data string based on the current layout + */ +function _createPathData(d) { + var sX = d.source.y, + sY = d.source.x, + tX = d.target.y, + tY = d.target.x, + mX = (sX + tX)/2, + mY = (sY + tY)/2, + + sH = Math.abs(sX - tX) * 0.35, + sV = 0; // strength + + if(d.isBackwardLink) { + if(sY === tY) { + sV = 45; + mY -= 50; + if(sX === tX) { + sX += _layout.linkDelta; + tX -= _layout.linkDelta; + } + } + sH = Math.abs(sX - tX) * 1.1; + } + + return _layout.pathFormatter( + sX, sY, + + sX + sH, sY - sV, + mX, mY, + + tX - sH, tY - sV, + tX, tY + ); +} + +/** + * Get the node from/to which the node must transition on enter/exit + * @param d {DataNode} + * @param property {String} Property to be checked for + * @return vertex node + */ +function _getVertexNode(d, property) { + if(d.get('vertex.' + property)) { + return d.get('vertex'); + } +} +/** + * Update position of all nodes in the list and preform required transitions. + * @param nodes {Array} Nodes to be updated + * @param source {d3 element} Node that trigged the update, in first update source will be root. + */ +function _updateNodes(nodes, source) { + // Enter any new nodes at the parent's previous position. + nodes.enter().append('g') + .attr('transform', function(d) { + var node = _getVertexNode(d, "x0") || source; + node = _layout.projector(node.x0, node.y0); + return 'translate(' + node.x + ',' + node.y + ')'; + }) + .on({ + mouseover: _onMouseOver, + mouseout: _tip.hide, + mousedown: _onMouse, + mousemove: _onMouse, + dblclick: _onDblclick + }) + .style('opacity', 1e-6) + .each(_addContent); + + // Transition nodes to their new position. + nodes.transition() + .duration(DURATION) + .attr('transform', function(d) { + d = _layout.projector(d.x, d.y); + return 'translate(' + d.x + ',' + d.y + ')'; + }) + .style('opacity', 1); + + // Transition exiting nodes to the parent's new position. + nodes.exit().transition() + .duration(DURATION) + .attr('transform', function(d) { + var node = _getVertexNode(d, "x") || source; + node = _layout.projector(node.x, node.y); + return 'translate(' + node.x + ',' + node.y + ')'; + }) + .style('opacity', 1e-6) + .remove(); +} + +/** + * Get the node from/to which the link must transition on enter/exit + * @param d {DataNode} + * @param property {String} Property to be checked for + * @return node + */ +function _getTargetNode(d, property) { + if(d.get('target.type') === GraphDataProcessor.types.OUTPUT && d.get('source.' + property)) { + return d.source; + } + if(d.get('target.' + property)) { + return d.target; + } +} +/** + * Update position of all links in the list and preform required transitions. + * @param links {Array} Links to be updated + * @param source {d3 element} Node that trigged the update, in first update source will be root. + */ +function _updateLinks(links, source) { + // Enter any new links at the parent's previous position. + links.enter().insert('path', 'g') + .attr('class', function (d) { + var type = d.get('dataMovementType') || ""; + return 'link ' + type.toLowerCase(); + }) + /** + * IE11 rendering does not work for svg path element with marker set. + * See https://connect.microsoft.com/IE/feedback/details/801938 + * This can be removed once the bug is fixed in all supported IE versions + */ + .attr("style", isIE ? "" : Ember.String.htmlSafe("marker-mid: url(" + window.location.pathname + "#arrow-marker);")) + .attr('d', function(d) { + var node = _getTargetNode(d, "x0") || source; + var o = {x: node.x0, y: node.y0}; + return _createPathData({source: o, target: o}); + }) + .on({ + mouseover: _onMouseOver, + mouseout: _tip.hide + }); + + // Transition links to their new position. + links.transition() + .duration(DURATION) + .attr('d', _createPathData); + + // Transition exiting nodes to the parent's new position. + links.exit().transition() + .duration(DURATION) + .attr('d', function(d) { + var node = _getTargetNode(d, "x") || source; + var o = {x: node.x, y: node.y}; + return _createPathData({source: o, target: o}); + }) + .remove(); +} + +function _getNodeId(d) { + return d.id || (d.id = ++_idCounter); +} +function _getLinkId(d) { + return d.source.id.toString() + d.target.id; +} +function _stashOldPositions(d) { + d.x0 = d.x; + d.y0 = d.y; +} + +/** + * Updates position of nodes and links based on changes in _treeData. + */ +function _update() { + var nodesData = _treeLayout.nodes(_treeData), + linksData = _getLinks(nodesData); + + _normalize(nodesData); + + var nodes = _g.selectAll('g.node') + .data(nodesData, _getNodeId); + _updateNodes(nodes, _treeData); + + var links = _g.selectAll('path.link') + .data(linksData, _getLinkId); + _updateLinks(links, _treeData); + + nodesData.forEach(_stashOldPositions); +} + +/** + * Attach pan and zoom events on to the container. + * @param container {DOM element} Element onto which events are attached. + * @param g {d3 DOM element} SVG(d3) element that will be moved or scaled + */ +function _attachPanZoom(container, g, element) { + var SCALE_TUNER = 1 / 700, + MIN_SCALE = 0.5, + MAX_SCALE = 2; + + var prevX = 0, + prevY = 0, + + panX = PADDING, + panY = PADDING, + scale = 1, + + scheduleId = 0; + + /** + * Transform g to current panX, panY and scale. + * @param animate {Boolean} Animate the transformation in DURATION time. + */ + function transform(animate) { + var base = animate ? g.transition().duration(DURATION) : g; + base.attr('transform', `translate(${panX}, ${panY}) scale(${scale})`); + } + + /** + * Check if the item have moved out of the visible area, and reset if required + */ + function visibilityCheck() { + var graphBound = g.node().getBoundingClientRect(), + containerBound = container[0].getBoundingClientRect(); + + if(graphBound.right < containerBound.left || + graphBound.bottom < containerBound.top || + graphBound.left > containerBound.right || + graphBound.top > containerBound.bottom) { + panX = PADDING; + panY = PADDING; + scale = 1; + transform(true); + } + } + + /** + * Schedule a visibility check and reset if required + */ + function scheduleVisibilityCheck() { + if(scheduleId) { + clearTimeout(scheduleId); + scheduleId = 0; + } + scheduleId = setTimeout(visibilityCheck, 100); + } + + /** + * Set pan values + */ + function onMouseMove(event) { + panX += event.pageX - prevX; + panY += event.pageY - prevY; + + transform(); + + prevX = event.pageX; + prevY = event.pageY; + } + /** + * Set zoom values, pan also would change as we are zooming with mouse position as pivote. + */ + function onWheel(event) { + var prevScale = scale, + + offset = container.offset(), + mouseX = event.pageX - offset.left, + mouseY = event.pageY - offset.top, + factor = 0; + + scale += event.deltaY * SCALE_TUNER; + if(scale < MIN_SCALE) { + scale = MIN_SCALE; + } + else if(scale > MAX_SCALE) { + scale = MAX_SCALE; + } + + factor = 1 - scale / prevScale; + panX += (mouseX - panX) * factor; + panY += (mouseY - panY) * factor; + + transform(); + scheduleVisibilityCheck(); + + _tip.reposition(); + event.preventDefault(); + } + + Ember.$(element).on('mousewheel', onWheel); + + container + .mousedown(function (event){ + prevX = event.pageX; + prevY = event.pageY; + + container.on('mousemove', onMouseMove); + container.parent().addClass('panning'); + }) + .mouseup(function (){ + container.off('mousemove', onMouseMove); + container.parent().removeClass('panning'); + + scheduleVisibilityCheck(); + }); + + /** + * A closure to reset/modify panZoom based on an external event + * @param newPanX {Number} + * @param newPanY {Number} + * @param newScale {Number} + */ + return function(newPanX, newPanY, newScale) { + var values = { + panX: panX, + panY: panY, + scale: scale + }; + + panX = newPanX === undefined ? panX : newPanX; + panY = newPanY === undefined ? panY : newPanY; + scale = newScale === undefined ? scale : newScale; + + transform(true); + + return values; + }; +} + +/** + * Sets the layout and update the display. + * @param layout {Object} One of the values defined in LAYOUTS object + */ +function _setLayout(layout) { + var leafCount = _data.leafCount, + dimention; + + // If count is even dummy will be replaced by output, so output would no more be leaf + if(_data.tree.get('children.length') % 2 === 0) { + leafCount--; + } + dimention = layout.projector(leafCount, _data.maxDepth - 1); + + _layout = layout; + + _width = dimention.x *= _layout.hSpacing; + _height = dimention.y *= _layout.vSpacing; + + dimention = _layout.projector(dimention.x, dimention.y); // Because tree is always top to bottom + _treeLayout = d3.layout.tree().size([dimention.x, dimention.y]); + + _update(); +} + +var GraphView = { + /** + * Creates a DAG view in the given element based on the data + * @param component {DagViewComponent} Parent ember component, to sendAction + * @param element {HTML DOM Element} HTML element in which the view will be created + * @param data {Object} Created by data processor + */ + create: function (component, element, data) { + var svg = d3.select(element).select('svg'); + + _component = component; + _data = data; + _g = svg.append('g').attr('transform', `translate(${PADDING},${PADDING})`); + _svg = Ember.$(svg.node()); + _tip = Tip; + + _tip.init(Ember.$(element).find('.tool-tip'), _svg); + + _treeData = data.tree; + _treeData.x0 = 0; + _treeData.y0 = 0; + + _panZoom = _attachPanZoom(_svg, _g, element); + + _setLayout(LAYOUTS.topToBottom); + }, + + /** + * Calling this function would fit the graph to the available space. + */ + fitGraph: function (){ + var scale = Math.min( + (_svg.width() - PADDING * 2) / _width, + (_svg.height() - PADDING * 2) / _height + ), + panZoomValues = _panZoom(); + + if( + panZoomValues.panX !== PADDING || + panZoomValues.panY !== PADDING || + panZoomValues.scale !== scale + ) { + _panZoomValues = _panZoom(PADDING, PADDING, scale); + } + else { + _panZoomValues = _panZoom( + _panZoomValues.panX, + _panZoomValues.panY, + _panZoomValues.scale); + } + }, + + /** + * Control display of additionals or sources and sinks. + * @param hide {Boolean} If true the additionals will be excluded, else included in the display + */ + additionalDisplay: function (hide) { + if(hide) { + _g.attr('class', 'hide-io'); + _treeData.recursivelyCall('excludeAdditionals'); + } + else { + _treeData.recursivelyCall('includeAdditionals'); + _g.attr('class', null); + } + _update(); + }, + + /** + * Toggle graph layouts between the available options + */ + toggleLayouts: function () { + _setLayout(_layout === LAYOUTS.topToBottom ? + LAYOUTS.leftToRight : + LAYOUTS.topToBottom); + return _layout === LAYOUTS.topToBottom; + } +}; + +return GraphView; +} + +// TODO - Move to a better class based implementation +var GraphView = createNewGraphView(); +GraphView.createNewGraphView = createNewGraphView; + +// TODO - Convert to pure ES6 style export without using an object +export default GraphView; diff --git a/tez-ui/src/main/webapp/app/utils/tip.js b/tez-ui/src/main/webapp/app/utils/tip.js new file mode 100644 index 0000000000..9b0dbb2c34 --- /dev/null +++ b/tez-ui/src/main/webapp/app/utils/tip.js @@ -0,0 +1,188 @@ +/** + * 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 Ember from 'ember'; + +/** + * Displays a tooltip over an svg element. + */ +var _element = null, // jQuery tooltip DOM element + _bubble = null, // Tooltip bubble in _element + _svg = null, // HTML svg tag that contains the element + _svgPoint = null, // A SVGPoint object + _window = Ember.$(window), + + _data = null, // Last displayed data, for re-render + _node = null; // Last node over which tooltip was displayed + +/** + * Converts the provided list object into a tabular form. + * @param list {Object} : An object with properties to be displayed as key value pairs + * { + * propertyName1: "property value 1", + * .. + * propertyNameN: "property value N", + * } + */ +function _createList(list) { + var listContent = []; + + if(list) { + listContent.push(""); + + Ember.$.each(list, function (property, value) { + listContent.push( + "" + ); + }); + listContent.push("
", + property, + "", + value, + "
"); + + return listContent.join(""); + } +} + +/** + * Tip supports 3 visual entities in the tooltip. Title, description text and a list. + * _setData sets all these based on the passed data object + * @param data {Object} An object of the format + * { + * title: "tip title", + * text: "tip description text", + * kvList: { + * propertyName1: "property value 1", + * .. + * propertyNameN: "property value N", + * } + * } + */ +function _setData(data) { + _element.find('.tip-title').html(data.title || ""); + _element.find('.tip-text').html(data.text || ""); + _element.find('.tip-text')[data.text ? 'show' : 'hide'](); + _element.find('.tip-list').html(_createList(data.kvList) || ""); +} + +var Tip = { + /** + * Set the tip defaults + * @param tipElement {$} jQuery reference to the tooltip DOM element. + * The element must contain 3 children with class tip-title, tip-text & tip-list. + * @param svg {$} jQuery reference to svg html element + */ + init: function (tipElement, svg) { + _element = tipElement; + _bubble = _element.find('.bubble'); + _svg = svg; + _svgPoint = svg[0].createSVGPoint(); + }, + showTip: function () { + if(_data) { + _element.addClass('show'); + } + }, + /** + * Display a tooltip over an svg element. + * @param node {SVG Element} Svg element over which tooltip must be displayed. + * @param data {Object} An object of the format + * { + * title: "tip title", + * text: "tip description text", + * kvList: { + * propertyName1: "property value 1", + * .. + * propertyNameN: "property value N", + * } + * } + * @param event {MouseEvent} Event that triggered the tooltip. + */ + show: function (node, data, event) { + var point = data.position || (node.getScreenCTM ? _svgPoint.matrixTransform( + node.getScreenCTM() + ) : { + x: event.x, + y: event.y + }), + + windMid = _window.height() >> 1, + winWidth = _window.width(), + + showAbove = point.y < windMid, + offsetX = 0, + width = 0; + + if(_data !== data) { + _data = data; + _node = node; + + _setData(data); + } + + if(showAbove) { + _element.removeClass('below'); + _element.addClass('above'); + } + else { + _element.removeClass('above'); + _element.addClass('below'); + + point.y -= _element.height(); + } + + width = _element.width(); + offsetX = (width - 11) >> 1; + + if(point.x - offsetX < 0) { + offsetX = point.x - 20; + } + else if(point.x + offsetX > winWidth) { + offsetX = point.x - (winWidth - 10 - width); + } + + _bubble.css({ + left: -offsetX + }); + + Ember.run.debounce(Tip, "showTip", 500); + + _element.css({ + left: point.x, + top: point.y + }); + }, + /** + * Reposition the tooltip based on last passed data & node. + */ + reposition: function () { + if(_data) { + this.show(_node, _data); + } + }, + /** + * Hide the tooltip. + */ + hide: function () { + _data = _node = null; + _element.removeClass('show'); + } +}; + +export default Tip; diff --git a/tez-ui/src/main/webapp/package.json b/tez-ui/src/main/webapp/package.json index ad3aa74c5d..0e7511964b 100644 --- a/tez-ui/src/main/webapp/package.json +++ b/tez-ui/src/main/webapp/package.json @@ -61,6 +61,5 @@ "phantomjs-prebuilt": "2.1.13" }, "dependencies": { - "em-tgraph": "0.0.14" } } diff --git a/tez-ui/src/main/webapp/tests/integration/components/em-tgraph-test.js b/tez-ui/src/main/webapp/tests/integration/components/em-tgraph-test.js new file mode 100644 index 0000000000..b36f9d2c85 --- /dev/null +++ b/tez-ui/src/main/webapp/tests/integration/components/em-tgraph-test.js @@ -0,0 +1,41 @@ +/** + * 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 { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('em-tgraph', 'Integration | Component | em tgraph', { + integration: true +}); + +test('Basic failure test', function(assert) { + + this.set("data", []); + this.render(hbs`{{em-tgraph data=data}}`); + + assert.equal(this.$().text().trim(), 'Rendering failed! Vertices not found!'); + + // Template block usage:" + EOL + + this.render(hbs` + {{#em-tgraph data=data}} + template block text + {{/em-tgraph}} + `); + + assert.equal(this.$().text().trim(), 'Rendering failed! Vertices not found!'); +}); diff --git a/tez-ui/src/main/webapp/tests/unit/utils/fullscreen-test.js b/tez-ui/src/main/webapp/tests/unit/utils/fullscreen-test.js new file mode 100644 index 0000000000..1388238dc3 --- /dev/null +++ b/tez-ui/src/main/webapp/tests/unit/utils/fullscreen-test.js @@ -0,0 +1,30 @@ +/** + * 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 fullscreen from '../../../utils/fullscreen'; +import { module, test } from 'qunit'; + +module('Unit | Utility | fullscreen'); + +test('Basic creation test', function(assert) { + + assert.ok(fullscreen); + assert.ok(fullscreen.inFullscreenMode); + assert.ok(fullscreen.toggle); + +}); diff --git a/tez-ui/src/main/webapp/tests/unit/utils/graph-data-processor-test.js b/tez-ui/src/main/webapp/tests/unit/utils/graph-data-processor-test.js new file mode 100644 index 0000000000..ca1a96e45c --- /dev/null +++ b/tez-ui/src/main/webapp/tests/unit/utils/graph-data-processor-test.js @@ -0,0 +1,30 @@ +/** + * 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 GraphDataProcessor from '../../../utils/graph-data-processor'; +import { module, test } from 'qunit'; + +module('Unit | Utility | graph data processor'); + +test('Basic creation test', function(assert) { + + assert.ok(GraphDataProcessor); + assert.ok(GraphDataProcessor.types); + assert.ok(GraphDataProcessor.graphifyData); + +}); diff --git a/tez-ui/src/main/webapp/tests/unit/utils/graph-view-test.js b/tez-ui/src/main/webapp/tests/unit/utils/graph-view-test.js new file mode 100644 index 0000000000..5b2bf9edf3 --- /dev/null +++ b/tez-ui/src/main/webapp/tests/unit/utils/graph-view-test.js @@ -0,0 +1,32 @@ +/** + * 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 graphView from '../../../utils/graph-view'; +import { module, test } from 'qunit'; + +module('Unit | Utility | graph view'); + +test('Basic creation test', function(assert) { + + assert.ok(graphView); + assert.ok(graphView.create); + assert.ok(graphView.fitGraph); + assert.ok(graphView.additionalDisplay); + assert.ok(graphView.toggleLayouts); + +}); diff --git a/tez-ui/src/main/webapp/tests/unit/utils/tip-test.js b/tez-ui/src/main/webapp/tests/unit/utils/tip-test.js new file mode 100644 index 0000000000..8a02399e0b --- /dev/null +++ b/tez-ui/src/main/webapp/tests/unit/utils/tip-test.js @@ -0,0 +1,32 @@ +/** + * 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 tip from '../../../utils/tip'; +import { module, test } from 'qunit'; + +module('Unit | Utility | tip'); + +test('Basic creation test', function(assert) { + + assert.ok(tip); + assert.ok(tip.init); + assert.ok(tip.show); + assert.ok(tip.reposition); + assert.ok(tip.hide); + +}); diff --git a/tez-ui/src/main/webapp/yarn.lock b/tez-ui/src/main/webapp/yarn.lock index 660ac80d87..2c3eed4b0b 100644 --- a/tez-ui/src/main/webapp/yarn.lock +++ b/tez-ui/src/main/webapp/yarn.lock @@ -1391,16 +1391,6 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -em-tgraph@0.0.14: - version "0.0.14" - resolved "https://registry.yarnpkg.com/em-tgraph/-/em-tgraph-0.0.14.tgz#4d48b911760f85dec41904e4056ec52542391cc1" - dependencies: - ember-cli-htmlbars "^1.0.1" - ember-cli-less "^1.4.0" - source-map "^0.5.6" - optionalDependencies: - phantomjs-prebuilt "2.1.13" - ember-bootstrap@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/ember-bootstrap/-/ember-bootstrap-0.5.1.tgz#bbad60b2818c47b3fb31562967ae02ee7e92d38c"