diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js index 172d7e1dcb..22663ba6ed 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -23,9 +23,8 @@ import { /*eslint-enable no-unused-vars */ export class AbstractFrontendInterpreter { - constructor(magic, displayType) { + constructor(magic) { this.magic = magic; - this.displayType = displayType; } /** @@ -37,6 +36,7 @@ export class AbstractFrontendInterpreter { */ interpret(paragraphText) { /** implement this if you want to add a frontend interpreter */ + throw new Error('AbstractFrontendInterpreter.interpret is not overrided'); } /** @@ -47,13 +47,4 @@ export class AbstractFrontendInterpreter { getMagic() { return this.magic; } - - /** - * return display type for this frontend interpreter. - * (e.g `DefaultDisplayType.TEXT`) - * @return {string} - */ - getDisplayType() { - return this.displayType; - } } diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js index 0437751755..e06459b5cf 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js @@ -53,7 +53,7 @@ export class GeneratorWithType { return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); } - const splited = generator.split("\n"); + const splited = generator.split('\n'); const gensWithTypes = []; let mergedGens = []; diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index e91873ff9d..d1a93158f4 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -230,18 +230,18 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat $scope.paragraph.errorMessage = error.stack; console.error('Failed to execute FrontendInterpreter.interpret\n', error); $scope.$digest(); - } + }; - $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, magic) { - // clear results which is watched by `ng-repeat`. without this, - // frontend interpreter results cannot be rendered in view more than once + $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, + magic, digestRequired) { $scope.paragraph.results = {}; - $scope.$digest(); + if (digestRequired) { $scope.$digest(); } try { // remove magic from paragraphText const splited = paragraphText.split(magic); - const textWithoutMagic = splited[1]; + // remove leading spaces + const textWithoutMagic = splited[1].replace(/^\s+/g, ''); $scope.paragraph.status = 'FINISHED'; const frontIntpResult = intp.interpret(textWithoutMagic); const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( @@ -249,9 +249,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat parsed.then(resultsMsg => { $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; - $scope.$digest(); + if (digestRequired) { $scope.$digest(); } }).catch($scope.handleFrontendInterpreterError); - // TODO(1ambda): broadcast to other clients } catch (error) { $scope.handleFrontendInterpreterError(error); } @@ -277,7 +276,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat commitParagraph(paragraph); }; - $scope.runParagraph = function(paragraphText) { + $scope.runParagraph = function(paragraphText, digestRequired) { if (!paragraphText || $scope.isRunning($scope.paragraph)) { return; } @@ -286,7 +285,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic); if (frontendIntp) { - $scope.runParagraphUsingFrontendInterpreter(frontendIntp, paragraphText, magic); + $scope.runParagraphUsingFrontendInterpreter( + frontendIntp, paragraphText, magic, digestRequired); } else { $scope.runParagraphUsingBackendInterpreter(paragraphText); } @@ -306,14 +306,14 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat }; $scope.runParagraphFromShortcut = function(paragraphText) { - // we need to call `$digest()` to update view immediately - $scope.runParagraph(paragraphText); - $scope.$digest(); + // passing `digestRequired` as true to update view immediately + // without this, results cannot be rendered in view more than once + $scope.runParagraph(paragraphText, true); }; $scope.runParagraphFromButton = function(paragraphText) { // we come here from `$scope.on`, so we don't need to call `$digest()` - $scope.runParagraph(paragraphText) + $scope.runParagraph(paragraphText, false) }; $scope.moveUp = function(paragraph) { diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index d17b42f536..cb32a3200a 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -252,8 +252,40 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; - var renderResult = function(type, refresh) { - var activeApp; + $scope.createDisplayDOMId = function(baseDOMId, type) { + if (type === DefaultDisplayType.TABLE) { + 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) { + if (type === DefaultDisplayType.TABLE) { + $scope.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); + } else if (type === DefaultDisplayType.ELEMENT) { + renderElem(targetElemId, data); + } else { + console.error(`Unknown Display Type: ${type}`); + } + }; + + const renderResult = function(type, refresh) { + let activeApp; if (enableHelium) { getSuggestions(); getApplicationStates(); @@ -261,50 +293,64 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } if (activeApp) { - const app = _.find($scope.apps, {id: activeApp}); - renderApp(app, `p${appState.id}`); + const appState = _.find($scope.apps, {id: activeApp}); + renderApp(`p${appState.id}`, appState); } else { - - /** - * find proper interpreter which can handle the non-default display type - * the displayed type which is generated from `display()` - * should be one of the default interpreter type - */ - // const isDefaultDisplayType = DefaultDisplayType[type]; - // if (!isDefaultDisplayType) { - // const intp = heliumService.getFrontendInterpreterWithDisplayType(type); - // - // if (intp) { - // // currently display only accepts `Object` data not - // // doesn't support function and promise - // const result = intp.display(data); - // type = result.getType(); - // data = result.getData(); - // } - // } - - if (type === DefaultDisplayType.TABLE) { - $scope.renderGraph($scope.graphMode, refresh); - } else if (type === DefaultDisplayType.HTML) { - renderHtml(`p${$scope.id}_html`, data); - } else if (type === DefaultDisplayType.ANGULAR) { - renderAngular(`p${$scope.id}_angular`, data); - } else if (type === DefaultDisplayType.TEXT) { - renderText(`p${$scope.id}_text`, data); - } else if (type === DefaultDisplayType.ELEMENT) { - renderElem(`p${$scope.id}_elem`, data); + if (!DefaultDisplayType[type]) { + const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(type); + if (!frontendIntp) { + console.error(`Unknown Display Type: ${type}`); + return; + } + $scope.renderCustomDisplay(type, data, frontendIntp); } else { - console.error(`Unknown Display Type: ${type}`); + 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, frontendIntp) { + // get result from intp + + const frontIntpResult = frontendIntp.interpret(data.trim()); + const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( + heliumService.getAvailableFrontendInterpreters()); + + // custom display result can include multiple subset results + parsed.then(dataWithTypes => { + const containerDOM = document.getElementById(`p${$scope.id}_custom`); + for(let i = 0; i < dataWithTypes.length; i++) { + const dt = dataWithTypes[i]; + const data = dt.data; + const type = dt.type; + + // prepare DOM to be filled + const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom`, type); + const subResultDOM = document.createElement('div'); + containerDOM.appendChild(subResultDOM); + subResultDOM.setAttribute('id', subResultDOMId); + + $scope.renderDefaultDisplay(subResultDOMId, type, data, true); + } + }).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 generator {Object or Promise or Function} + * @param generator {Object or Function} * @param type {string} Display Type * @param successCallback * @param failureCallback @@ -317,13 +363,6 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location failureCallback(error); console.error(`Failed to handle ${type} type, function generator\n`, error); } - } else if (FrontendInterpreterResult.isPromiseGenerator(generator)) { - generator - .then((generated) => { successCallback(generated); }) - .catch((error) => { - failureCallback(error); - console.error(`Failed to handle ${type} type, promise generator\n`, error); - }); } else if (FrontendInterpreterResult.isObjectGenerator(generator)) { try { successCallback(generator); @@ -343,7 +382,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location generateData(() => { generator(targetElemId) }, DefaultDisplayType.ELEMENT, () => {}, /** HTML element will be filled in generator . thus pass empty success callback */ - (error) => { elem.html(`${error.stack}`); } + (error) => { elem.html(`${error.stack}`); } ); } @@ -421,7 +460,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } $timeout(retryRenderer); - } + }; var clearTextOutput = function() { var textEl = getTextResultElem($scope.id); @@ -457,11 +496,11 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; - $scope.renderGraph = function(type, refresh) { + $scope.renderGraph = function(targetElemId, type, refresh) { // set graph height var height = $scope.config.graph.height; - var graphContainerEl = angular.element('#p' + $scope.id + '_graph'); - graphContainerEl.height(height); + var targetElem = angular.element(`#${targetElemId}`); + targetElem.height(height); if (!type) { type = 'table'; @@ -807,7 +846,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location const renderApp = function(targetElemId, appState) { function retryRenderer() { - var elem = angular.element(document.getElementById(elememId)); + var elem = angular.element(document.getElementById(targetElemId)); console.log('retry renderApp %o', elem); if (!elem.length) { $timeout(retryRenderer, 1000); diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index 90fa9bc6c0..287cb65379 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -67,6 +67,10 @@ limitations under the License. tooltip="Scroll Top"> +
+
+
diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index c438d355de..8f3c70a131 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -13,10 +13,6 @@ */ import { HeliumType, } from './helium-type'; -import { - AbstractFrontendInterpreter, - DefaultDisplayType -} from '../../app/frontend-interpreter' (function() { angular.module('zeppelinWebApp').service('heliumService', heliumService);