You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2017/02/01 23:34:01 UTC
[2/3] zeppelin git commit: [ZEPPELIN-2008] Introduce Spell
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',