zeppelin-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From m...@apache.org
Subject [2/3] zeppelin git commit: [ZEPPELIN-2008] Introduce Spell
Date Wed, 01 Feb 2017 23:34:01 GMT
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',


Mime
View raw message