http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/package.json ---------------------------------------------------------------------- diff --git a/zeppelin-web/package.json b/zeppelin-web/package.json index 0ad6398..c9bba37 100644 --- a/zeppelin-web/package.json +++ b/zeppelin-web/package.json @@ -12,7 +12,7 @@ "build": "grunt pre-webpack-dist && webpack && grunt post-webpack-dist", "predev": "grunt pre-webpack-dev", "dev:server": "webpack-dev-server --hot", - "visdev:server": "HELIUM_VIS_DEV=true webpack-dev-server --hot", + "dev:helium": "HELIUM_BUNDLE_DEV=true webpack-dev-server --hot", "dev:watch": "grunt watch-webpack-dev", "dev": "npm-run-all --parallel dev:server dev:watch", "visdev": "npm-run-all --parallel visdev:server dev:watch",
http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/helium/helium.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/helium/helium.controller.js b/zeppelin-web/src/app/helium/helium.controller.js index a344e80..b68c1bb 100644 --- a/zeppelin-web/src/app/helium/helium.controller.js +++ b/zeppelin-web/src/app/helium/helium.controller.js @@ -12,208 +12,205 @@ * limitations under the License. */ -(function() { - - angular.module('zeppelinWebApp').controller('HeliumCtrl', HeliumCtrl); - - HeliumCtrl.$inject = ['$scope', '$rootScope', '$sce', 'baseUrlSrv', 'ngToast', 'heliumService']; - - function HeliumCtrl($scope, $rootScope, $sce, baseUrlSrv, ngToast, heliumService) { - $scope.packageInfos = {}; - $scope.defaultVersions = {}; - $scope.showVersions = {}; - $scope.visualizationOrder = []; - $scope.visualizationOrderChanged = false; - - var buildDefaultVersionListToDisplay = function(packageInfos) { - var defaultVersions = {}; - // show enabled version if any version of package is enabled - for (var name in packageInfos) { - var pkgs = packageInfos[name]; - for (var pkgIdx in pkgs) { - var pkg = pkgs[pkgIdx]; - pkg.pkg.icon = $sce.trustAsHtml(pkg.pkg.icon); - if (pkg.enabled) { - defaultVersions[name] = pkg; - pkgs.splice(pkgIdx, 1); - break; - } - } - - // show first available version if package is not enabled - if (!defaultVersions[name]) { - defaultVersions[name] = pkgs[0]; - pkgs.splice(0, 1); +angular.module('zeppelinWebApp').controller('HeliumCtrl', HeliumCtrl); + +HeliumCtrl.$inject = ['$scope', '$rootScope', '$sce', 'baseUrlSrv', 'ngToast', 'heliumService']; + +function HeliumCtrl($scope, $rootScope, $sce, baseUrlSrv, ngToast, heliumService) { + $scope.packageInfos = {}; + $scope.defaultVersions = {}; + $scope.showVersions = {}; + $scope.bundleOrder = []; + $scope.bundleOrderChanged = false; + + var buildDefaultVersionListToDisplay = function(packageInfos) { + var defaultVersions = {}; + // show enabled version if any version of package is enabled + for (var name in packageInfos) { + var pkgs = packageInfos[name]; + for (var pkgIdx in pkgs) { + var pkg = pkgs[pkgIdx]; + pkg.pkg.icon = $sce.trustAsHtml(pkg.pkg.icon); + if (pkg.enabled) { + defaultVersions[name] = pkg; + pkgs.splice(pkgIdx, 1); + break; } } - $scope.defaultVersions = defaultVersions; - }; - - var getAllPackageInfo = function() { - heliumService.getAllPackageInfo(). - success(function(data, status) { - $scope.packageInfos = data.body; - buildDefaultVersionListToDisplay($scope.packageInfos); - }). - error(function(data, status) { - console.log('Can not load package info %o %o', status, data); - }); - }; - - var getVisualizationOrder = function() { - heliumService.getVisualizationOrder(). - success(function(data, status) { - $scope.visualizationOrder = data.body; - }). - error(function(data, status) { - console.log('Can not get visualization order %o %o', status, data); - }); - }; - - $scope.visualizationOrderListeners = { - accept: function(sourceItemHandleScope, destSortableScope) {return true;}, - itemMoved: function(event) {}, - orderChanged: function(event) { - $scope.visualizationOrderChanged = true; + + // show first available version if package is not enabled + if (!defaultVersions[name]) { + defaultVersions[name] = pkgs[0]; + pkgs.splice(0, 1); } - }; - - var init = function() { - getAllPackageInfo(); - getVisualizationOrder(); - $scope.visualizationOrderChanged = false; - }; - - init(); - - $scope.saveVisualizationOrder = function() { - var confirm = BootstrapDialog.confirm({ - closable: false, - closeByBackdrop: false, - closeByKeyboard: false, - title: '', - message: 'Save changes?', - callback: function(result) { - if (result) { - confirm.$modalFooter.find('button').addClass('disabled'); - confirm.$modalFooter.find('button:contains("OK")') - .html('<i class="fa fa-circle-o-notch fa-spin"></i> Enabling'); - heliumService.setVisualizationOrder($scope.visualizationOrder). - success(function(data, status) { - init(); - confirm.close(); - }). - error(function(data, status) { - confirm.close(); - console.log('Failed to save order'); - BootstrapDialog.show({ - title: 'Error on saving order ', - message: data.message - }); - }); - return false; - } - } - }); } + $scope.defaultVersions = defaultVersions; + }; + + var getAllPackageInfo = function() { + heliumService.getAllPackageInfo(). + success(function(data, status) { + $scope.packageInfos = data.body; + buildDefaultVersionListToDisplay($scope.packageInfos); + }). + error(function(data, status) { + console.log('Can not load package info %o %o', status, data); + }); + }; + + var getBundleOrder = function() { + heliumService.getVisualizationPackageOrder(). + success(function(data, status) { + $scope.bundleOrder = data.body; + }). + error(function(data, status) { + console.log('Can not get bundle order %o %o', status, data); + }); + }; + + $scope.bundleOrderListeners = { + accept: function(sourceItemHandleScope, destSortableScope) {return true;}, + itemMoved: function(event) {}, + orderChanged: function(event) { + $scope.bundleOrderChanged = true; + } + }; + + var init = function() { + getAllPackageInfo(); + getBundleOrder(); + $scope.bundleOrderChanged = false; + }; + + init(); + + $scope.saveBundleOrder = function() { + var confirm = BootstrapDialog.confirm({ + closable: false, + closeByBackdrop: false, + closeByKeyboard: false, + title: '', + message: 'Save changes?', + callback: function(result) { + if (result) { + confirm.$modalFooter.find('button').addClass('disabled'); + confirm.$modalFooter.find('button:contains("OK")') + .html('<i class="fa fa-circle-o-notch fa-spin"></i> Enabling'); + heliumService.setVisualizationPackageOrder($scope.bundleOrder). + success(function(data, status) { + init(); + confirm.close(); + }). + error(function(data, status) { + confirm.close(); + console.log('Failed to save order'); + BootstrapDialog.show({ + title: 'Error on saving order ', + message: data.message + }); + }); + return false; + } + } + }); + } - var getLicense = function(name, artifact) { - var pkg = _.filter($scope.defaultVersions[name], function(p) { - return p.artifact === artifact; - }); + var getLicense = function(name, artifact) { + var pkg = _.filter($scope.defaultVersions[name], function(p) { + return p.artifact === artifact; + }); - var license; - if (pkg.length === 0) { - pkg = _.filter($scope.packageInfos[name], function(p) { - return p.pkg.artifact === artifact; - }); + var license; + if (pkg.length === 0) { + pkg = _.filter($scope.packageInfos[name], function(p) { + return p.pkg.artifact === artifact; + }); - if (pkg.length > 0) { - license = pkg[0].pkg.license; - } - } else { - license = pkg[0].license; + if (pkg.length > 0) { + license = pkg[0].pkg.license; } + } else { + license = pkg[0].license; + } - if (!license) { - license = 'Unknown'; - } - return license; + if (!license) { + license = 'Unknown'; } + return license; + } - $scope.enable = function(name, artifact) { - var license = getLicense(name, artifact); - - var confirm = BootstrapDialog.confirm({ - closable: false, - closeByBackdrop: false, - closeByKeyboard: false, - title: '', - message: 'Do you want to enable ' + name + '?' + - '<div style="color:gray">' + artifact + '</div>' + - '<div style="border-top: 1px solid #efefef; margin-top: 10px; padding-top: 5px;">License</div>' + - '<div style="color:gray">' + license + '</div>', - callback: function(result) { - if (result) { - confirm.$modalFooter.find('button').addClass('disabled'); - confirm.$modalFooter.find('button:contains("OK")') - .html('<i class="fa fa-circle-o-notch fa-spin"></i> Enabling'); - heliumService.enable(name, artifact). - success(function(data, status) { - init(); - confirm.close(); - }). - error(function(data, status) { - confirm.close(); - console.log('Failed to enable package %o %o. %o', name, artifact, data); - BootstrapDialog.show({ - title: 'Error on enabling ' + name, - message: data.message - }); - }); - return false; - } + $scope.enable = function(name, artifact) { + var license = getLicense(name, artifact); + + var confirm = BootstrapDialog.confirm({ + closable: false, + closeByBackdrop: false, + closeByKeyboard: false, + title: '', + message: 'Do you want to enable ' + name + '?' + + '<div style="color:gray">' + artifact + '</div>' + + '<div style="border-top: 1px solid #efefef; margin-top: 10px; padding-top: 5px;">License</div>' + + '<div style="color:gray">' + license + '</div>', + callback: function(result) { + if (result) { + confirm.$modalFooter.find('button').addClass('disabled'); + confirm.$modalFooter.find('button:contains("OK")') + .html('<i class="fa fa-circle-o-notch fa-spin"></i> Enabling'); + heliumService.enable(name, artifact). + success(function(data, status) { + init(); + confirm.close(); + }). + error(function(data, status) { + confirm.close(); + console.log('Failed to enable package %o %o. %o', name, artifact, data); + BootstrapDialog.show({ + title: 'Error on enabling ' + name, + message: data.message + }); + }); + return false; } - }); - }; - - $scope.disable = function(name) { - var confirm = BootstrapDialog.confirm({ - closable: false, - closeByBackdrop: false, - closeByKeyboard: false, - title: '', - message: 'Do you want to disable ' + name + '?', - callback: function(result) { - if (result) { - confirm.$modalFooter.find('button').addClass('disabled'); - confirm.$modalFooter.find('button:contains("OK")') - .html('<i class="fa fa-circle-o-notch fa-spin"></i> Disabling'); - heliumService.disable(name). - success(function(data, status) { - init(); - confirm.close(); - }). - error(function(data, status) { - confirm.close(); - console.log('Failed to disable package %o. %o', name, data); - BootstrapDialog.show({ - title: 'Error on disabling ' + name, - message: data.message - }); - }); - return false; - } + } + }); + }; + + $scope.disable = function(name) { + var confirm = BootstrapDialog.confirm({ + closable: false, + closeByBackdrop: false, + closeByKeyboard: false, + title: '', + message: 'Do you want to disable ' + name + '?', + callback: function(result) { + if (result) { + confirm.$modalFooter.find('button').addClass('disabled'); + confirm.$modalFooter.find('button:contains("OK")') + .html('<i class="fa fa-circle-o-notch fa-spin"></i> Disabling'); + heliumService.disable(name). + success(function(data, status) { + init(); + confirm.close(); + }). + error(function(data, status) { + confirm.close(); + console.log('Failed to disable package %o. %o', name, data); + BootstrapDialog.show({ + title: 'Error on disabling ' + name, + message: data.message + }); + }); + return false; } - }); - }; - - $scope.toggleVersions = function(pkgName) { - if ($scope.showVersions[pkgName]) { - $scope.showVersions[pkgName] = false; - } else { - $scope.showVersions[pkgName] = true; } - }; - } -})(); + }); + }; + + $scope.toggleVersions = function(pkgName) { + if ($scope.showVersions[pkgName]) { + $scope.showVersions[pkgName] = false; + } else { + $scope.showVersions[pkgName] = true; + } + }; +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/helium/helium.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/helium/helium.css b/zeppelin-web/src/app/helium/helium.css index f17d6bd..e66797d 100644 --- a/zeppelin-web/src/app/helium/helium.css +++ b/zeppelin-web/src/app/helium/helium.css @@ -51,11 +51,33 @@ margin-top: 0; } -.heliumPackageList .heliumPackageName span { - font-size: 10px; +.heliumPackageList .heliumPackageName .heliumType { + font-size: 13px; color: #AAAAAA; } +.spellInfo { + margin-top: 15px; + font-size: 20px; + font-weight: bold; +} + +.spellInfo .spellInfoDesc { + font-size: 13px; + color: #AAAAAA; +} + +.spellInfo .spellInfoValue { + font-size: 13px; + font-style: italic; + color: #444444; +} + +.spellInfo .spellUsage { + margin-top: 8px; + margin-bottom: 4px; + width: 500px; +} .heliumPackageList .heliumPackageDisabledArtifact { color:gray; @@ -77,12 +99,12 @@ margin-top: 10px; } -.heliumVisualizationOrder { +.heliumBundleOrder { display: inline-block; } -.heliumVisualizationOrder .as-sortable-item, -.heliumVisualizationOrder .as-sortable-placeholder { +.heliumBundleOrder .as-sortable-item, +.heliumBundleOrder .as-sortable-placeholder { display: inline-block; float: left; } @@ -97,7 +119,7 @@ height: 100%; } -.heliumVisualizationOrder .saveLink { +.heliumBundleOrder .saveLink { margin-left:10px; margin-top:5px; cursor:pointer; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/helium/helium.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/helium/helium.html b/zeppelin-web/src/app/helium/helium.html index 546995c..341ade3 100644 --- a/zeppelin-web/src/app/helium/helium.html +++ b/zeppelin-web/src/app/helium/helium.html @@ -20,13 +20,13 @@ limitations under the License. </h3> </div> </div> - <div ng-show="visualizationOrder.length > 1" - class="row heliumVisualizationOrder"> - <div style="margin:0 0 5px 15px">Visualization package display order (drag and drop to reorder)</div> + <div ng-show="bundleOrder.length > 1" + class="row heliumBundleOrder"> + <div style="margin:0 0 5px 15px">Bundle package display order (drag and drop to reorder)</div> <div class="col-md-12 sortable-row btn-group" - as-sortable="visualizationOrderListeners" - data-ng-model="visualizationOrder"> - <div class="btn-group" data-ng-repeat="pkgName in visualizationOrder" + as-sortable="bundleOrderListeners" + data-ng-model="bundleOrder"> + <div class="btn-group" data-ng-repeat="pkgName in bundleOrder" as-sortable-item> <div class="btn btn-default btn-sm" ng-bind-html='defaultVersions[pkgName].pkg.icon' @@ -34,8 +34,8 @@ limitations under the License. </div> </div> <span class="saveLink" - ng-show="visualizationOrderChanged" - ng-click="saveVisualizationOrder()"> + ng-show="bundleOrderChanged" + ng-click="saveBundleOrder()"> save </span> </div> @@ -50,7 +50,10 @@ limitations under the License. <div class="heliumPackageHead"> <div class="heliumPackageIcon" ng-bind-html=pkgInfo.pkg.icon></div> - <div class="heliumPackageName">{{pkgName}} <span>{{pkgInfo.pkg.type}}</span></div> + <div class="heliumPackageName"> + {{pkgName}} + <span class="heliumType">{{pkgInfo.pkg.type}}</span> + </div> <div ng-show="!pkgInfo.enabled" ng-click="enable(pkgName, pkgInfo.pkg.artifact)" class="btn btn-success btn-xs" @@ -81,6 +84,17 @@ limitations under the License. <div class="heliumPackageDescription"> {{pkgInfo.pkg.description}} </div> + <div ng-if="pkgInfo.pkg.type === 'SPELL' && pkgInfo.pkg.spell" + class="spellInfo"> + <div> + <span class="spellInfoDesc">MAGIC</span> + <span class="spellInfoValue">{{pkgInfo.pkg.spell.magic}} </span> + </div> + <div> + <span class="spellInfoDesc">USAGE</span> + <pre class="spellUsage">{{pkgInfo.pkg.spell.usage}} </pre> + </div> + </div> </div> </div> </div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/notebook.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index ccf64b7..b434b64 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -28,12 +28,14 @@ NotebookCtrl.$inject = [ 'ngToast', 'noteActionSrv', 'noteVarShareService', - 'TRASH_FOLDER_ID' + 'TRASH_FOLDER_ID', + 'heliumService', ]; function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, $http, websocketMsgSrv, baseUrlSrv, $timeout, saveAsService, - ngToast, noteActionSrv, noteVarShareService, TRASH_FOLDER_ID) { + ngToast, noteActionSrv, noteVarShareService, TRASH_FOLDER_ID, + heliumService) { ngToast.dismiss(); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html index 644761e..351fb5f 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html @@ -25,7 +25,7 @@ limitations under the License. <!-- Run / Cancel button --> <span ng-if="!revisionView"> <span class="icon-control-play" style="cursor:pointer;color:#3071A9" tooltip-placement="top" tooltip="Run this paragraph (Shift+Enter)" - ng-click="runParagraph(getEditorValue())" + ng-click="runParagraphFromButton(getEditorValue())" ng-show="paragraph.status!='RUNNING' && paragraph.status!='PENDING' && paragraph.config.enabled"></span> <span class="icon-control-pause" style="cursor:pointer;color:#CD5C5C" tooltip-placement="top" tooltip="Cancel (Ctrl+{{ (isMac ? 'Option' : 'Alt') }}+c)" http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html index 117e11c..65d13b7 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html @@ -22,14 +22,14 @@ limitations under the License. <input class="form-control input-sm" ng-if="!paragraph.settings.forms[formulaire.name].options" - ng-enter="runParagraph(getEditorValue())" + ng-enter="runParagraphFromButton(getEditorValue())" ng-model="paragraph.settings.params[formulaire.name]" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" name="{{formulaire.name}}" /> <select class="form-control input-sm" ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'" - ng-enter="runParagraph(getEditorValue())" + ng-enter="runParagraphFromButton(getEditorValue())" ng-model="paragraph.settings.params[formulaire.name]" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" name="{{formulaire.name}}" http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index ef35b49..da82080 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -12,6 +12,10 @@ * limitations under the License. */ +import { + SpellResult, +} from '../../spell'; + angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); ParagraphCtrl.$inject = [ @@ -29,15 +33,19 @@ ParagraphCtrl.$inject = [ 'baseUrlSrv', 'ngToast', 'saveAsService', - 'noteVarShareService' + 'noteVarShareService', + 'heliumService' ]; function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location, $timeout, $compile, $http, $q, websocketMsgSrv, - baseUrlSrv, ngToast, saveAsService, noteVarShareService) { + baseUrlSrv, ngToast, saveAsService, noteVarShareService, + heliumService) { var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; $scope.parentNote = null; - $scope.paragraph = null; + $scope.paragraph = {}; + $scope.paragraph.results = {}; + $scope.paragraph.results.msg = []; $scope.originalText = ''; $scope.editor = null; @@ -219,21 +227,77 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat websocketMsgSrv.cancelParagraphRun(paragraph.id); }; - $scope.runParagraph = function(data) { - websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title, - data, $scope.paragraph.config, $scope.paragraph.settings.params); - $scope.originalText = angular.copy(data); - $scope.dirtyText = undefined; + $scope.propagateSpellResult = function(paragraphId, paragraphTitle, + paragraphText, paragraphResults, + paragraphStatus, paragraphErrorMessage, + paragraphConfig, paragraphSettingsParam) { + websocketMsgSrv.paragraphExecutedBySpell( + paragraphId, paragraphTitle, + paragraphText, paragraphResults, + paragraphStatus, paragraphErrorMessage, + paragraphConfig, paragraphSettingsParam); + }; - if ($scope.paragraph.config.editorSetting.editOnDblClick) { - closeEditorAndOpenTable($scope.paragraph); - } else if (editorSetting.isOutputHidden && - !$scope.paragraph.config.editorSetting.editOnDblClick) { - // %md/%angular repl make output to be hidden by default after running - // so should open output if repl changed from %md/%angular to another - openEditorAndOpenTable($scope.paragraph); + $scope.handleSpellError = function(paragraphText, error, + digestRequired, propagated) { + const errorMessage = error.stack; + $scope.paragraph.status = 'ERROR'; + $scope.paragraph.errorMessage = errorMessage; + console.error('Failed to execute interpret() in spell\n', error); + if (digestRequired) { $scope.$digest(); } + + if (!propagated) { + $scope.propagateSpellResult( + $scope.paragraph.id, $scope.paragraph.title, + paragraphText, [], $scope.paragraph.status, errorMessage, + $scope.paragraph.config, $scope.paragraph.settings.params); + } + }; + + $scope.runParagraphUsingSpell = function(spell, paragraphText, + magic, digestRequired, propagated) { + $scope.paragraph.results = {}; + $scope.paragraph.errorMessage = ''; + if (digestRequired) { $scope.$digest(); } + + try { + // remove magic from paragraphText + const splited = paragraphText.split(magic); + // remove leading spaces + const textWithoutMagic = splited[1].replace(/^\s+/g, ''); + const spellResult = spell.interpret(textWithoutMagic); + const parsed = spellResult.getAllParsedDataWithTypes( + heliumService.getAllSpells(), magic, textWithoutMagic); + + // handle actual result message in promise + parsed.then(resultsMsg => { + const status = 'FINISHED'; + $scope.paragraph.status = status; + $scope.paragraph.results.code = status; + $scope.paragraph.results.msg = resultsMsg; + $scope.paragraph.config.tableHide = false; + if (digestRequired) { $scope.$digest(); } + + if (!propagated) { + const propagable = SpellResult.createPropagable(resultsMsg); + $scope.propagateSpellResult( + $scope.paragraph.id, $scope.paragraph.title, + paragraphText, propagable, status, '', + $scope.paragraph.config, $scope.paragraph.settings.params); + } + }).catch(error => { + $scope.handleSpellError(paragraphText, error, + digestRequired, propagated); + }); + } catch (error) { + $scope.handleSpellError(paragraphText, error, + digestRequired, propagated); } - editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick; + }; + + $scope.runParagraphUsingBackendInterpreter = function(paragraphText) { + websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title, + paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params); }; $scope.saveParagraph = function(paragraph) { @@ -251,10 +315,49 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat commitParagraph(paragraph); }; - $scope.run = function(paragraph, editorValue) { - if (editorValue && !$scope.isRunning(paragraph)) { - $scope.runParagraph(editorValue); + /** + * @param paragraphText to be parsed + * @param digestRequired true if calling `$digest` is required + * @param propagated true if update request is sent from other client + */ + $scope.runParagraph = function(paragraphText, digestRequired, propagated) { + if (!paragraphText || $scope.isRunning($scope.paragraph)) { + return; } + + const magic = SpellResult.extractMagic(paragraphText); + const spell = heliumService.getSpellByMagic(magic); + + if (spell) { + $scope.runParagraphUsingSpell( + spell, paragraphText, magic, digestRequired, propagated); + } else { + $scope.runParagraphUsingBackendInterpreter(paragraphText); + } + + $scope.originalText = angular.copy(paragraphText); + $scope.dirtyText = undefined; + + if ($scope.paragraph.config.editorSetting.editOnDblClick) { + closeEditorAndOpenTable($scope.paragraph); + } else if (editorSetting.isOutputHidden && + !$scope.paragraph.config.editorSetting.editOnDblClick) { + // %md/%angular repl make output to be hidden by default after running + // so should open output if repl changed from %md/%angular to another + openEditorAndOpenTable($scope.paragraph); + } + editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick; + }; + + $scope.runParagraphFromShortcut = function(paragraphText) { + // passing `digestRequired` as true to update view immediately + // without this, results cannot be rendered in view more than once + $scope.runParagraph(paragraphText, true, false); + }; + + $scope.runParagraphFromButton = function(paragraphText) { + // we come here from the view, so we don't need to call `$digest()` + $scope.runParagraph(paragraphText, false, false) }; $scope.moveUp = function(paragraph) { @@ -807,15 +910,6 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat editor.navigateFileEnd(); }; - $scope.getResultType = function(paragraph) { - var pdata = (paragraph) ? paragraph : $scope.paragraph; - if (pdata.results && pdata.results.type) { - return pdata.results.type; - } else { - return 'TEXT'; - } - }; - $scope.parseTableCell = function(cell) { if (!isNaN(cell)) { if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) { @@ -974,101 +1068,146 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat } }); - $scope.$on('updateParagraph', function(event, data) { - if (data.paragraph.id === $scope.paragraph.id && - (data.paragraph.dateCreated !== $scope.paragraph.dateCreated || - data.paragraph.dateFinished !== $scope.paragraph.dateFinished || - data.paragraph.dateStarted !== $scope.paragraph.dateStarted || - data.paragraph.dateUpdated !== $scope.paragraph.dateUpdated || - data.paragraph.status !== $scope.paragraph.status || - data.paragraph.jobName !== $scope.paragraph.jobName || - data.paragraph.title !== $scope.paragraph.title || - isEmpty(data.paragraph.results) !== isEmpty($scope.paragraph.results) || - data.paragraph.errorMessage !== $scope.paragraph.errorMessage || - !angular.equals(data.paragraph.settings, $scope.paragraph.settings) || - !angular.equals(data.paragraph.config, $scope.paragraph.config)) - ) { - var statusChanged = (data.paragraph.status !== $scope.paragraph.status); - var resultRefreshed = (data.paragraph.dateFinished !== $scope.paragraph.dateFinished) || - isEmpty(data.paragraph.results) !== isEmpty($scope.paragraph.results) || - data.paragraph.status === 'ERROR' || (data.paragraph.status === 'FINISHED' && statusChanged); - - if ($scope.paragraph.text !== data.paragraph.text) { - if ($scope.dirtyText) { // check if editor has local update - if ($scope.dirtyText === data.paragraph.text) { // when local update is the same from remote, clear local update - $scope.paragraph.text = data.paragraph.text; - $scope.dirtyText = undefined; - $scope.originalText = angular.copy(data.paragraph.text); - } else { // if there're local update, keep it. - $scope.paragraph.text = data.paragraph.text; - } - } else { - $scope.paragraph.text = data.paragraph.text; - $scope.originalText = angular.copy(data.paragraph.text); + /** + * @returns {boolean} true if updated is needed + */ + function isUpdateRequired(oldPara, newPara) { + return (newPara.id === oldPara.id && + (newPara.dateCreated !== oldPara.dateCreated || + newPara.dateFinished !== oldPara.dateFinished || + newPara.dateStarted !== oldPara.dateStarted || + newPara.dateUpdated !== oldPara.dateUpdated || + newPara.status !== oldPara.status || + newPara.jobName !== oldPara.jobName || + newPara.title !== oldPara.title || + isEmpty(newPara.results) !== isEmpty(oldPara.results) || + newPara.errorMessage !== oldPara.errorMessage || + !angular.equals(newPara.settings, oldPara.settings) || + !angular.equals(newPara.config, oldPara.config))) + } + + $scope.updateAllScopeTexts = function(oldPara, newPara) { + if (oldPara.text !== newPara.text) { + if ($scope.dirtyText) { // check if editor has local update + if ($scope.dirtyText === newPara.text) { // when local update is the same from remote, clear local update + $scope.paragraph.text = newPara.text; + $scope.dirtyText = undefined; + $scope.originalText = angular.copy(newPara.text); + + } else { // if there're local update, keep it. + $scope.paragraph.text = newPara.text; } + } else { + $scope.paragraph.text = newPara.text; + $scope.originalText = angular.copy(newPara.text); } + } + }; - /** broadcast update to result controller **/ - if (data.paragraph.results && data.paragraph.results.msg) { - for (var i in data.paragraph.results.msg) { - var newResult = data.paragraph.results.msg ? data.paragraph.results.msg[i] : {}; - var oldResult = ($scope.paragraph.results && $scope.paragraph.results.msg) ? - $scope.paragraph.results.msg[i] : {}; - var newConfig = data.paragraph.config.results ? data.paragraph.config.results[i] : {}; - var oldConfig = $scope.paragraph.config.results ? $scope.paragraph.config.results[i] : {}; - if (!angular.equals(newResult, oldResult) || - !angular.equals(newConfig, oldConfig)) { - $rootScope.$broadcast('updateResult', newResult, newConfig, data.paragraph, parseInt(i)); - } - } - } + $scope.updateParagraphObjectWhenUpdated = function(newPara) { + // resize col width + if ($scope.paragraph.config.colWidth !== newPara.colWidth) { + $rootScope.$broadcast('paragraphResized', $scope.paragraph.id); + } - // resize col width - if ($scope.paragraph.config.colWidth !== data.paragraph.colWidth) { - $rootScope.$broadcast('paragraphResized', $scope.paragraph.id); - } + /** push the rest */ + $scope.paragraph.aborted = newPara.aborted; + $scope.paragraph.user = newPara.user; + $scope.paragraph.dateUpdated = newPara.dateUpdated; + $scope.paragraph.dateCreated = newPara.dateCreated; + $scope.paragraph.dateFinished = newPara.dateFinished; + $scope.paragraph.dateStarted = newPara.dateStarted; + $scope.paragraph.errorMessage = newPara.errorMessage; + $scope.paragraph.jobName = newPara.jobName; + $scope.paragraph.title = newPara.title; + $scope.paragraph.lineNumbers = newPara.lineNumbers; + $scope.paragraph.status = newPara.status; + if (newPara.status !== 'RUNNING') { + $scope.paragraph.results = newPara.results; + } + $scope.paragraph.settings = newPara.settings; + if ($scope.editor) { + $scope.editor.setReadOnly($scope.isRunning(newPara)); + } - /** push the rest */ - $scope.paragraph.aborted = data.paragraph.aborted; - $scope.paragraph.user = data.paragraph.user; - $scope.paragraph.dateUpdated = data.paragraph.dateUpdated; - $scope.paragraph.dateCreated = data.paragraph.dateCreated; - $scope.paragraph.dateFinished = data.paragraph.dateFinished; - $scope.paragraph.dateStarted = data.paragraph.dateStarted; - $scope.paragraph.errorMessage = data.paragraph.errorMessage; - $scope.paragraph.jobName = data.paragraph.jobName; - $scope.paragraph.title = data.paragraph.title; - $scope.paragraph.lineNumbers = data.paragraph.lineNumbers; - $scope.paragraph.status = data.paragraph.status; - if (data.paragraph.status !== 'RUNNING') { - $scope.paragraph.results = data.paragraph.results; - } - $scope.paragraph.settings = data.paragraph.settings; - if ($scope.editor) { - $scope.editor.setReadOnly($scope.isRunning(data.paragraph)); - } + if (!$scope.asIframe) { + $scope.paragraph.config = newPara.config; + initializeDefault(newPara.config); + } else { + newPara.config.editorHide = true; + newPara.config.tableHide = false; + $scope.paragraph.config = newPara.config; + } + }; - if (!$scope.asIframe) { - $scope.paragraph.config = data.paragraph.config; - initializeDefault(data.paragraph.config); - } else { - data.paragraph.config.editorHide = true; - data.paragraph.config.tableHide = false; - $scope.paragraph.config = data.paragraph.config; + $scope.updateParagraph = function(oldPara, newPara, updateCallback) { + // 1. get status, refreshed + const statusChanged = (newPara.status !== oldPara.status); + const resultRefreshed = (newPara.dateFinished !== oldPara.dateFinished) || + isEmpty(newPara.results) !== isEmpty(oldPara.results) || + newPara.status === 'ERROR' || (newPara.status === 'FINISHED' && statusChanged); + + // 2. update texts managed by $scope + $scope.updateAllScopeTexts(oldPara, newPara); + + // 3. execute callback to update result + updateCallback(); + + // 4. update remaining paragraph objects + $scope.updateParagraphObjectWhenUpdated(newPara); + + // 5. handle scroll down by key properly if new paragraph is added + if (statusChanged || resultRefreshed) { + // when last paragraph runs, zeppelin automatically appends new paragraph. + // this broadcast will focus to the newly inserted paragraph + const paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); + if (paragraphs.length >= 2 && paragraphs[paragraphs.length - 2].id.indexOf($scope.paragraph.id) === 0) { + // rendering output can took some time. So delay scrolling event firing for sometime. + setTimeout(() => { $rootScope.$broadcast('scrollToCursor'); }, 500); } + } + }; + + $scope.$on('runParagraphUsingSpell', function(event, data) { + const oldPara = $scope.paragraph; + let newPara = data.paragraph; + const updateCallback = () => { + $scope.runParagraph(newPara.text, true, true); + }; + + if (!isUpdateRequired(oldPara, newPara)) { + return; + } + + $scope.updateParagraph(oldPara, newPara, updateCallback) + }); - if (statusChanged || resultRefreshed) { - // when last paragraph runs, zeppelin automatically appends new paragraph. - // this broadcast will focus to the newly inserted paragraph - var paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); - if (paragraphs.length >= 2 && paragraphs[paragraphs.length - 2].id.indexOf($scope.paragraph.id) === 0) { - // rendering output can took some time. So delay scrolling event firing for sometime. - setTimeout(function() { - $rootScope.$broadcast('scrollToCursor'); - }, 500); + $scope.$on('updateParagraph', function(event, data) { + const oldPara = $scope.paragraph; + const newPara = data.paragraph; + + if (!isUpdateRequired(oldPara, newPara)) { + return; + } + + const updateCallback = () => { + // broadcast `updateResult` message to trigger result update + if (newPara.results && newPara.results.msg) { + for (let i in newPara.results.msg) { + const newResult = newPara.results.msg ? newPara.results.msg[i] : {}; + const oldResult = (oldPara.results && oldPara.results.msg) ? + oldPara.results.msg[i] : {}; + const newConfig = newPara.config.results ? newPara.config.results[i] : {}; + const oldConfig = oldPara.config.results ? oldPara.config.results[i] : {}; + if (!angular.equals(newResult, oldResult) || + !angular.equals(newConfig, oldConfig)) { + $rootScope.$broadcast('updateResult', newResult, newConfig, newPara, parseInt(i)); + } } } - } + }; + + $scope.updateParagraph(oldPara, newPara, updateCallback) }); $scope.$on('updateProgress', function(event, data) { @@ -1092,7 +1231,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat // move focus to next paragraph $scope.$emit('moveFocusToNextParagraph', paragraphId); } else if (keyEvent.shiftKey && keyCode === 13) { // Shift + Enter - $scope.run($scope.paragraph, $scope.getEditorValue()); + $scope.runParagraphFromShortcut($scope.getEditorValue()); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c $scope.cancelParagraph($scope.paragraph); } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/paragraph.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.html b/zeppelin-web/src/app/notebook/paragraph/paragraph.html index 95ad9eb..0de5e64 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.html @@ -58,9 +58,7 @@ limitations under the License. ng-init="init(result, paragraph.config.results[$index], paragraph, $index)" ng-include src="'app/notebook/paragraph/result/result.html'"> </div> - <div id="{{paragraph.id}}_error" - class="error text" - ng-if="paragraph.status == 'ERROR'" + <div id="{{paragraph.id}}_error" class="error text" ng-bind="paragraph.errorMessage"> </div> </div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js ---------------------------------------------------------------------- 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 40f8248..5757e1a 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -19,6 +19,10 @@ import PiechartVisualization from '../../../visualization/builtins/visualization import AreachartVisualization from '../../../visualization/builtins/visualization-areachart'; import LinechartVisualization from '../../../visualization/builtins/visualization-linechart'; import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart'; +import { + DefaultDisplayType, + SpellResult, +} from '../../../spell' angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl); @@ -150,13 +154,13 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // image data $scope.imageData; - $scope.init = function(result, config, paragraph, index) { - console.log('result controller init %o %o %o', result, config, index); + // queue for append output + const textResultQueueForAppend = []; + $scope.init = function(result, config, paragraph, index) { // register helium plugin vis - var heliumVis = heliumService.get(); - console.log('Helium visualizations %o', heliumVis); - heliumVis.forEach(function(vis) { + var visBundles = heliumService.getVisualizationBundles(); + visBundles.forEach(function(vis) { $scope.builtInTableDataVisualizationList.push({ id: vis.id, name: vis.name, @@ -171,11 +175,30 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location 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; } - console.log('updateResult %o %o %o %o', result, newConfig, paragraphRef, index); + var refresh = !angular.equals(newConfig, $scope.config) || !angular.equals(result.type, $scope.type) || !angular.equals(result.data, data); @@ -196,14 +219,10 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location if (paragraph.id === data.paragraphId && resultIndex === data.index && (paragraph.status === 'RUNNING' || paragraph.status === 'PENDING')) { - appendTextOutput(data.data); - } - }); - $scope.$on('updateParagraphOutput', function(event, data) { - if (paragraph.id === data.paragraphId && - resultIndex === data.index) { - clearTextOutput(); + if (DefaultDisplayType.TEXT !== $scope.type) { + $scope.type = DefaultDisplayType.TEXT; + } appendTextOutput(data.data); } }); @@ -250,8 +269,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(); @@ -259,228 +310,291 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } if (activeApp) { - var app = _.find($scope.apps, {id: activeApp}); - renderApp(app); + const appState = _.find($scope.apps, {id: activeApp}); + renderApp(`p${appState.id}`, appState); } else { - if (type === 'TABLE') { - $scope.renderGraph($scope.graphMode, refresh); - } else if (type === 'HTML') { - renderHtml(); - } else if (type === 'ANGULAR') { - renderAngular(); - } else if (type === 'TEXT') { - renderText(); + if (!DefaultDisplayType[type]) { + const spell = heliumService.getSpellByMagic(type); + if (!spell) { + console.error(`Can't execute spell due to unknown display type: ${type}`); + return; + } + $scope.renderCustomDisplay(type, data, spell); + } else { + const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type); + $scope.renderDefaultDisplay(targetElemId, type, data, refresh); } } }; - var renderHtml = function() { - var retryRenderer = function() { - var htmlEl = angular.element('#p' + $scope.id + '_html'); - if (htmlEl.length) { - try { - htmlEl.html(data); + $scope.isDefaultDisplay = function() { + return DefaultDisplayType[$scope.type]; + }; - htmlEl.find('pre code').each(function(i, e) { - hljs.highlightBlock(e); - }); - /*eslint new-cap: [2, {"capIsNewExceptions": ["MathJax.Hub.Queue"]}]*/ - MathJax.Hub.Queue(['Typeset', MathJax.Hub, htmlEl[0]]); - } catch (err) { - console.log('HTML rendering error %o', err); + /** + * Render multiple sub results for custom display + */ + $scope.renderCustomDisplay = function(type, data, spell) { + // get result from intp + + const spellResult = spell.interpret(data.trim()); + const parsed = spellResult.getAllParsedDataWithTypes( + heliumService.getAllSpells()); + + // custom display result can include multiple subset results + parsed.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); } - } else { - $timeout(retryRenderer, 10); + }; + + 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 afterLoaded = () => { + 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}`); } + ); }; - $timeout(retryRenderer); + + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; - var renderAngular = function() { - var retryRenderer = function() { - if (angular.element('#p' + $scope.id + '_angular').length) { - try { - angular.element('#p' + $scope.id + '_angular').html(data); + const renderHtml = function(targetElemId, data) { + const afterLoaded = () => { + 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}`); } + ); + }; - var paragraphScope = noteVarShareService.get(paragraph.id + '_paragraphScope'); - $compile(angular.element('#p' + $scope.id + '_angular').contents())(paragraphScope); - } catch (err) { - console.log('ANGULAR rendering error %o', err); - } - } else { - $timeout(retryRenderer, 10); - } + retryUntilElemIsLoaded(targetElemId, afterLoaded); + }; + + const renderAngular = function(targetElemId, data) { + const afterLoaded = () => { + 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}`); } + ); }; - $timeout(retryRenderer); + + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; - var getTextEl = function (paragraphId) { - return angular.element('#p' + $scope.id + '_text'); - } + const getTextResultElemId = function (resultId) { + return `p${resultId}_text`; + }; - var textRendererInitialized = false; - var renderText = function() { - var retryRenderer = function() { - var textEl = getTextEl($scope.id); - if (textEl.length) { - // clear all lines before render - clearTextOutput(); - textRendererInitialized = true; - - if (data) { - appendTextOutput(data); - } else { - flushAppendQueue(); - } + const renderText = function(targetElemId, data) { + const afterLoaded = () => { + const elem = angular.element(`#${targetElemId}`); + handleData(data, DefaultDisplayType.TEXT, + (generated) => { + // clear all lines before render + removeChildrenDOM(targetElemId); + + if (generated) { + const divDOM = angular.element('<div></div>').text(generated); + elem.append(divDOM); + } - getTextEl($scope.id).bind('mousewheel', function(e) { - $scope.keepScrollDown = false; - }); - } else { - $timeout(retryRenderer, 10); - } + elem.bind('mousewheel', (e) => { $scope.keepScrollDown = false; }); + }, + (error) => { elem.html(`${error.stack}`); } + ); }; - $timeout(retryRenderer); + + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; - var clearTextOutput = function() { - var textEl = getTextEl($scope.id); - if (textEl.length) { - textEl.children().remove(); + const removeChildrenDOM = function(targetElemId) { + const elem = angular.element(`#${targetElemId}`); + if (elem.length) { + elem.children().remove(); } }; - var textAppendQueueBeforeInitialize = []; + function appendTextOutput(data) { + const elemId = getTextResultElemId($scope.id); + textResultQueueForAppend.push(data); - var flushAppendQueue = function() { - while (textAppendQueueBeforeInitialize.length > 0) { - appendTextOutput(textAppendQueueBeforeInitialize.pop()); + // if DOM is not loaded, just push data and return + if (!isDOMLoaded(elemId)) { + return; } - }; - var appendTextOutput = function(msg) { - if (!textRendererInitialized) { - textAppendQueueBeforeInitialize.push(msg); - } else { - flushAppendQueue(); - var textEl = getTextEl($scope.id); - if (textEl.length) { - var lines = msg.split('\n'); - for (var i = 0; i < lines.length; i++) { - textEl.append(angular.element('<div></div>').text(lines[i])); - } + const elem = angular.element(`#${elemId}`); + + // pop all stacked data and append to the DOM + while (textResultQueueForAppend.length > 0) { + const stacked = textResultQueueForAppend.pop(); + + const lines = stacked.split('\n'); + for (let i = 0; i < lines.length; i++) { + elem.append(angular.element('<div></div>').text(lines[i])); } + if ($scope.keepScrollDown) { - var doc = getTextEl($scope.id); + const doc = angular.element(`#${elemId}`); doc[0].scrollTop = doc[0].scrollHeight; } } - }; + } - $scope.renderGraph = function(type, refresh) { + $scope.renderGraph = function(graphElemId, graphMode, refresh) { // set graph height - var height = $scope.config.graph.height; - var graphContainerEl = angular.element('#p' + $scope.id + '_graph'); - graphContainerEl.height(height); + const height = $scope.config.graph.height; + const graphElem = angular.element(`#${graphElemId}`); + graphElem.height(height); - if (!type) { - type = 'table'; - } + if (!graphMode) { graphMode = 'table'; } + const tableElemId = `p${$scope.id}_${graphMode}`; - var builtInViz = builtInVisualizations[type]; - if (builtInViz) { - // deactive previsouly active visualization - for (var t in builtInVisualizations) { - var v = builtInVisualizations[t].instance; + const builtInViz = builtInVisualizations[graphMode]; + if (!builtInViz) { return; } - if (t !== type && v && v.isActive()) { - v.deactivate(); - break; - } - } + // deactive previsouly active visualization + for (let t in builtInVisualizations) { + const v = builtInVisualizations[t].instance; - if (!builtInViz.instance) { // not instantiated yet - // render when targetEl is available - var retryRenderer = function() { - var targetEl = angular.element('#p' + $scope.id + '_' + type); - var transformationSettingTargetEl = angular.element('#trsetting' + $scope.id + '_' + type); - var visualizationSettingTargetEl = angular.element('#vizsetting' + $scope.id + '_' + type); - if (targetEl.length) { - try { - // set height - targetEl.height(height); - - // instantiate visualization - var config = getVizConfig(type); - var Visualization = builtInViz.class; - builtInViz.instance = new Visualization(targetEl, config); - - // inject emitter, $templateRequest - var emitter = function(graphSetting) { - commitVizConfigChange(graphSetting, type); - }; - builtInViz.instance._emitter = emitter; - builtInViz.instance._compile = $compile; - builtInViz.instance._createNewScope = createNewScope; - var transformation = builtInViz.instance.getTransformation(); - transformation._emitter = emitter; - transformation._templateRequest = $templateRequest; - transformation._compile = $compile; - transformation._createNewScope = createNewScope; - - // render - var transformed = transformation.transform(tableData); - transformation.renderSetting(transformationSettingTargetEl); - builtInViz.instance.render(transformed); - builtInViz.instance.renderSetting(visualizationSettingTargetEl); - builtInViz.instance.activate(); - angular.element(window).resize(function() { - builtInViz.instance.resize(); - }); - } catch (err) { - console.error('Graph drawing error %o', err); - } - } else { - $timeout(retryRenderer, 10); - } - }; - $timeout(retryRenderer); - } else if (refresh) { - console.log('Refresh data %o', tableData); - // when graph options or data are changed - var retryRenderer = function() { - var targetEl = angular.element('#p' + $scope.id + '_' + type); - var transformationSettingTargetEl = angular.element('#trsetting' + $scope.id + '_' + type); - var visualizationSettingTargetEl = angular.element('#trsetting' + $scope.id + '_' + type); - if (targetEl.length) { - var config = getVizConfig(type); - targetEl.height(height); - var transformation = builtInViz.instance.getTransformation(); - transformation.setConfig(config); - var transformed = transformation.transform(tableData); - transformation.renderSetting(transformationSettingTargetEl); - builtInViz.instance.setConfig(config); - builtInViz.instance.render(transformed); - builtInViz.instance.renderSetting(visualizationSettingTargetEl); - } else { - $timeout(retryRenderer, 10); - } - }; - $timeout(retryRenderer); - } else { - var retryRenderer = function() { - var targetEl = angular.element('#p' + $scope.id + '_' + type); - if (targetEl.length) { - targetEl.height(height); - builtInViz.instance.activate(); - } else { - $timeout(retryRenderer, 10); - } - }; - $timeout(retryRenderer); + if (t !== graphMode && v && v.isActive()) { + v.deactivate(); + break; } } + + if (!builtInViz.instance) { // not instantiated yet + // render when targetEl is available + const afterLoaded = (loadedElem) => { + try { + const transformationSettingTargetEl = angular.element('#trsetting' + $scope.id + '_' + graphMode); + const visualizationSettingTargetEl = angular.element('#trsetting' + $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; + builtInViz.instance._createNewScope = createNewScope; + 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); + } + }; + + retryUntilElemIsLoaded(tableElemId, afterLoaded); + } else if (refresh) { + // when graph options or data are changed + console.log('Refresh data %o', tableData); + + const afterLoaded = (loadedElem) => { + const transformationSettingTargetEl = angular.element('#trsetting' + $scope.id + '_' + graphMode); + const visualizationSettingTargetEl = angular.element('#trsetting' + $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); + }; + + retryUntilElemIsLoaded(tableElemId, afterLoaded); + } else { + const afterLoaded = (loadedElem) => { + loadedElem.height(height); + builtInViz.instance.activate(); + }; + + retryUntilElemIsLoaded(tableElemId, afterLoaded); + } }; + $scope.switchViz = function(newMode) { var newConfig = angular.copy($scope.config); var newParams = angular.copy(paragraph.settings.params); @@ -728,23 +842,17 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }); }; - var renderApp = function(appState) { - var retryRenderer = function() { - var targetEl = angular.element(document.getElementById('p' + appState.id)); - console.log('retry renderApp %o', targetEl); - if (targetEl.length) { - try { - console.log('renderApp %o', appState); - targetEl.html(appState.output); - $compile(targetEl.contents())(getAppScope(appState)); - } catch (err) { - console.log('App rendering error %o', err); - } - } else { - $timeout(retryRenderer, 1000); + 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); } }; - $timeout(retryRenderer); + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; /* @@ -927,4 +1035,4 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } } }); -}; +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/notebook/paragraph/result/result.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index df09c4d..5b251e5 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -37,8 +37,7 @@ limitations under the License. <!-- graph --> <div id="p{{id}}_graph" class="graphContainer" - ng-class="{'noOverflow': graphMode=='table'}" - > + ng-class="{'noOverflow': graphMode=='table'}"> <div ng-repeat="viz in builtInTableDataVisualizationList track by $index" id="p{{id}}_{{viz.id}}" ng-show="graphMode == viz.id"> @@ -67,13 +66,19 @@ limitations under the License. tooltip="Scroll Top"></div> </div> - <div id="p{{id}}_html" - class="resultContained" + <div id="p{{id}}_custom" class="resultContained" + ng-if="!isDefaultDisplay()"> + </div> + + <div id="p{{id}}_elem" class="resultContained" + ng-if="type == 'ELEMENT'"> + </div> + + <div id="p{{id}}_html" class="resultContained" ng-if="type == 'HTML'"> </div> - <div id="p{{id}}_angular" - class="resultContained" + <div id="p{{id}}_angular" class="resultContained" ng-if="type == 'ANGULAR'"> </div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/spell/.npmignore ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/.npmignore b/zeppelin-web/src/app/spell/.npmignore new file mode 100644 index 0000000..0b84df0 --- /dev/null +++ b/zeppelin-web/src/app/spell/.npmignore @@ -0,0 +1 @@ +*.html \ No newline at end of file http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/spell/index.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/index.js b/zeppelin-web/src/app/spell/index.js new file mode 100644 index 0000000..8ec4753 --- /dev/null +++ b/zeppelin-web/src/app/spell/index.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export { + DefaultDisplayType, + SpellResult, +} from './spell-result'; + +export { + SpellBase, +} from './spell-base'; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/spell/package.json ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/package.json b/zeppelin-web/src/app/spell/package.json new file mode 100644 index 0000000..7003e06 --- /dev/null +++ b/zeppelin-web/src/app/spell/package.json @@ -0,0 +1,13 @@ +{ + "name": "zeppelin-spell", + "description": "Zeppelin Spell Framework", + "version": "0.8.0-SNAPSHOT", + "main": "index", + "dependencies": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/apache/zeppelin" + }, + "license": "Apache-2.0" +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/spell/spell-base.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/spell-base.js b/zeppelin-web/src/app/spell/spell-base.js new file mode 100644 index 0000000..85c85e5 --- /dev/null +++ b/zeppelin-web/src/app/spell/spell-base.js @@ -0,0 +1,48 @@ +/* + * 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. + */ + +/*eslint-disable no-unused-vars */ +import { + DefaultDisplayType, + SpellResult, +} from './spell-result'; +/*eslint-enable no-unused-vars */ + +export class SpellBase { + constructor(magic) { + this.magic = magic; + } + + /** + * Consumes text and return `SpellResult`. + * + * @param paragraphText {string} which doesn't include magic + * @return {SpellResult} + */ + interpret(paragraphText) { + throw new Error('SpellBase.interpret() should be overrided'); + } + + /** + * return magic for this spell. + * (e.g `%flowchart`) + * @return {string} + */ + getMagic() { + return this.magic; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/app/spell/spell-result.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js new file mode 100644 index 0000000..d62e97a --- /dev/null +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -0,0 +1,275 @@ +/* + * 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. + */ + +export const DefaultDisplayType = { + ELEMENT: 'ELEMENT', + TABLE: 'TABLE', + HTML: 'HTML', + ANGULAR: 'ANGULAR', + TEXT: 'TEXT', +}; + +export const DefaultDisplayMagic = { + '%element': DefaultDisplayType.ELEMENT, + '%table': DefaultDisplayType.TABLE, + '%html': DefaultDisplayType.HTML, + '%angular': DefaultDisplayType.ANGULAR, + '%text': DefaultDisplayType.TEXT, +}; + +export class DataWithType { + constructor(data, type, magic, text) { + this.data = data; + this.type = type; + + /** + * keep for `DefaultDisplayType.ELEMENT` (function data type) + * to propagate a result to other client. + * + * otherwise we will send function as `data` and it will not work + * since they don't have context where they are created. + */ + + this.magic = magic; + this.text = text; + } + + static handleDefaultMagic(m) { + // let's use default display type instead of magic in case of default + // to keep consistency with backend interpreter + if (DefaultDisplayMagic[m]) { + return DefaultDisplayMagic[m]; + } else { + return m; + } + } + + static createPropagable(dataWithType) { + if (!SpellResult.isFunction(dataWithType.data)) { + return dataWithType; + } + + const data = dataWithType.getText(); + const type = dataWithType.getMagic(); + + return new DataWithType(data, type); + } + + /** + * consume 1 data and produce multiple + * @param data {string} + * @param customDisplayType + * @return {Array<DataWithType>} + */ + static parseStringData(data, customDisplayMagic) { + function availableMagic(magic) { + return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); + } + + const splited = data.split('\n'); + + const gensWithTypes = []; + let mergedGens = []; + let previousMagic = DefaultDisplayType.TEXT; + + // create `DataWithType` whenever see available display type. + for(let i = 0; i < splited.length; i++) { + const g = splited[i]; + const magic = SpellResult.extractMagic(g); + + // create `DataWithType` only if see new magic + if (availableMagic(magic) && mergedGens.length > 0) { + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); + mergedGens = []; + } + + // accumulate `data` to mergedGens + if (availableMagic(magic)) { + const withoutMagic = g.split(magic)[1]; + mergedGens.push(`${withoutMagic}\n`); + previousMagic = DataWithType.handleDefaultMagic(magic); + } else { + mergedGens.push(`${g}\n`); + } + } + + // cleanup the last `DataWithType` + if (mergedGens.length > 0) { + previousMagic = DataWithType.handleDefaultMagic(previousMagic); + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); + } + + return gensWithTypes; + } + + /** + * get 1 `DataWithType` and produce multiples using available displays + * return an wrapped with a promise to generalize result output which can be + * object, function or promise + * @param dataWithType {DataWithType} + * @param availableDisplays {Object} Map for available displays + * @param magic + * @param textWithoutMagic + * @return {Promise<Array<DataWithType>>} + */ + static produceMultipleData(dataWithType, customDisplayType, + magic, textWithoutMagic) { + const data = dataWithType.getData(); + const type = dataWithType.getType(); + + // if the type is specified, just return it + // handle non-specified dataWithTypes only + if (type) { + return new Promise((resolve) => { resolve([dataWithType]); }); + } + + let wrapped; + + if (SpellResult.isFunction(data)) { + // if data is a function, we consider it as ELEMENT type. + wrapped = new Promise((resolve) => { + const dt = new DataWithType( + data, DefaultDisplayType.ELEMENT, magic, textWithoutMagic); + const result = [dt]; + return resolve(result); + }); + } else if (SpellResult.isPromise(data)) { + // if data is a promise, + wrapped = data.then(generated => { + const result = + DataWithType.parseStringData(generated, customDisplayType); + return result; + }) + + } else { + // if data is a object, parse it to multiples + wrapped = new Promise((resolve) => { + const result = + DataWithType.parseStringData(data, customDisplayType); + return resolve(result); + }); + } + + return wrapped; + } + + /** + * `data` can be promise, function or just object + * - if data is an object, it will be used directly. + * - if data is a function, it will be called with DOM element id + * where the final output is rendered. + * - if data is a promise, the post processing logic + * will be called in `then()` of this promise. + * @returns {*} `data` which can be object, function or promise. + */ + getData() { + return this.data; + } + + /** + * Value of `type` might be empty which means + * data can be separated into multiples + * by `SpellResult.parseStringData()` + * @returns {string} + */ + getType() { + return this.type; + } + + getMagic() { + return this.magic; + } + + getText() { + return this.text; + } +} + +export class SpellResult { + constructor(resultData, resultType) { + this.dataWithTypes = []; + this.add(resultData, resultType); + } + + static isFunction(data) { + return (data && typeof data === 'function'); + } + + static isPromise(data) { + return (data && typeof data.then === 'function'); + } + + static isObject(data) { + return (data && + !SpellResult.isFunction(data) && + !SpellResult.isPromise(data)); + } + + static extractMagic(allParagraphText) { + const pattern = /^\s*%(\S+)\s*/g; + try { + let match = pattern.exec(allParagraphText); + if (match) { + return `%${match[1].trim()}`; + } + } catch (error) { + // failed to parse, ignore + } + + return undefined; + } + + static createPropagable(resultMsg) { + return resultMsg.map(dt => { + return DataWithType.createPropagable(dt); + }) + } + + add(resultData, resultType) { + if (resultData) { + this.dataWithTypes.push( + new DataWithType(resultData, resultType)); + } + + return this; + } + + /** + * @param customDisplayType + * @param textWithoutMagic + * @return {Promise<Array<DataWithType>>} + */ + getAllParsedDataWithTypes(customDisplayType, magic, textWithoutMagic) { + const promises = this.dataWithTypes.map(dt => { + return DataWithType.produceMultipleData( + dt, customDisplayType, magic, textWithoutMagic); + }); + + // some promises can include an array so we need to flatten them + const flatten = Promise.all(promises).then(values => { + return values.reduce((acc, cur) => { + if (Array.isArray(cur)) { + return acc.concat(cur); + } else { + return acc.concat([cur]); + } + }) + }); + + return flatten; + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/components/helium/helium-type.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/helium/helium-type.js b/zeppelin-web/src/components/helium/helium-type.js new file mode 100644 index 0000000..0ef4eb6 --- /dev/null +++ b/zeppelin-web/src/components/helium/helium-type.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export const HeliumType = { + VISUALIZATION: 'VISUALIZATION', + SPELL: 'SPELL', +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/components/helium/helium.service.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index ae44425..a8664d3 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -12,51 +12,80 @@ * limitations under the License. */ -(function() { +import { HeliumType, } from './helium-type'; - angular.module('zeppelinWebApp').service('heliumService', heliumService); +angular.module('zeppelinWebApp').service('heliumService', heliumService); - heliumService.$inject = ['$http', 'baseUrlSrv', 'ngToast']; +heliumService.$inject = ['$http', 'baseUrlSrv', 'ngToast']; - function heliumService($http, baseUrlSrv, ngToast) { +function heliumService($http, baseUrlSrv, ngToast) { - var url = baseUrlSrv.getRestApiBase() + '/helium/visualizations/load'; - if (process.env.HELIUM_VIS_DEV) { - url = url + '?refresh=true'; + var url = baseUrlSrv.getRestApiBase() + '/helium/bundle/load'; + if (process.env.HELIUM_BUNDLE_DEV) { + url = url + '?refresh=true'; + } + // name `heliumBundles` should be same as `HelumBundleFactory.HELIUM_BUNDLES_VAR` + var heliumBundles = []; + // map for `{ magic: interpreter }` + let spellPerMagic = {}; + let visualizationBundles = []; + + // load should be promise + this.load = $http.get(url).success(function(response) { + if (response.substring(0, 'ERROR:'.length) !== 'ERROR:') { + // evaluate bundles + eval(response); + + // extract bundles by type + heliumBundles.map(b => { + if (b.type === HeliumType.SPELL) { + const spell = new b.class(); // eslint-disable-line new-cap + spellPerMagic[spell.getMagic()] = spell; + } else if (b.type === HeliumType.VISUALIZATION) { + visualizationBundles.push(b); + } + }); + } else { + console.error(response); } - var visualizations = []; - - // load should be promise - this.load = $http.get(url).success(function(response) { - if (response.substring(0, 'ERROR:'.length) !== 'ERROR:') { - eval(response); - } else { - console.error(response); - } - }); - - this.get = function() { - return visualizations; - }; - - this.getVisualizationOrder = function() { - return $http.get(baseUrlSrv.getRestApiBase() + '/helium/visualizationOrder'); - }; - - this.setVisualizationOrder = function(list) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/visualizationOrder', list); - }; - - this.getAllPackageInfo = function() { - return $http.get(baseUrlSrv.getRestApiBase() + '/helium/all'); - }; - - this.enable = function(name, artifact) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/enable/' + name, artifact); - }; - - this.disable = function(name) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/disable/' + name); - }; - }; -})(); + }); + + /** + * @param magic {string} e.g `%flowchart` + * @returns {SpellBase} undefined if magic is not registered + */ + this.getSpellByMagic = function(magic) { + return spellPerMagic[magic]; + }; + + /** + * @returns {Object} map for `{ magic : spell }` + */ + this.getAllSpells = function() { + return spellPerMagic; + }; + + this.getVisualizationBundles = function() { + return visualizationBundles; + }; + + this.getVisualizationPackageOrder = function() { + return $http.get(baseUrlSrv.getRestApiBase() + '/helium/order/visualization'); + }; + + this.setVisualizationPackageOrder = function(list) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/order/visualization', list); + }; + + this.getAllPackageInfo = function() { + return $http.get(baseUrlSrv.getRestApiBase() + '/helium/all'); + }; + + this.enable = function(name, artifact) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/enable/' + name, artifact); + }; + + this.disable = function(name) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/disable/' + name); + }; +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 5436f34..aceffbb 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -105,6 +105,8 @@ function websocketEvents($rootScope, $websocket, $location, baseUrlSrv) { } else if (op === 'PARAGRAPH') { $rootScope.$broadcast('updateParagraph', data); + } else if (op === 'RUN_PARAGRAPH_USING_SPELL') { + $rootScope.$broadcast('runParagraphUsingSpell', data); } else if (op === 'PARAGRAPH_APPEND_OUTPUT') { $rootScope.$broadcast('appendParagraphOutput', data); } else if (op === 'PARAGRAPH_UPDATE_OUTPUT') { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index d597ff4..4fd4b95 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -159,6 +159,31 @@ function websocketMsgSrv($rootScope, websocketEvents) { websocketEvents.sendNewEvent({op: 'CANCEL_PARAGRAPH', data: {id: paragraphId}}); }, + paragraphExecutedBySpell: function(paragraphId, paragraphTitle, + paragraphText, paragraphResultsMsg, + paragraphStatus, paragraphErrorMessage, + paragraphConfig, paragraphParams) { + websocketEvents.sendNewEvent({ + op: 'PARAGRAPH_EXECUTED_BY_SPELL', + data: { + id: paragraphId, + title: paragraphTitle, + paragraph: paragraphText, + results: { + code: paragraphStatus, + msg: paragraphResultsMsg.map(dataWithType => { + let serializedData = dataWithType.data; + return { type: dataWithType.type, data: serializedData, }; + }) + }, + status: paragraphStatus, + errorMessage: paragraphErrorMessage, + config: paragraphConfig, + params: paragraphParams + } + }); + }, + runParagraph: function(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { websocketEvents.sendNewEvent({ op: 'RUN_PARAGRAPH',
