| /* |
| * Licensed 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 moment from 'moment'; |
| |
| import DatasetFactory from '../../../tabledata/datasetfactory'; |
| import TableVisualization from '../../../visualization/builtins/visualization-table'; |
| import BarchartVisualization from '../../../visualization/builtins/visualization-barchart'; |
| import PiechartVisualization from '../../../visualization/builtins/visualization-piechart'; |
| import AreachartVisualization from '../../../visualization/builtins/visualization-areachart'; |
| import LinechartVisualization from '../../../visualization/builtins/visualization-linechart'; |
| import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart'; |
| import NetworkVisualization from '../../../visualization/builtins/visualization-d3network'; |
| import {DefaultDisplayType, SpellResult} from '../../../spell'; |
| import {ParagraphStatus} from '../paragraph.status'; |
| import Result from './result'; |
| |
| const AnsiUp = require('ansi_up'); |
| const AnsiUpConverter = new AnsiUp.default; // eslint-disable-line new-parens,new-cap |
| const TableGridFilterTemplate = require('../../../visualization/builtins/visualization-table-grid-filter.html'); |
| |
| angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl); |
| |
| function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location, |
| $timeout, $compile, $http, $q, $templateCache, $templateRequest, $sce, websocketMsgSrv, |
| baseUrlSrv, ngToast, saveAsService, noteVarShareService, heliumService, |
| uiGridConstants) { |
| 'ngInject'; |
| |
| /** |
| * Built-in visualizations |
| */ |
| $scope.builtInTableDataVisualizationList = [ |
| { |
| id: 'table', // paragraph.config.graph.mode |
| name: 'Table', // human readable name. tooltip |
| icon: '<i class="fa fa-table"></i>', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'multiBarChart', |
| name: 'Bar Chart', |
| icon: '<i class="fa fa-bar-chart"></i>', |
| transformation: 'pivot', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'pieChart', |
| name: 'Pie Chart', |
| icon: '<i class="fa fa-pie-chart"></i>', |
| transformation: 'pivot', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'stackedAreaChart', |
| name: 'Area Chart', |
| icon: '<i class="fa fa-area-chart"></i>', |
| transformation: 'pivot', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'lineChart', |
| name: 'Line Chart', |
| icon: '<i class="fa fa-line-chart"></i>', |
| transformation: 'pivot', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'scatterChart', |
| name: 'Scatter Chart', |
| icon: '<i class="cf cf-scatter-chart"></i>', |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }, |
| { |
| id: 'network', |
| name: 'Network', |
| icon: '<i class="fa fa-share-alt"></i>', |
| supports: [DefaultDisplayType.NETWORK], |
| }, |
| ]; |
| |
| /** |
| * Holds class and actual runtime instance and related infos of built-in visualizations |
| */ |
| let builtInVisualizations = { |
| 'table': { |
| class: TableVisualization, |
| instance: undefined, // created from setGraphMode() |
| }, |
| 'multiBarChart': { |
| class: BarchartVisualization, |
| instance: undefined, |
| }, |
| 'pieChart': { |
| class: PiechartVisualization, |
| instance: undefined, |
| }, |
| 'stackedAreaChart': { |
| class: AreachartVisualization, |
| instance: undefined, |
| }, |
| 'lineChart': { |
| class: LinechartVisualization, |
| instance: undefined, |
| }, |
| 'scatterChart': { |
| class: ScatterchartVisualization, |
| instance: undefined, |
| }, |
| 'network': { |
| class: NetworkVisualization, |
| instance: undefined, |
| }, |
| }; |
| |
| // type |
| $scope.type = null; |
| |
| // Data of the result |
| let data; |
| |
| // config |
| $scope.config = null; |
| |
| // resultId = paragraph.id + index |
| $scope.id = null; |
| |
| // referece to paragraph |
| let paragraph; |
| |
| // index of the result |
| let resultIndex; |
| |
| // TableData instance |
| let tableData; |
| |
| // available columns in tabledata |
| $scope.tableDataColumns = []; |
| |
| // enable helium |
| let enableHelium = false; |
| |
| // graphMode |
| $scope.graphMode = null; |
| |
| // image data |
| $scope.imageData = null; |
| |
| // queue for append output |
| const textResultQueueForAppend = []; |
| |
| // prevent body area scrollbar from blocking due to scroll in paragraph results |
| $scope.mouseOver = false; |
| $scope.onMouseOver = function() { |
| $scope.mouseOver = true; |
| }; |
| $scope.onMouseOut = function() { |
| $scope.mouseOver = false; |
| }; |
| $scope.getPointerEvent = function() { |
| return ($scope.mouseOver) ? {'pointer-events': 'auto'} |
| : {'pointer-events': 'none'}; |
| }; |
| |
| $scope.init = function(result, config, paragraph, index) { |
| // register helium plugin vis packages |
| let visPackages = heliumService.getVisualizationCachedPackages(); |
| const visPackageOrder = heliumService.getVisualizationCachedPackageOrder(); |
| |
| // push the helium vis packages following the order |
| visPackageOrder.map((visName) => { |
| visPackages.map((vis) => { |
| if (vis.name !== visName) { |
| return; |
| } |
| $scope.builtInTableDataVisualizationList.push({ |
| id: vis.id, |
| name: vis.name, |
| icon: $sce.trustAsHtml(vis.icon), |
| supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK], |
| }); |
| builtInVisualizations[vis.id] = { |
| class: vis.class, |
| }; |
| }); |
| }); |
| |
| updateData(result, config, paragraph, index); |
| renderResult($scope.type); |
| }; |
| |
| function isDOMLoaded(targetElemId) { |
| const elem = angular.element(`#${targetElemId}`); |
| return elem.length; |
| } |
| |
| function retryUntilElemIsLoaded(targetElemId, callback) { |
| function retry() { |
| if (!isDOMLoaded(targetElemId)) { |
| $timeout(retry, 10); |
| return; |
| } |
| |
| const elem = angular.element(`#${targetElemId}`); |
| callback(elem); |
| } |
| |
| $timeout(retry); |
| } |
| |
| $scope.$on('updateResult', function(event, result, newConfig, paragraphRef, index) { |
| if (paragraph.id !== paragraphRef.id || index !== resultIndex) { |
| return; |
| } |
| |
| let refresh = !angular.equals(newConfig, $scope.config) || |
| !angular.equals(result.type, $scope.type) || |
| !angular.equals(result.data, data); |
| |
| updateData(result, newConfig, paragraph, resultIndex); |
| renderResult($scope.type, refresh); |
| }); |
| |
| $scope.$on('appendParagraphOutput', function(event, data) { |
| /* It has been observed that append events |
| * can be errorneously called even if paragraph |
| * execution has ended, and in that case, no append |
| * should be made. Also, it was observed that between PENDING |
| * and RUNNING states, append-events can be called and we can't |
| * miss those, else during the length of paragraph run, few |
| * initial output line/s will be missing. |
| */ |
| if (paragraph.id === data.paragraphId && |
| resultIndex === data.index && |
| (paragraph.status === ParagraphStatus.PENDING || paragraph.status === ParagraphStatus.RUNNING)) { |
| // Check if result type is eiter TEXT or TABLE, if not then treat it like TEXT |
| if ([DefaultDisplayType.TEXT, DefaultDisplayType.TABLE].indexOf($scope.type) < 0) { |
| $scope.type = DefaultDisplayType.TEXT; |
| } |
| if ($scope.type === DefaultDisplayType.TEXT) { |
| appendTextOutput(data.data); |
| } else if ($scope.type === DefaultDisplayType.TABLE) { |
| appendTableOutput(data); |
| } |
| } |
| if (paragraph.id === data.paragraphId && |
| resultIndex === data.index && |
| paragraph.status === ParagraphStatus.FINISHED) { |
| if ($scope.type === DefaultDisplayType.TABLE) { |
| appendTableOutput(data); |
| } |
| } |
| }); |
| |
| const updateData = function(result, config, paragraphRef, index) { |
| data = result.data; |
| paragraph = paragraphRef; |
| resultIndex = parseInt(index); |
| |
| $scope.id = paragraph.id + '_' + index; |
| $scope.type = result.type; |
| config = config ? config : {}; |
| |
| // initialize default config values |
| if (!config.graph) { |
| config.graph = {}; |
| } |
| |
| if (!config.graph.mode) { |
| config.graph.mode = 'table'; |
| } |
| |
| if (!config.graph.height) { |
| config.graph.height = 300; |
| } |
| |
| if (!config.graph.optionOpen) { |
| config.graph.optionOpen = false; |
| } |
| |
| $scope.graphMode = config.graph.mode; |
| $scope.config = angular.copy(config); |
| |
| // enable only when it is last result |
| enableHelium = (index === paragraphRef.results.msg.length - 1); |
| |
| if ($scope.type === 'TABLE' || $scope.type === 'NETWORK') { |
| tableData = new DatasetFactory().createDataset($scope.type); |
| tableData.loadParagraphResult({type: $scope.type, msg: data}); |
| $scope.tableDataColumns = tableData.columns; |
| $scope.tableDataComment = tableData.comment; |
| if ($scope.type === 'NETWORK') { |
| $scope.networkNodes = tableData.networkNodes; |
| $scope.networkRelationships = tableData.networkRelationships; |
| $scope.networkProperties = tableData.networkProperties; |
| } |
| } else if ($scope.type === 'IMG') { |
| $scope.imageData = data; |
| } |
| }; |
| |
| $scope.createDisplayDOMId = function(baseDOMId, type) { |
| if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { |
| return `${baseDOMId}_graph`; |
| } else if (type === DefaultDisplayType.HTML) { |
| return `${baseDOMId}_html`; |
| } else if (type === DefaultDisplayType.ANGULAR) { |
| return `${baseDOMId}_angular`; |
| } else if (type === DefaultDisplayType.TEXT) { |
| return `${baseDOMId}_text`; |
| } else if (type === DefaultDisplayType.ELEMENT) { |
| return `${baseDOMId}_elem`; |
| } else { |
| console.error(`Cannot create display DOM Id due to unknown display type: ${type}`); |
| } |
| }; |
| |
| $scope.renderDefaultDisplay = function(targetElemId, type, data, refresh) { |
| const afterLoaded = () => { |
| if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) { |
| renderGraph(targetElemId, $scope.graphMode, refresh); |
| } else if (type === DefaultDisplayType.HTML) { |
| renderHtml(targetElemId, data); |
| } else if (type === DefaultDisplayType.ANGULAR) { |
| renderAngular(targetElemId, data); |
| } else if (type === DefaultDisplayType.TEXT) { |
| renderText(targetElemId, data, refresh); |
| } else if (type === DefaultDisplayType.ELEMENT) { |
| renderElem(targetElemId, data); |
| } else { |
| console.error(`Unknown Display Type: ${type}`); |
| } |
| }; |
| |
| retryUntilElemIsLoaded(targetElemId, afterLoaded); |
| |
| // send message to parent that this result is rendered |
| const paragraphId = $scope.$parent.paragraph.id; |
| $scope.$emit('resultRendered', paragraphId); |
| }; |
| |
| const renderResult = function(type, refresh) { |
| let activeApp; |
| if (enableHelium) { |
| getSuggestions(); |
| getApplicationStates(); |
| activeApp = _.get($scope.config, 'helium.activeApp'); |
| } |
| |
| if (activeApp) { |
| const appState = _.find($scope.apps, {id: activeApp}); |
| renderApp(`p${appState.id}`, appState); |
| } else { |
| if (!DefaultDisplayType[type]) { |
| $scope.renderCustomDisplay(type, data); |
| } else { |
| const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type); |
| $scope.renderDefaultDisplay(targetElemId, type, data, refresh); |
| } |
| } |
| }; |
| |
| $scope.isDefaultDisplay = function() { |
| return DefaultDisplayType[$scope.type]; |
| }; |
| |
| /** |
| * Render multiple sub results for custom display |
| */ |
| $scope.renderCustomDisplay = function(type, data) { |
| // get result from intp |
| if (!heliumService.getSpellByMagic(type)) { |
| console.error(`Can't execute spell due to unknown display type: ${type}`); |
| return; |
| } |
| |
| // custom display result can include multiple subset results |
| heliumService.executeSpellAsDisplaySystem(type, data) |
| .then((dataWithTypes) => { |
| const containerDOMId = `p${$scope.id}_custom`; |
| const afterLoaded = () => { |
| const containerDOM = angular.element(`#${containerDOMId}`); |
| // Spell.interpret() can create multiple outputs |
| for (let i = 0; i < dataWithTypes.length; i++) { |
| const dt = dataWithTypes[i]; |
| const data = dt.data; |
| const type = dt.type; |
| |
| // prepare each DOM to be filled |
| const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom_${i}`, type); |
| const subResultDOM = document.createElement('div'); |
| containerDOM.append(subResultDOM); |
| subResultDOM.setAttribute('id', subResultDOMId); |
| |
| $scope.renderDefaultDisplay(subResultDOMId, type, data, true); |
| } |
| }; |
| |
| retryUntilElemIsLoaded(containerDOMId, afterLoaded); |
| }) |
| .catch((error) => { |
| console.error(`Failed to render custom display: ${$scope.type}\n` + error); |
| }); |
| }; |
| |
| /** |
| * generates actually object which will be consumed from `data` property |
| * feed it to the success callback. |
| * if error occurs, the error is passed to the failure callback |
| * |
| * @param data {Object or Function} |
| * @param type {string} Display Type |
| * @param successCallback |
| * @param failureCallback |
| */ |
| const handleData = function(data, type, successCallback, failureCallback) { |
| if (SpellResult.isFunction(data)) { |
| try { |
| successCallback(data()); |
| } catch (error) { |
| failureCallback(error); |
| console.error(`Failed to handle ${type} type, function data\n`, error); |
| } |
| } else if (SpellResult.isObject(data)) { |
| try { |
| successCallback(data); |
| } catch (error) { |
| console.error(`Failed to handle ${type} type, object data\n`, error); |
| } |
| } |
| }; |
| |
| const renderElem = function(targetElemId, data) { |
| const elem = angular.element(`#${targetElemId}`); |
| handleData(() => { |
| data(targetElemId); |
| }, DefaultDisplayType.ELEMENT, |
| () => {}, /** HTML element will be filled with data. thus pass empty success callback */ |
| (error) => { |
| elem.html(`${error.stack}`); |
| } |
| ); |
| }; |
| |
| const renderHtml = function(targetElemId, data) { |
| const elem = angular.element(`#${targetElemId}`); |
| handleData(data, DefaultDisplayType.HTML, |
| (generated) => { |
| elem.html(generated); |
| elem.find('pre code').each(function(i, e) { |
| hljs.highlightBlock(e); |
| }); |
| /* eslint new-cap: [2, {"capIsNewExceptions": ["MathJax.Hub.Queue"]}] */ |
| MathJax.Hub.Queue(['Typeset', MathJax.Hub, elem[0]]); |
| }, |
| (error) => { |
| elem.html(`${error.stack}`); |
| } |
| ); |
| }; |
| |
| const renderAngular = function(targetElemId, data) { |
| const elem = angular.element(`#${targetElemId}`); |
| const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`); |
| handleData(data, DefaultDisplayType.ANGULAR, |
| (generated) => { |
| elem.html(generated); |
| $compile(elem.contents())(paragraphScope); |
| }, |
| (error) => { |
| elem.html(`${error.stack}`); |
| } |
| ); |
| }; |
| |
| const getTextResultElemId = function(resultId) { |
| return `p${resultId}_text`; |
| }; |
| |
| const checkAndReplaceCarriageReturn = function(str) { |
| return new Result(str).checkAndReplaceCarriageReturn(); |
| }; |
| |
| const renderText = function(targetElemId, data, refresh) { |
| const elem = angular.element(`#${targetElemId}`); |
| handleData(data, DefaultDisplayType.TEXT, |
| (generated) => { |
| // clear all lines before render |
| removeChildrenDOM(targetElemId); |
| |
| if (generated) { |
| generated = checkAndReplaceCarriageReturn(generated); |
| const escaped = AnsiUpConverter.ansi_to_html(generated); |
| const divDOM = angular.element('<div></div>').innerHTML = escaped; |
| if (refresh) { |
| elem.html(divDOM); |
| } else { |
| elem.append(divDOM); |
| } |
| } else if (refresh) { |
| elem.html(''); |
| } |
| |
| elem.bind('mousewheel', (e) => { |
| $scope.keepScrollDown = false; |
| }); |
| }, |
| (error) => { |
| elem.html(`${error.stack}`); |
| } |
| ); |
| }; |
| |
| const removeChildrenDOM = function(targetElemId) { |
| const elem = angular.element(`#${targetElemId}`); |
| if (elem.length) { |
| elem.children().remove(); |
| } |
| }; |
| |
| function appendTableOutput(data) { |
| if (ParagraphStatus.FINISHED !== paragraph.status) { |
| if (!$scope.$parent.result.data) { |
| $scope.$parent.result.data = []; |
| tableData = undefined; |
| } |
| if (!$scope.$parent.result.data[data.index]) { |
| $scope.$parent.result.data[data.index] = ''; |
| } |
| if (tableData) { |
| let textRows = data.data.split('\n'); |
| for (let i = 0; i < textRows.length; i++) { |
| if (textRows[i] !== '') { |
| let row = textRows[i].split('\t'); |
| tableData.rows.push(row); |
| let builtInViz = builtInVisualizations['table']; |
| if (builtInViz.instance !== undefined) { |
| builtInViz.instance.append([row], tableData.columns); |
| } |
| } |
| } |
| } |
| if (!tableData |
| || !builtInVisualizations[$scope.graphMode].instance.append) { |
| $scope.$parent.result.data[data.index] = $scope.$parent.result.data[data.index].concat( |
| data.data); |
| $rootScope.$broadcast( |
| 'updateResult', |
| {'data': $scope.$parent.result.data[data.index], 'type': 'TABLE'}, |
| $scope.config, |
| paragraph, |
| data.index); |
| let elemId = `p${$scope.id}_` + $scope.graphMode; |
| renderGraph(elemId, $scope.graphMode, true); |
| } |
| } |
| } |
| |
| function appendTextOutput(data) { |
| const elemId = getTextResultElemId($scope.id); |
| textResultQueueForAppend.push(data); |
| |
| // if DOM is not loaded, just push data and return |
| if (!isDOMLoaded(elemId)) { |
| return; |
| } |
| |
| const elem = angular.element(`#${elemId}`); |
| |
| // pop all stacked data and append to the DOM |
| while (textResultQueueForAppend.length > 0) { |
| const line = elem.html() + AnsiUpConverter.ansi_to_html(textResultQueueForAppend.pop()); |
| elem.html(checkAndReplaceCarriageReturn(line)); |
| if ($scope.keepScrollDown) { |
| const doc = angular.element(`#${elemId}`); |
| doc[0].scrollTop = doc[0].scrollHeight; |
| } |
| } |
| } |
| |
| const getTrSettingElem = function(scopeId, graphMode) { |
| return angular.element('#trsetting' + scopeId + '_' + graphMode); |
| }; |
| |
| const getVizSettingElem = function(scopeId, graphMode) { |
| return angular.element('#vizsetting' + scopeId + '_' + graphMode); |
| }; |
| |
| const renderGraph = function(graphElemId, graphMode, refresh) { |
| // set graph height |
| const height = $scope.config.graph.height; |
| const graphElem = angular.element(`#${graphElemId}`); |
| graphElem.height(height); |
| |
| if (!graphMode) { |
| graphMode = 'table'; |
| } |
| |
| let builtInViz = builtInVisualizations[graphMode]; |
| if (!builtInViz) { |
| /** helium package is not available, fallback to table vis */ |
| graphMode = 'table'; |
| $scope.graphMode = graphMode; /** html depends on this scope value */ |
| builtInViz = builtInVisualizations[graphMode]; |
| } |
| |
| // deactive previsouly active visualization |
| for (let t in builtInVisualizations) { |
| if (builtInVisualizations.hasOwnProperty(t)) { |
| const v = builtInVisualizations[t].instance; |
| |
| if (t !== graphMode && v && v.isActive()) { |
| v.deactivate(); |
| break; |
| } |
| } |
| } |
| |
| let afterLoaded = function() { /** will be overwritten */ }; |
| |
| if (!builtInViz.instance) { // not instantiated yet |
| // render when targetEl is available |
| afterLoaded = function(loadedElem) { |
| try { |
| const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode); |
| const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode); |
| // set height |
| loadedElem.height(height); |
| |
| // instantiate visualization |
| const config = getVizConfig(graphMode); |
| const Visualization = builtInViz.class; |
| builtInViz.instance = new Visualization(loadedElem, config); |
| |
| // inject emitter, $templateRequest |
| const emitter = function(graphSetting) { |
| commitVizConfigChange(graphSetting, graphMode); |
| }; |
| builtInViz.instance._emitter = emitter; |
| builtInViz.instance._compile = $compile; |
| |
| // ui-grid related |
| $templateCache.put('ui-grid/ui-grid-filter', TableGridFilterTemplate); |
| builtInViz.instance._uiGridConstants = uiGridConstants; |
| builtInViz.instance._timeout = $timeout; |
| |
| builtInViz.instance._createNewScope = createNewScope; |
| builtInViz.instance._templateRequest = $templateRequest; |
| const transformation = builtInViz.instance.getTransformation(); |
| transformation._emitter = emitter; |
| transformation._templateRequest = $templateRequest; |
| transformation._compile = $compile; |
| transformation._createNewScope = createNewScope; |
| |
| // render |
| const transformed = transformation.transform(tableData); |
| transformation.renderSetting(transformationSettingTargetEl); |
| builtInViz.instance.render(transformed); |
| builtInViz.instance.renderSetting(visualizationSettingTargetEl); |
| builtInViz.instance.activate(); |
| angular.element(window).resize(() => { |
| builtInViz.instance.resize(); |
| }); |
| } catch (err) { |
| console.error('Graph drawing error %o', err); |
| } |
| }; |
| } else if (refresh) { |
| // when graph options or data are changed |
| console.log('Refresh data %o', tableData); |
| |
| afterLoaded = function(loadedElem) { |
| const transformationSettingTargetEl = getTrSettingElem($scope.id, graphMode); |
| const visualizationSettingTargetEl = getVizSettingElem($scope.id, graphMode); |
| const config = getVizConfig(graphMode); |
| loadedElem.height(height); |
| const transformation = builtInViz.instance.getTransformation(); |
| transformation.setConfig(config); |
| const transformed = transformation.transform(tableData); |
| transformation.renderSetting(transformationSettingTargetEl); |
| builtInViz.instance.setConfig(config); |
| builtInViz.instance.render(transformed); |
| builtInViz.instance.renderSetting(visualizationSettingTargetEl); |
| builtInViz.instance.activate(); |
| }; |
| } else { |
| afterLoaded = function(loadedElem) { |
| loadedElem.height(height); |
| builtInViz.instance.activate(); |
| }; |
| } |
| |
| const tableElemId = `p${$scope.id}_${graphMode}`; |
| retryUntilElemIsLoaded(tableElemId, afterLoaded); |
| }; |
| |
| $scope.switchViz = function(newMode) { |
| let newConfig = angular.copy($scope.config); |
| let newParams = angular.copy(paragraph.settings.params); |
| |
| // graph options |
| newConfig.graph.mode = newMode; |
| |
| // see switchApp() |
| _.set(newConfig, 'helium.activeApp', undefined); |
| |
| commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); |
| }; |
| |
| const createNewScope = function() { |
| return $rootScope.$new(true); |
| }; |
| |
| const commitParagraphResult = function(title, text, config, params) { |
| let newParagraphConfig = angular.copy(paragraph.config); |
| newParagraphConfig.results = newParagraphConfig.results || []; |
| newParagraphConfig.results[resultIndex] = config; |
| if ($scope.revisionView === true) { |
| // local update without commit |
| updateData({ |
| type: $scope.type, |
| data: data, |
| }, newParagraphConfig.results[resultIndex], paragraph, resultIndex); |
| renderResult($scope.type, true); |
| } else { |
| return websocketMsgSrv.commitParagraph(paragraph.id, title, text, newParagraphConfig, params); |
| } |
| }; |
| |
| $scope.toggleGraphSetting = function() { |
| let newConfig = angular.copy($scope.config); |
| if (newConfig.graph.optionOpen) { |
| newConfig.graph.optionOpen = false; |
| } else { |
| newConfig.graph.optionOpen = true; |
| } |
| |
| let newParams = angular.copy(paragraph.settings.params); |
| commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); |
| }; |
| |
| const getVizConfig = function(vizId) { |
| let config; |
| let graph = $scope.config.graph; |
| if (graph) { |
| // copy setting for vizId |
| if (graph.setting) { |
| config = angular.copy(graph.setting[vizId]); |
| } |
| |
| if (!config) { |
| config = {}; |
| } |
| |
| // copy common setting |
| config.common = angular.copy(graph.commonSetting) || {}; |
| |
| // copy pivot setting |
| if (graph.keys) { |
| config.common.pivot = { |
| keys: angular.copy(graph.keys), |
| groups: angular.copy(graph.groups), |
| values: angular.copy(graph.values), |
| }; |
| } |
| } |
| console.debug('getVizConfig', config); |
| return config; |
| }; |
| |
| const commitVizConfigChange = function(config, vizId) { |
| if ([ParagraphStatus.RUNNING, ParagraphStatus.PENDING].indexOf(paragraph.status) < 0) { |
| let newConfig = angular.copy($scope.config); |
| if (!newConfig.graph) { |
| newConfig.graph = {}; |
| } |
| // copy setting for vizId |
| if (!newConfig.graph.setting) { |
| newConfig.graph.setting = {}; |
| } |
| newConfig.graph.setting[vizId] = angular.copy(config); |
| // copy common setting |
| if (newConfig.graph.setting[vizId]) { |
| newConfig.graph.commonSetting = newConfig.graph.setting[vizId].common; |
| delete newConfig.graph.setting[vizId].common; |
| } |
| // copy pivot setting |
| if (newConfig.graph.commonSetting && newConfig.graph.commonSetting.pivot) { |
| newConfig.graph.keys = newConfig.graph.commonSetting.pivot.keys; |
| newConfig.graph.groups = newConfig.graph.commonSetting.pivot.groups; |
| newConfig.graph.values = newConfig.graph.commonSetting.pivot.values; |
| delete newConfig.graph.commonSetting.pivot; |
| } |
| if (angular.equals($scope.config, newConfig)) { |
| return; |
| } |
| console.debug('committVizConfig', newConfig); |
| let newParams = angular.copy(paragraph.settings.params); |
| commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); |
| } |
| }; |
| |
| $scope.$on('paragraphResized', function(event, paragraphId) { |
| // paragraph col width changed |
| if (paragraphId === paragraph.id) { |
| let builtInViz = builtInVisualizations[$scope.graphMode]; |
| if (builtInViz && builtInViz.instance) { |
| $timeout(() => builtInViz.instance.resize(), 200); |
| } |
| } |
| }); |
| |
| $scope.resize = function(width, height) { |
| $timeout(function() { |
| changeHeight(width, height); |
| }, 200); |
| }; |
| |
| const changeHeight = function(width, height) { |
| let newParams = angular.copy(paragraph.settings.params); |
| let newConfig = angular.copy($scope.config); |
| |
| newConfig.graph.height = height; |
| paragraph.config.colWidth = width; |
| |
| commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams); |
| }; |
| |
| $scope.exportToDSV = function(delimiter) { |
| let dsv = ''; |
| let dateFinished = moment(paragraph.dateFinished).format('YYYY-MM-DD hh:mm:ss A'); |
| let exportedFileName = paragraph.title ? paragraph.title + '_' + dateFinished : 'data_' + dateFinished; |
| |
| for (let titleIndex in tableData.columns) { |
| if (tableData.columns.hasOwnProperty(titleIndex)) { |
| dsv += tableData.columns[titleIndex].name + delimiter; |
| } |
| } |
| dsv = dsv.substring(0, dsv.length - 1) + '\n'; |
| for (let r in tableData.rows) { |
| if (tableData.rows.hasOwnProperty(r)) { |
| let row = tableData.rows[r]; |
| let dsvRow = ''; |
| for (let index in row) { |
| if (row.hasOwnProperty(index)) { |
| let stringValue = (row[index]).toString(); |
| if (stringValue.indexOf(delimiter) > -1) { |
| dsvRow += '"' + stringValue + '"' + delimiter; |
| } else { |
| dsvRow += row[index] + delimiter; |
| } |
| } |
| } |
| dsv += dsvRow.substring(0, dsvRow.length - 1) + '\n'; |
| } |
| } |
| let extension = ''; |
| if (delimiter === '\t') { |
| extension = 'tsv'; |
| } else if (delimiter === ',') { |
| extension = 'csv'; |
| } |
| saveAsService.saveAs(dsv, exportedFileName, extension); |
| }; |
| |
| $scope.getBase64ImageSrc = function(base64Data) { |
| return 'data:image/png;base64,' + base64Data; |
| }; |
| |
| // Helium ---------------- |
| let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; |
| |
| // app states |
| $scope.apps = []; |
| |
| // suggested apps |
| $scope.suggestion = {}; |
| |
| $scope.switchApp = function(appId) { |
| let newConfig = angular.copy($scope.config); |
| let newParams = angular.copy(paragraph.settings.params); |
| |
| // 'helium.activeApp' can be cleared by switchViz() |
| _.set(newConfig, 'helium.activeApp', appId); |
| |
| commitConfig(newConfig, newParams); |
| }; |
| |
| $scope.loadApp = function(heliumPackage) { |
| let noteId = $route.current.pathParams.noteId; |
| $http.post(baseUrlSrv.getRestApiBase() + '/helium/load/' + noteId + '/' + paragraph.id, heliumPackage) |
| .success(function(data, status, headers, config) { |
| console.log('Load app %o', data); |
| }) |
| .error(function(err, status, headers, config) { |
| console.log('Error %o', err); |
| }); |
| }; |
| |
| const commitConfig = function(config, params) { |
| commitParagraphResult(paragraph.title, paragraph.text, config, params); |
| }; |
| |
| const getApplicationStates = function() { |
| let appStates = []; |
| |
| // Display ApplicationState |
| if (paragraph.apps) { |
| _.forEach(paragraph.apps, function(app) { |
| appStates.push({ |
| id: app.id, |
| pkg: app.pkg, |
| status: app.status, |
| output: app.output, |
| }); |
| }); |
| } |
| |
| // update or remove app states no longer exists |
| _.forEach($scope.apps, function(currentAppState, idx) { |
| let newAppState = _.find(appStates, {id: currentAppState.id}); |
| if (newAppState) { |
| angular.extend($scope.apps[idx], newAppState); |
| } else { |
| $scope.apps.splice(idx, 1); |
| } |
| }); |
| |
| // add new app states |
| _.forEach(appStates, function(app, idx) { |
| if ($scope.apps.length <= idx || $scope.apps[idx].id !== app.id) { |
| $scope.apps.splice(idx, 0, app); |
| } |
| }); |
| }; |
| |
| const getSuggestions = function() { |
| // Get suggested apps |
| let noteId = $route.current.pathParams.noteId; |
| if (!noteId) { |
| return; |
| } |
| $http.get(baseUrlSrv.getRestApiBase() + '/helium/suggest/' + noteId + '/' + paragraph.id) |
| .success(function(data, status, headers, config) { |
| $scope.suggestion = data.body; |
| }) |
| .error(function(err, status, headers, config) { |
| console.log('Error %o', err); |
| }); |
| }; |
| |
| const renderApp = function(targetElemId, appState) { |
| const afterLoaded = (loadedElem) => { |
| try { |
| console.log('renderApp %o', appState); |
| loadedElem.html(appState.output); |
| $compile(loadedElem.contents())(getAppScope(appState)); |
| } catch (err) { |
| console.log('App rendering error %o', err); |
| } |
| }; |
| retryUntilElemIsLoaded(targetElemId, afterLoaded); |
| }; |
| |
| /* |
| ** $scope.$on functions below |
| */ |
| $scope.$on('appendAppOutput', function(event, data) { |
| if (paragraph.id === data.paragraphId) { |
| let app = _.find($scope.apps, {id: data.appId}); |
| if (app) { |
| app.output += data.data; |
| |
| let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); |
| paragraphAppState.output = app.output; |
| |
| let targetEl = angular.element(document.getElementById('p' + app.id)); |
| targetEl.html(app.output); |
| $compile(targetEl.contents())(getAppScope(app)); |
| console.log('append app output %o', $scope.apps); |
| } |
| } |
| }); |
| |
| $scope.$on('updateAppOutput', function(event, data) { |
| if (paragraph.id === data.paragraphId) { |
| let app = _.find($scope.apps, {id: data.appId}); |
| if (app) { |
| app.output = data.data; |
| |
| let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); |
| paragraphAppState.output = app.output; |
| |
| let targetEl = angular.element(document.getElementById('p' + app.id)); |
| targetEl.html(app.output); |
| $compile(targetEl.contents())(getAppScope(app)); |
| console.log('append app output'); |
| } |
| } |
| }); |
| |
| $scope.$on('appLoad', function(event, data) { |
| if (paragraph.id === data.paragraphId) { |
| let app = _.find($scope.apps, {id: data.appId}); |
| if (!app) { |
| app = { |
| id: data.appId, |
| pkg: data.pkg, |
| status: 'UNLOADED', |
| output: '', |
| }; |
| |
| $scope.apps.push(app); |
| paragraph.apps.push(app); |
| $scope.switchApp(app.id); |
| } |
| } |
| }); |
| |
| $scope.$on('appStatusChange', function(event, data) { |
| if (paragraph.id === data.paragraphId) { |
| let app = _.find($scope.apps, {id: data.appId}); |
| if (app) { |
| app.status = data.status; |
| let paragraphAppState = _.find(paragraph.apps, {id: data.appId}); |
| paragraphAppState.status = app.status; |
| } |
| } |
| }); |
| |
| let getAppRegistry = function(appState) { |
| if (!appState.registry) { |
| appState.registry = {}; |
| } |
| |
| return appState.registry; |
| }; |
| |
| const getAppScope = function(appState) { |
| if (!appState.scope) { |
| appState.scope = $rootScope.$new(true, $rootScope); |
| } |
| return appState.scope; |
| }; |
| |
| $scope.$on('angularObjectUpdate', function(event, data) { |
| let noteId = $route.current.pathParams.noteId; |
| if (!data.noteId || data.noteId === noteId) { |
| let scope; |
| let registry; |
| |
| let app = _.find($scope.apps, {id: data.paragraphId}); |
| if (app) { |
| scope = getAppScope(app); |
| registry = getAppRegistry(app); |
| } else { |
| // no matching app in this paragraph |
| return; |
| } |
| |
| let varName = data.angularObject.name; |
| |
| if (angular.equals(data.angularObject.object, scope[varName])) { |
| // return when update has no change |
| return; |
| } |
| |
| if (!registry[varName]) { |
| registry[varName] = { |
| interpreterGroupId: data.interpreterGroupId, |
| noteId: data.noteId, |
| paragraphId: data.paragraphId, |
| }; |
| } else { |
| registry[varName].noteId = registry[varName].noteId || data.noteId; |
| registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId; |
| } |
| |
| registry[varName].skipEmit = true; |
| |
| if (!registry[varName].clearWatcher) { |
| registry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) { |
| console.log('angular object (paragraph) updated %o %o', varName, registry[varName]); |
| if (registry[varName].skipEmit) { |
| registry[varName].skipEmit = false; |
| return; |
| } |
| websocketMsgSrv.updateAngularObject( |
| registry[varName].noteId, |
| registry[varName].paragraphId, |
| varName, |
| newValue, |
| registry[varName].interpreterGroupId); |
| }); |
| } |
| console.log('angular object (paragraph) created %o', varName); |
| scope[varName] = data.angularObject.object; |
| |
| // create proxy for AngularFunction |
| if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { |
| let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); |
| scope[funcName] = function() { |
| // eslint-disable-next-line prefer-rest-params |
| scope[varName] = arguments; |
| // eslint-disable-next-line prefer-rest-params |
| console.log('angular function (paragraph) invoked %o', arguments); |
| }; |
| |
| console.log('angular function (paragraph) created %o', scope[funcName]); |
| } |
| } |
| }); |
| |
| $scope.$on('angularObjectRemove', function(event, data) { |
| let noteId = $route.current.pathParams.noteId; |
| if (!data.noteId || data.noteId === noteId) { |
| let scope; |
| let registry; |
| |
| let app = _.find($scope.apps, {id: data.paragraphId}); |
| if (app) { |
| scope = getAppScope(app); |
| registry = getAppRegistry(app); |
| } else { |
| // no matching app in this paragraph |
| return; |
| } |
| |
| let varName = data.name; |
| |
| // clear watcher |
| if (registry[varName]) { |
| registry[varName].clearWatcher(); |
| registry[varName] = undefined; |
| } |
| |
| // remove scope variable |
| scope[varName] = undefined; |
| |
| // remove proxy for AngularFunction |
| if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { |
| let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); |
| scope[funcName] = undefined; |
| } |
| } |
| }); |
| } |