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:00 UTC

[1/3] zeppelin git commit: [ZEPPELIN-2008] Introduce Spell

Repository: zeppelin
Updated Branches:
  refs/heads/master 019df1f6b -> 0589e27e7


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/test/spec/controllers/paragraph.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/test/spec/controllers/paragraph.js b/zeppelin-web/test/spec/controllers/paragraph.js
index 4089d2d..1aa4a4a 100644
--- a/zeppelin-web/test/spec/controllers/paragraph.js
+++ b/zeppelin-web/test/spec/controllers/paragraph.js
@@ -41,10 +41,6 @@ describe('Controller: ParagraphCtrl', function() {
     });
   });
 
-  it('should return "TEXT" by default when getResultType() is called with no parameter', function() {
-    expect(scope.getResultType()).toEqual('TEXT');
-  });
-
   it('should have this array of values for "colWidthOption"', function() {
     expect(scope.colWidthOption).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
   });

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-web/webpack.config.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/webpack.config.js b/zeppelin-web/webpack.config.js
index d3a2681..8198b55 100644
--- a/zeppelin-web/webpack.config.js
+++ b/zeppelin-web/webpack.config.js
@@ -210,7 +210,7 @@ module.exports = function makeWebpackConfig () {
       // Reference: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
       new webpack.DefinePlugin({
         'process.env': {
-          HELIUM_VIS_DEV: process.env.HELIUM_VIS_DEV
+          HELIUM_BUNDLE_DEV: process.env.HELIUM_BUNDLE_DEV
         }
       })
     )

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java
index 0ef3237..e2e1b49 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/Helium.java
@@ -45,20 +45,20 @@ public class Helium {
   private final File registryCacheDir;
 
   private final Gson gson;
-  private final HeliumVisualizationFactory visualizationFactory;
+  private final HeliumBundleFactory bundleFactory;
   private final HeliumApplicationFactory applicationFactory;
 
   public Helium(
       String heliumConfPath,
       String registryPaths,
       File registryCacheDir,
-      HeliumVisualizationFactory visualizationFactory,
+      HeliumBundleFactory bundleFactory,
       HeliumApplicationFactory applicationFactory)
       throws IOException {
     this.heliumConfPath = heliumConfPath;
     this.registryPaths = registryPaths;
     this.registryCacheDir = registryCacheDir;
-    this.visualizationFactory = visualizationFactory;
+    this.bundleFactory = bundleFactory;
     this.applicationFactory = applicationFactory;
 
     GsonBuilder builder = new GsonBuilder();
@@ -95,8 +95,8 @@ public class Helium {
     return applicationFactory;
   }
 
-  public HeliumVisualizationFactory getVisualizationFactory() {
-    return visualizationFactory;
+  public HeliumBundleFactory getBundleFactory() {
+    return bundleFactory;
   }
 
   private synchronized HeliumConf loadConf(String path) throws IOException {
@@ -145,14 +145,14 @@ public class Helium {
     Map<String, List<HeliumPackageSearchResult>> all = getAllPackageInfo();
 
     // clear visualization display order
-    List<String> packageOrder = heliumConf.getVisualizationDisplayOrder();
+    List<String> packageOrder = heliumConf.getBundleDisplayOrder();
     List<String> clearedOrder = new LinkedList<>();
     for (String pkgName : packageOrder) {
       if (all.containsKey(pkgName)) {
         clearedOrder.add(pkgName);
       }
     }
-    heliumConf.setVisualizationDisplayOrder(clearedOrder);
+    heliumConf.setBundleDisplayOrder(clearedOrder);
 
     // clear enabled package
     Map<String, String> enabledPackages = heliumConf.getEnabledPackages();
@@ -215,8 +215,8 @@ public class Helium {
     return null;
   }
 
-  public File recreateVisualizationBundle() throws IOException {
-    return visualizationFactory.bundle(getVisualizationPackagesToBundle(), true);
+  public File recreateBundle() throws IOException {
+    return bundleFactory.buildBundle(getBundlePackagesToBundle(), true);
   }
 
   public void enable(String name, String artifact) throws IOException {
@@ -231,8 +231,8 @@ public class Helium {
     heliumConf.enablePackage(name, artifact);
 
     // if package is visualization, rebuild bundle
-    if (pkgInfo.getPkg().getType() == HeliumPackage.Type.VISUALIZATION) {
-      visualizationFactory.bundle(getVisualizationPackagesToBundle());
+    if (HeliumPackage.isBundleType(pkgInfo.getPkg().getType())) {
+      bundleFactory.buildBundle(getBundlePackagesToBundle());
     }
 
     save();
@@ -247,9 +247,9 @@ public class Helium {
 
     heliumConf.disablePackage(name);
 
-    HeliumPackageSearchResult pkg = getPackageInfo(name, artifact);
-    if (pkg == null || pkg.getPkg().getType() == HeliumPackage.Type.VISUALIZATION) {
-      visualizationFactory.bundle(getVisualizationPackagesToBundle());
+    HeliumPackageSearchResult pkgInfo = getPackageInfo(name, artifact);
+    if (pkgInfo == null || HeliumPackage.isBundleType(pkgInfo.getPkg().getType())) {
+      bundleFactory.buildBundle(getBundlePackagesToBundle());
     }
 
     save();
@@ -278,7 +278,7 @@ public class Helium {
 
     for (List<HeliumPackageSearchResult> pkgs : getAllPackageInfo().values()) {
       for (HeliumPackageSearchResult pkg : pkgs) {
-        if (pkg.getPkg().getType() == HeliumPackage.Type.APPLICATION && pkg.isEnabled()) {
+        if (pkg.getPkg().getType() == HeliumType.APPLICATION && pkg.isEnabled()) {
           ResourceSet resources = ApplicationLoader.findRequiredResourceSet(
               pkg.getPkg().getResources(),
               paragraph.getNote().getId(),
@@ -299,15 +299,15 @@ public class Helium {
   }
 
   /**
-   * Get enabled visualization packages
+   * Get enabled buildBundle packages
    *
-   * @return ordered list of enabled visualization package
+   * @return ordered list of enabled buildBundle package
    */
-  public List<HeliumPackage> getVisualizationPackagesToBundle() {
+  public List<HeliumPackage> getBundlePackagesToBundle() {
     Map<String, List<HeliumPackageSearchResult>> allPackages = getAllPackageInfo();
-    List<String> visOrder = heliumConf.getVisualizationDisplayOrder();
+    List<String> visOrder = heliumConf.getBundleDisplayOrder();
 
-    List<HeliumPackage> orderedVisualizationPackages = new LinkedList<>();
+    List<HeliumPackage> orderedBundlePackages = new LinkedList<>();
 
     // add enabled packages in visOrder
     for (String name : visOrder) {
@@ -316,8 +316,8 @@ public class Helium {
         continue;
       }
       for (HeliumPackageSearchResult pkgInfo : versions) {
-        if (pkgInfo.getPkg().getType() == HeliumPackage.Type.VISUALIZATION && pkgInfo.isEnabled()) {
-          orderedVisualizationPackages.add(pkgInfo.getPkg());
+        if (canBundle(pkgInfo)) {
+          orderedBundlePackages.add(pkgInfo.getPkg());
           allPackages.remove(name);
           break;
         }
@@ -325,28 +325,35 @@ public class Helium {
     }
 
     // add enabled packages not in visOrder
-    for (List<HeliumPackageSearchResult> pkgs : allPackages.values()) {
-      for (HeliumPackageSearchResult pkg : pkgs) {
-        if (pkg.getPkg().getType() == HeliumPackage.Type.VISUALIZATION && pkg.isEnabled()) {
-          orderedVisualizationPackages.add(pkg.getPkg());
+    for (List<HeliumPackageSearchResult> pkgInfos : allPackages.values()) {
+      for (HeliumPackageSearchResult pkgInfo : pkgInfos) {
+        if (canBundle(pkgInfo)) {
+          orderedBundlePackages.add(pkgInfo.getPkg());
           break;
         }
       }
     }
 
-    return orderedVisualizationPackages;
+    return orderedBundlePackages;
+  }
+
+  public boolean canBundle(HeliumPackageSearchResult pkgInfo) {
+    return (pkgInfo.isEnabled() &&
+        HeliumPackage.isBundleType(pkgInfo.getPkg().getType()));
   }
 
   /**
    * Get enabled package list in order
    * @return
    */
-  public List<String> getVisualizationPackageOrder() {
+  public List<String> setVisualizationPackageOrder() {
     List orderedPackageList = new LinkedList<>();
-    List<HeliumPackage> packages = getVisualizationPackagesToBundle();
+    List<HeliumPackage> packages = getBundlePackagesToBundle();
 
     for (HeliumPackage pkg : packages) {
-      orderedPackageList.add(pkg.getName());
+      if (HeliumType.VISUALIZATION == pkg.getType()) {
+        orderedPackageList.add(pkg.getName());
+      }
     }
 
     return orderedPackageList;
@@ -354,10 +361,10 @@ public class Helium {
 
   public void setVisualizationPackageOrder(List<String> orderedPackageList)
       throws IOException {
-    heliumConf.setVisualizationDisplayOrder(orderedPackageList);
+    heliumConf.setBundleDisplayOrder(orderedPackageList);
 
-    // if package is visualization, rebuild bundle
-    visualizationFactory.bundle(getVisualizationPackagesToBundle());
+    // if package is visualization, rebuild buildBundle
+    bundleFactory.buildBundle(getBundlePackagesToBundle());
 
     save();
   }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
new file mode 100644
index 0000000..664030f
--- /dev/null
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java
@@ -0,0 +1,415 @@
+/*
+ * 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.
+ */
+package org.apache.zeppelin.helium;
+
+import com.github.eirslett.maven.plugins.frontend.lib.*;
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import com.google.gson.Gson;
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Appender;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.WriterAppender;
+import org.apache.log4j.spi.Filter;
+import org.apache.log4j.spi.LoggingEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.URL;
+import java.util.*;
+
+/**
+ * Load helium visualization
+ */
+public class HeliumBundleFactory {
+  Logger logger = LoggerFactory.getLogger(HeliumBundleFactory.class);
+  private final String NODE_VERSION = "v6.9.1";
+  private final String NPM_VERSION = "3.10.8";
+  private final String DEFAULT_NPM_REGISTRY_URL = "http://registry.npmjs.org/";
+  public static final String HELIUM_LOCAL_REPO = "helium-bundle";
+  public static final String HELIUM_BUNDLE_CACHE = "helium.bundle.cache.js";
+  public static final String HELIUM_BUNDLE = "helium.bundle.js";
+  public static final String HELIUM_BUNDLES_VAR = "heliumBundles";
+
+  private final FrontendPluginFactory frontEndPluginFactory;
+  private final File workingDirectory;
+  private File tabledataModulePath;
+  private File visualizationModulePath;
+  private File spellModulePath;
+  private Gson gson;
+
+  String bundleCacheKey = "";
+  File currentCacheBundle;
+
+  ByteArrayOutputStream out  = new ByteArrayOutputStream();
+
+  public HeliumBundleFactory(
+      File moduleDownloadPath,
+      File tabledataModulePath,
+      File visualizationModulePath,
+      File spellModulePath) throws TaskRunnerException {
+    this(moduleDownloadPath);
+    this.tabledataModulePath = tabledataModulePath;
+    this.visualizationModulePath = visualizationModulePath;
+    this.spellModulePath = spellModulePath;
+  }
+
+  public HeliumBundleFactory(File moduleDownloadPath) throws TaskRunnerException {
+    this.workingDirectory = new File(moduleDownloadPath, HELIUM_LOCAL_REPO);
+    File installDirectory = workingDirectory;
+
+    frontEndPluginFactory = new FrontendPluginFactory(
+        workingDirectory, installDirectory);
+
+    currentCacheBundle = new File(workingDirectory, HELIUM_BUNDLE_CACHE);
+    gson = new Gson();
+    installNodeAndNpm();
+    configureLogger();
+  }
+
+  private void installNodeAndNpm() {
+    try {
+      NPMInstaller npmInstaller = frontEndPluginFactory.getNPMInstaller(getProxyConfig());
+      npmInstaller.setNpmVersion(NPM_VERSION);
+      npmInstaller.install();
+
+      NodeInstaller nodeInstaller = frontEndPluginFactory.getNodeInstaller(getProxyConfig());
+      nodeInstaller.setNodeVersion(NODE_VERSION);
+      nodeInstaller.install();
+    } catch (InstallationException e) {
+      logger.error(e.getMessage(), e);
+    }
+  }
+
+  private ProxyConfig getProxyConfig() {
+    List<ProxyConfig.Proxy> proxy = new LinkedList<>();
+    return new ProxyConfig(proxy);
+  }
+
+  public File buildBundle(List<HeliumPackage> pkgs) throws IOException {
+    return buildBundle(pkgs, false);
+  }
+
+  public synchronized File buildBundle(List<HeliumPackage> pkgs, boolean forceRefresh)
+      throws IOException {
+    // package.json
+    URL pkgUrl = Resources.getResource("helium/package.json");
+    String pkgJson = Resources.toString(pkgUrl, Charsets.UTF_8);
+    StringBuilder dependencies = new StringBuilder();
+    StringBuilder cacheKeyBuilder = new StringBuilder();
+
+    FileFilter npmPackageCopyFilter = new FileFilter() {
+      @Override
+      public boolean accept(File pathname) {
+        String fileName = pathname.getName();
+        if (fileName.startsWith(".") || fileName.startsWith("#") || fileName.startsWith("~")) {
+          return false;
+        } else {
+          return true;
+        }
+      }
+    };
+
+    for (HeliumPackage pkg : pkgs) {
+      String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
+      if (moduleNameVersion == null) {
+        logger.error("Can't get module name and version of package " + pkg.getName());
+        continue;
+      }
+      if (dependencies.length() > 0) {
+        dependencies.append(",\n");
+      }
+      dependencies.append("\"" + moduleNameVersion[0] + "\": \"" + moduleNameVersion[1] + "\"");
+      cacheKeyBuilder.append(pkg.getName() + pkg.getArtifact());
+
+      File pkgInstallDir = new File(workingDirectory, "node_modules/" + pkg.getName());
+      if (pkgInstallDir.exists()) {
+        FileUtils.deleteDirectory(pkgInstallDir);
+      }
+
+      if (isLocalPackage(pkg)) {
+        FileUtils.copyDirectory(
+            new File(pkg.getArtifact()),
+            pkgInstallDir,
+            npmPackageCopyFilter);
+      }
+    }
+    pkgJson = pkgJson.replaceFirst("DEPENDENCIES", dependencies.toString());
+
+    // check if we can use previous buildBundle or not
+    if (cacheKeyBuilder.toString().equals(bundleCacheKey) &&
+        currentCacheBundle.isFile() && !forceRefresh) {
+      return currentCacheBundle;
+    }
+
+    // webpack.config.js
+    URL webpackConfigUrl = Resources.getResource("helium/webpack.config.js");
+    String webpackConfig = Resources.toString(webpackConfigUrl, Charsets.UTF_8);
+
+    // generate load.js
+    StringBuilder loadJsImport = new StringBuilder();
+    StringBuilder loadJsRegister = new StringBuilder();
+
+    long idx = 0;
+    for (HeliumPackage pkg : pkgs) {
+      String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
+      if (moduleNameVersion == null) {
+        continue;
+      }
+
+      String className = "bundles" + idx++;
+      loadJsImport.append(
+          "import " + className + " from \"" + moduleNameVersion[0] + "\"\n");
+
+      loadJsRegister.append(HELIUM_BUNDLES_VAR + ".push({\n");
+      loadJsRegister.append("id: \"" + moduleNameVersion[0] + "\",\n");
+      loadJsRegister.append("name: \"" + pkg.getName() + "\",\n");
+      loadJsRegister.append("icon: " + gson.toJson(pkg.getIcon()) + ",\n");
+      loadJsRegister.append("type: \"" + pkg.getType() + "\",\n");
+      loadJsRegister.append("class: " + className + "\n");
+      loadJsRegister.append("})\n");
+    }
+
+    FileUtils.write(new File(workingDirectory, "package.json"), pkgJson);
+    FileUtils.write(new File(workingDirectory, "webpack.config.js"), webpackConfig);
+    FileUtils.write(new File(workingDirectory, "load.js"),
+        loadJsImport.append(loadJsRegister).toString());
+
+    copyFrameworkModuleToInstallPath(npmPackageCopyFilter);
+
+    try {
+      out.reset();
+      npmCommand("install --loglevel=error");
+    } catch (TaskRunnerException e) {
+      // ignore `(empty)` warning
+      String cause = new String(out.toByteArray());
+      if (!cause.contains("(empty)")) {
+        throw new IOException(cause);
+      }
+    }
+
+    try {
+      out.reset();
+      npmCommand("run bundle");
+    } catch (TaskRunnerException e) {
+      throw new IOException(new String(out.toByteArray()));
+    }
+
+    String bundleStdoutResult = new String(out.toByteArray());
+
+    File heliumBundle = new File(workingDirectory, HELIUM_BUNDLE);
+    if (!heliumBundle.isFile()) {
+      throw new IOException(
+          "Can't create bundle: \n" + bundleStdoutResult);
+    }
+
+    WebpackResult result = getWebpackResultFromOutput(bundleStdoutResult);
+    if (result.errors.length > 0) {
+      heliumBundle.delete();
+      throw new IOException(result.errors[0]);
+    }
+
+    synchronized (this) {
+      currentCacheBundle.delete();
+      FileUtils.moveFile(heliumBundle, currentCacheBundle);
+      bundleCacheKey = cacheKeyBuilder.toString();
+    }
+    return currentCacheBundle;
+  }
+
+  private void copyFrameworkModuleToInstallPath(FileFilter npmPackageCopyFilter)
+      throws IOException {
+    // install tabledata module
+    File tabledataModuleInstallPath = new File(workingDirectory,
+        "node_modules/zeppelin-tabledata");
+    if (tabledataModulePath != null) {
+      if (tabledataModuleInstallPath.exists()) {
+        FileUtils.deleteDirectory(tabledataModuleInstallPath);
+      }
+      FileUtils.copyDirectory(
+          tabledataModulePath,
+          tabledataModuleInstallPath,
+          npmPackageCopyFilter);
+    }
+
+    // install visualization module
+    File visModuleInstallPath = new File(workingDirectory,
+        "node_modules/zeppelin-vis");
+    if (visualizationModulePath != null) {
+      if (visModuleInstallPath.exists()) {
+        // when zeppelin-vis and zeppelin-table package is published to npm repository
+        // we don't need to remove module because npm install command will take care
+        // dependency version change. However, when two dependencies are copied manually
+        // into node_modules directory, changing vis package version results inconsistent npm
+        // install behavior.
+        //
+        // Remote vis package everytime and let npm download every time bundle as a workaround
+        FileUtils.deleteDirectory(visModuleInstallPath);
+      }
+      FileUtils.copyDirectory(visualizationModulePath, visModuleInstallPath, npmPackageCopyFilter);
+    }
+
+    // install spell module
+    File spellModuleInstallPath = new File(workingDirectory,
+        "node_modules/zeppelin-spell");
+    if (spellModulePath != null) {
+      if (spellModuleInstallPath.exists()) {
+        FileUtils.deleteDirectory(spellModuleInstallPath);
+      }
+
+      FileUtils.copyDirectory(
+          spellModulePath,
+          spellModuleInstallPath,
+          npmPackageCopyFilter);
+    }
+  }
+
+  private WebpackResult getWebpackResultFromOutput(String output) {
+    BufferedReader reader = new BufferedReader(new StringReader(output));
+
+    String line;
+    boolean webpackRunDetected = false;
+    boolean resultJsonDetected = false;
+    StringBuffer sb = new StringBuffer();
+    try {
+      while ((line = reader.readLine()) != null) {
+        if (!webpackRunDetected) {
+          if (line.contains("webpack.js") && line.endsWith("--json")) {
+            webpackRunDetected = true;
+          }
+          continue;
+        }
+
+        if (!resultJsonDetected) {
+          if (line.equals("{")) {
+            sb.append(line);
+            resultJsonDetected = true;
+          }
+          continue;
+        }
+
+        if (resultJsonDetected && webpackRunDetected) {
+          sb.append(line);
+        }
+      }
+
+      Gson gson = new Gson();
+      return gson.fromJson(sb.toString(), WebpackResult.class);
+    } catch (IOException e) {
+      logger.error(e.getMessage(), e);
+      return new WebpackResult();
+    }
+  }
+
+  public File getCurrentCacheBundle() {
+    synchronized (this) {
+      if (currentCacheBundle.isFile()) {
+        return currentCacheBundle;
+      } else {
+        return null;
+      }
+    }
+  }
+
+  private boolean isLocalPackage(HeliumPackage pkg) {
+    return (pkg.getArtifact().startsWith(".") || pkg.getArtifact().startsWith("/"));
+  }
+
+  private String[] getNpmModuleNameAndVersion(HeliumPackage pkg) {
+    String artifact = pkg.getArtifact();
+
+    if (isLocalPackage(pkg)) {
+      File packageJson = new File(artifact, "package.json");
+      if (!packageJson.isFile()) {
+        return null;
+      }
+      Gson gson = new Gson();
+      try {
+        NpmPackage npmPackage = gson.fromJson(
+            FileUtils.readFileToString(packageJson),
+            NpmPackage.class);
+
+        String[] nameVersion = new String[2];
+        nameVersion[0] = npmPackage.name;
+        nameVersion[1] = npmPackage.version;
+        return nameVersion;
+      } catch (IOException e) {
+        logger.error(e.getMessage(), e);
+        return null;
+      }
+    } else {
+      String[] nameVersion = new String[2];
+
+      int pos;
+      if ((pos = artifact.indexOf('@')) > 0) {
+        nameVersion[0] = artifact.substring(0, pos);
+        nameVersion[1] = artifact.substring(pos + 1);
+      } else if (
+          (pos = artifact.indexOf('^')) > 0 ||
+              (pos = artifact.indexOf('~')) > 0) {
+        nameVersion[0] = artifact.substring(0, pos);
+        nameVersion[1] = artifact.substring(pos);
+      } else {
+        nameVersion[0] = artifact;
+        nameVersion[1] = "";
+      }
+      return nameVersion;
+    }
+  }
+
+  public synchronized void install(HeliumPackage pkg) throws TaskRunnerException {
+    npmCommand("install " + pkg.getArtifact() + " npm install --loglevel=error");
+  }
+
+  private void npmCommand(String args) throws TaskRunnerException {
+    npmCommand(args, new HashMap<String, String>());
+  }
+
+  private void npmCommand(String args, Map<String, String> env) throws TaskRunnerException {
+    NpmRunner npm = frontEndPluginFactory.getNpmRunner(getProxyConfig(), DEFAULT_NPM_REGISTRY_URL);
+
+    npm.execute(args, env);
+  }
+
+  private void configureLogger() {
+    org.apache.log4j.Logger npmLogger = org.apache.log4j.Logger.getLogger(
+        "com.github.eirslett.maven.plugins.frontend.lib.DefaultNpmRunner");
+    Enumeration appenders = org.apache.log4j.Logger.getRootLogger().getAllAppenders();
+
+    if (appenders != null) {
+      while (appenders.hasMoreElements()) {
+        Appender appender = (Appender) appenders.nextElement();
+        appender.addFilter(new Filter() {
+
+          @Override
+          public int decide(LoggingEvent loggingEvent) {
+            if (loggingEvent.getLoggerName().contains("DefaultNpmRunner")) {
+              return DENY;
+            } else {
+              return NEUTRAL;
+            }
+          }
+        });
+      }
+    }
+    npmLogger.addAppender(new WriterAppender(
+        new PatternLayout("%m%n"),
+        out
+    ));
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumConf.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumConf.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumConf.java
index 5094934..d60aec7 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumConf.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumConf.java
@@ -26,7 +26,7 @@ public class HeliumConf {
   Map<String, String> enabled = Collections.synchronizedMap(new HashMap<String, String>());
 
   // enabled visualization package display order
-  List<String> visualizationDisplayOrder = new LinkedList<>();
+  List<String> bundleDisplayOrder = new LinkedList<>();
 
   public Map<String, String> getEnabledPackages() {
     return new HashMap<>(enabled);
@@ -48,15 +48,15 @@ public class HeliumConf {
     enabled.remove(name);
   }
 
-  public List<String> getVisualizationDisplayOrder() {
-    if (visualizationDisplayOrder == null) {
+  public List<String> getBundleDisplayOrder() {
+    if (bundleDisplayOrder == null) {
       return new LinkedList<String>();
     } else {
-      return visualizationDisplayOrder;
+      return bundleDisplayOrder;
     }
   }
 
-  public void setVisualizationDisplayOrder(List<String> orderedPackageList) {
-    visualizationDisplayOrder = orderedPackageList;
+  public void setBundleDisplayOrder(List<String> orderedPackageList) {
+    bundleDisplayOrder = orderedPackageList;
   }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java
deleted file mode 100644
index 624f12a..0000000
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * 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.
- */
-package org.apache.zeppelin.helium;
-
-import com.github.eirslett.maven.plugins.frontend.lib.*;
-import com.google.common.base.Charsets;
-import com.google.common.io.Resources;
-import com.google.gson.Gson;
-import org.apache.commons.io.FileUtils;
-import org.apache.log4j.Appender;
-import org.apache.log4j.PatternLayout;
-import org.apache.log4j.WriterAppender;
-import org.apache.log4j.spi.Filter;
-import org.apache.log4j.spi.LoggingEvent;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.*;
-import java.net.URL;
-import java.util.*;
-
-/**
- * Load helium visualization
- */
-public class HeliumVisualizationFactory {
-  Logger logger = LoggerFactory.getLogger(HeliumVisualizationFactory.class);
-  private final String NODE_VERSION = "v6.9.1";
-  private final String NPM_VERSION = "3.10.8";
-  private final String DEFAULT_NPM_REGISTRY_URL = "http://registry.npmjs.org/";
-
-  private final FrontendPluginFactory frontEndPluginFactory;
-  private final File workingDirectory;
-  private File tabledataModulePath;
-  private File visualizationModulePath;
-  private Gson gson;
-
-  String bundleCacheKey = "";
-  File currentBundle;
-
-  ByteArrayOutputStream out  = new ByteArrayOutputStream();
-
-  public HeliumVisualizationFactory(
-      File moduleDownloadPath,
-      File tabledataModulePath,
-      File visualizationModulePath) throws TaskRunnerException {
-    this(moduleDownloadPath);
-    this.tabledataModulePath = tabledataModulePath;
-    this.visualizationModulePath = visualizationModulePath;
-  }
-
-  public HeliumVisualizationFactory(File moduleDownloadPath) throws TaskRunnerException {
-    this.workingDirectory = new File(moduleDownloadPath, "vis");
-    File installDirectory = workingDirectory;
-
-    frontEndPluginFactory = new FrontendPluginFactory(
-        workingDirectory, installDirectory);
-
-    currentBundle = new File(workingDirectory, "vis.bundle.cache.js");
-    gson = new Gson();
-    installNodeAndNpm();
-    configureLogger();
-  }
-
-  private void installNodeAndNpm() {
-    try {
-      NPMInstaller npmInstaller = frontEndPluginFactory.getNPMInstaller(getProxyConfig());
-      npmInstaller.setNpmVersion(NPM_VERSION);
-      npmInstaller.install();
-
-      NodeInstaller nodeInstaller = frontEndPluginFactory.getNodeInstaller(getProxyConfig());
-      nodeInstaller.setNodeVersion(NODE_VERSION);
-      nodeInstaller.install();
-    } catch (InstallationException e) {
-      logger.error(e.getMessage(), e);
-    }
-  }
-
-  private ProxyConfig getProxyConfig() {
-    List<ProxyConfig.Proxy> proxy = new LinkedList<>();
-    return new ProxyConfig(proxy);
-  }
-
-  public File bundle(List<HeliumPackage> pkgs) throws IOException {
-    return bundle(pkgs, false);
-  }
-
-  public synchronized File bundle(List<HeliumPackage> pkgs, boolean forceRefresh)
-      throws IOException {
-    // package.json
-    URL pkgUrl = Resources.getResource("helium/package.json");
-    String pkgJson = Resources.toString(pkgUrl, Charsets.UTF_8);
-    StringBuilder dependencies = new StringBuilder();
-    StringBuilder cacheKeyBuilder = new StringBuilder();
-
-    FileFilter npmPackageCopyFilter = new FileFilter() {
-      @Override
-      public boolean accept(File pathname) {
-        String fileName = pathname.getName();
-        if (fileName.startsWith(".") || fileName.startsWith("#") || fileName.startsWith("~")) {
-          return false;
-        } else {
-          return true;
-        }
-      }
-    };
-
-    for (HeliumPackage pkg : pkgs) {
-      String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
-      if (moduleNameVersion == null) {
-        logger.error("Can't get module name and version of package " + pkg.getName());
-        continue;
-      }
-      if (dependencies.length() > 0) {
-        dependencies.append(",\n");
-      }
-      dependencies.append("\"" + moduleNameVersion[0] + "\": \"" + moduleNameVersion[1] + "\"");
-      cacheKeyBuilder.append(pkg.getName() + pkg.getArtifact());
-
-      File pkgInstallDir = new File(workingDirectory, "node_modules/" + pkg.getName());
-      if (pkgInstallDir.exists()) {
-        FileUtils.deleteDirectory(pkgInstallDir);
-      }
-
-      if (isLocalPackage(pkg)) {
-        FileUtils.copyDirectory(
-            new File(pkg.getArtifact()),
-            pkgInstallDir,
-            npmPackageCopyFilter);
-      }
-    }
-    pkgJson = pkgJson.replaceFirst("DEPENDENCIES", dependencies.toString());
-
-    // check if we can use previous bundle or not
-    if (cacheKeyBuilder.toString().equals(bundleCacheKey)
-        && currentBundle.isFile() && !forceRefresh) {
-      return currentBundle;
-    }
-
-    // webpack.config.js
-    URL webpackConfigUrl = Resources.getResource("helium/webpack.config.js");
-    String webpackConfig = Resources.toString(webpackConfigUrl, Charsets.UTF_8);
-
-    // generate load.js
-    StringBuilder loadJsImport = new StringBuilder();
-    StringBuilder loadJsRegister = new StringBuilder();
-
-    long idx = 0;
-    for (HeliumPackage pkg : pkgs) {
-      String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
-      if (moduleNameVersion == null) {
-        continue;
-      }
-
-      String className = "vis" + idx++;
-      loadJsImport.append(
-          "import " + className + " from \"" + moduleNameVersion[0] + "\"\n");
-
-      loadJsRegister.append("visualizations.push({\n");
-      loadJsRegister.append("id: \"" + moduleNameVersion[0] + "\",\n");
-      loadJsRegister.append("name: \"" + pkg.getName() + "\",\n");
-      loadJsRegister.append("icon: " + gson.toJson(pkg.getIcon()) + ",\n");
-      loadJsRegister.append("class: " + className + "\n");
-      loadJsRegister.append("})\n");
-    }
-
-    FileUtils.write(new File(workingDirectory, "package.json"), pkgJson);
-    FileUtils.write(new File(workingDirectory, "webpack.config.js"), webpackConfig);
-    FileUtils.write(new File(workingDirectory, "load.js"),
-        loadJsImport.append(loadJsRegister).toString());
-
-    // install tabledata module
-    File tabledataModuleInstallPath = new File(workingDirectory,
-        "node_modules/zeppelin-tabledata");
-    if (tabledataModulePath != null) {
-      if (tabledataModuleInstallPath.exists()) {
-        FileUtils.deleteDirectory(tabledataModuleInstallPath);
-      }
-      FileUtils.copyDirectory(
-          tabledataModulePath,
-          tabledataModuleInstallPath,
-          npmPackageCopyFilter);
-    }
-
-    // install visualization module
-    File visModuleInstallPath = new File(workingDirectory,
-        "node_modules/zeppelin-vis");
-    if (visualizationModulePath != null) {
-      if (visModuleInstallPath.exists()) {
-        // when zeppelin-vis and zeppelin-table package is published to npm repository
-        // we don't need to remove module because npm install command will take care
-        // dependency version change. However, when two dependencies are copied manually
-        // into node_modules directory, changing vis package version results inconsistent npm
-        // install behavior.
-        //
-        // Remote vis package everytime and let npm download every time bundle as a workaround
-        FileUtils.deleteDirectory(visModuleInstallPath);
-      }
-      FileUtils.copyDirectory(visualizationModulePath, visModuleInstallPath, npmPackageCopyFilter);
-    }
-
-    out.reset();
-    try {
-      npmCommand("install");
-      npmCommand("run bundle");
-    } catch (TaskRunnerException e) {
-      throw new IOException(new String(out.toByteArray()));
-    }
-
-    File visBundleJs = new File(workingDirectory, "vis.bundle.js");
-    if (!visBundleJs.isFile()) {
-      throw new IOException(
-          "Can't create visualization bundle : \n" + new String(out.toByteArray()));
-    }
-
-    WebpackResult result = getWebpackResultFromOutput(new String(out.toByteArray()));
-    if (result.errors.length > 0) {
-      visBundleJs.delete();
-      throw new IOException(result.errors[0]);
-    }
-
-    synchronized (this) {
-      currentBundle.delete();
-      FileUtils.moveFile(visBundleJs, currentBundle);
-      bundleCacheKey = cacheKeyBuilder.toString();
-    }
-    return currentBundle;
-  }
-
-  private WebpackResult getWebpackResultFromOutput(String output) {
-    BufferedReader reader = new BufferedReader(new StringReader(output));
-
-    String line;
-    boolean webpackRunDetected = false;
-    boolean resultJsonDetected = false;
-    StringBuffer sb = new StringBuffer();
-    try {
-      while ((line = reader.readLine()) != null) {
-        if (!webpackRunDetected) {
-          if (line.contains("webpack.js") && line.endsWith("--json")) {
-            webpackRunDetected = true;
-          }
-          continue;
-        }
-
-        if (!resultJsonDetected) {
-          if (line.equals("{")) {
-            sb.append(line);
-            resultJsonDetected = true;
-          }
-          continue;
-        }
-
-        if (resultJsonDetected && webpackRunDetected) {
-          sb.append(line);
-        }
-      }
-
-      Gson gson = new Gson();
-      return gson.fromJson(sb.toString(), WebpackResult.class);
-    } catch (IOException e) {
-      logger.error(e.getMessage(), e);
-      return new WebpackResult();
-    }
-  }
-
-  public File getCurrentBundle() {
-    synchronized (this) {
-      if (currentBundle.isFile()) {
-        return currentBundle;
-      } else {
-        return null;
-      }
-    }
-  }
-
-  private boolean isLocalPackage(HeliumPackage pkg) {
-    return (pkg.getArtifact().startsWith(".") || pkg.getArtifact().startsWith("/"));
-  }
-
-  private String[] getNpmModuleNameAndVersion(HeliumPackage pkg) {
-    String artifact = pkg.getArtifact();
-
-    if (isLocalPackage(pkg)) {
-      File packageJson = new File(artifact, "package.json");
-      if (!packageJson.isFile()) {
-        return null;
-      }
-      Gson gson = new Gson();
-      try {
-        NpmPackage npmPackage = gson.fromJson(
-            FileUtils.readFileToString(packageJson),
-            NpmPackage.class);
-
-        String[] nameVersion = new String[2];
-        nameVersion[0] = npmPackage.name;
-        nameVersion[1] = npmPackage.version;
-        return nameVersion;
-      } catch (IOException e) {
-        logger.error(e.getMessage(), e);
-        return null;
-      }
-    } else {
-      String[] nameVersion = new String[2];
-
-      int pos;
-      if ((pos = artifact.indexOf('@')) > 0) {
-        nameVersion[0] = artifact.substring(0, pos);
-        nameVersion[1] = artifact.substring(pos + 1);
-      } else if (
-          (pos = artifact.indexOf('^')) > 0 ||
-              (pos = artifact.indexOf('~')) > 0) {
-        nameVersion[0] = artifact.substring(0, pos);
-        nameVersion[1] = artifact.substring(pos);
-      } else {
-        nameVersion[0] = artifact;
-        nameVersion[1] = "";
-      }
-      return nameVersion;
-    }
-  }
-
-  public synchronized void install(HeliumPackage pkg) throws TaskRunnerException {
-    npmCommand("install " + pkg.getArtifact());
-  }
-
-  private void npmCommand(String args) throws TaskRunnerException {
-    npmCommand(args, new HashMap<String, String>());
-  }
-
-  private void npmCommand(String args, Map<String, String> env) throws TaskRunnerException {
-    NpmRunner npm = frontEndPluginFactory.getNpmRunner(getProxyConfig(), DEFAULT_NPM_REGISTRY_URL);
-
-    npm.execute(args, env);
-  }
-
-  private void configureLogger() {
-    org.apache.log4j.Logger npmLogger = org.apache.log4j.Logger.getLogger(
-        "com.github.eirslett.maven.plugins.frontend.lib.DefaultNpmRunner");
-    Enumeration appenders = org.apache.log4j.Logger.getRootLogger().getAllAppenders();
-
-    if (appenders != null) {
-      while (appenders.hasMoreElements()) {
-        Appender appender = (Appender) appenders.nextElement();
-        appender.addFilter(new Filter() {
-
-          @Override
-          public int decide(LoggingEvent loggingEvent) {
-            if (loggingEvent.getLoggerName().contains("DefaultNpmRunner")) {
-              return DENY;
-            } else {
-              return NEUTRAL;
-            }
-          }
-        });
-      }
-    }
-    npmLogger.addAppender(new WriterAppender(
-        new PatternLayout("%m%n"),
-        out
-    ));
-  }
-}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
index 162baf8..a6d1546 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
@@ -172,7 +172,9 @@ public class Message {
     PARAGRAPH_REMOVED,            // [s-c] paragraph deleted
     PARAGRAPH_MOVED,              // [s-c] paragraph moved
     NOTE_UPDATED,                 // [s-c] paragraph updated(name, config)
-    RUN_ALL_PARAGRAPHS            // [c-s] run all paragraphs
+    RUN_ALL_PARAGRAPHS,           // [c-s] run all paragraphs
+    PARAGRAPH_EXECUTED_BY_SPELL,  // [c-s] paragraph was executed by spell
+    RUN_PARAGRAPH_USING_SPELL     // [s-c] run paragraph using spell
   }
 
   public static final Message EMPTY = new Message(null);

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/resources/helium/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/resources/helium/package.json b/zeppelin-zengine/src/main/resources/helium/package.json
index e6ec612..cd4e470 100644
--- a/zeppelin-zengine/src/main/resources/helium/package.json
+++ b/zeppelin-zengine/src/main/resources/helium/package.json
@@ -1,5 +1,5 @@
 {
-  "name": "zeppelin-vis-bundle",
+  "name": "zeppelin-helium-bundle",
   "main": "load",
   "scripts": {
     "bundle": "node/node node_modules/webpack/bin/webpack.js --display-error-details --json"

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/main/resources/helium/webpack.config.js
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/resources/helium/webpack.config.js b/zeppelin-zengine/src/main/resources/helium/webpack.config.js
index 2b5015e..c318c10 100644
--- a/zeppelin-zengine/src/main/resources/helium/webpack.config.js
+++ b/zeppelin-zengine/src/main/resources/helium/webpack.config.js
@@ -18,7 +18,7 @@ module.exports = {
     entry: ['./'],
     output: {
         path: './',
-        filename: 'vis.bundle.js',
+        filename: 'helium.bundle.js',
     },
     resolve: {
         root: __dirname + "/node_modules"

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java
index 2588c4c..99cdeca 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumApplicationFactoryTest.java
@@ -129,7 +129,7 @@ public class HeliumApplicationFactoryTest implements JobListenerFactory {
   public void testLoadRunUnloadApplication()
       throws IOException, ApplicationException, InterruptedException {
     // given
-    HeliumPackage pkg1 = new HeliumPackage(HeliumPackage.Type.APPLICATION,
+    HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION,
         "name1",
         "desc1",
         "",
@@ -175,7 +175,7 @@ public class HeliumApplicationFactoryTest implements JobListenerFactory {
   @Test
   public void testUnloadOnParagraphRemove() throws IOException {
     // given
-    HeliumPackage pkg1 = new HeliumPackage(HeliumPackage.Type.APPLICATION,
+    HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION,
         "name1",
         "desc1",
         "",
@@ -215,7 +215,7 @@ public class HeliumApplicationFactoryTest implements JobListenerFactory {
   @Test
   public void testUnloadOnInterpreterUnbind() throws IOException {
     // given
-    HeliumPackage pkg1 = new HeliumPackage(HeliumPackage.Type.APPLICATION,
+    HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION,
         "name1",
         "desc1",
         "",
@@ -276,7 +276,7 @@ public class HeliumApplicationFactoryTest implements JobListenerFactory {
   @Test
   public void testUnloadOnInterpreterRestart() throws IOException {
     // given
-    HeliumPackage pkg1 = new HeliumPackage(HeliumPackage.Type.APPLICATION,
+    HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION,
         "name1",
         "desc1",
         "",

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
new file mode 100644
index 0000000..503cc07
--- /dev/null
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+package org.apache.zeppelin.helium;
+
+import com.github.eirslett.maven.plugins.frontend.lib.InstallationException;
+import com.github.eirslett.maven.plugins.frontend.lib.TaskRunnerException;
+import com.google.common.io.Resources;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class HeliumBundleFactoryTest {
+  private File tmpDir;
+  private HeliumBundleFactory hbf;
+
+  @Before
+  public void setUp() throws InstallationException, TaskRunnerException {
+    tmpDir = new File(System.getProperty("java.io.tmpdir") + "/ZeppelinLTest_" + System.currentTimeMillis());
+    tmpDir.mkdirs();
+
+    // get module dir
+    URL res = Resources.getResource("helium/webpack.config.js");
+    String resDir = new File(res.getFile()).getParent();
+    File moduleDir = new File(resDir + "/../../../../zeppelin-web/src/app/");
+
+    hbf = new HeliumBundleFactory(tmpDir,
+        new File(moduleDir, "tabledata"),
+        new File(moduleDir, "visualization"),
+        new File(moduleDir, "spell"));
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    FileUtils.deleteDirectory(tmpDir);
+  }
+
+  @Test
+  public void testInstallNpm() throws InstallationException {
+    assertTrue(new File(tmpDir,
+        HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node/npm").isFile());
+    assertTrue(new File(tmpDir,
+        HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node/node").isFile());
+  }
+
+  @Test
+  public void downloadPackage() throws TaskRunnerException {
+    HeliumPackage pkg = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "lodash",
+        "lodash",
+        "lodash@3.9.3",
+        "",
+        null,
+        "license",
+        "icon"
+    );
+    hbf.install(pkg);
+    assertTrue(new File(tmpDir,
+        HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node_modules/lodash").isDirectory());
+  }
+
+  @Test
+  public void bundlePackage() throws IOException, TaskRunnerException {
+    HeliumPackage pkg = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart@0.0.3",
+        "",
+        null,
+        "license",
+        "icon"
+    );
+    List<HeliumPackage> pkgs = new LinkedList<>();
+    pkgs.add(pkg);
+    File bundle = hbf.buildBundle(pkgs);
+    assertTrue(bundle.isFile());
+    long lastModified = bundle.lastModified();
+
+    // buildBundle again and check if it served from cache
+    bundle = hbf.buildBundle(pkgs);
+    assertEquals(lastModified, bundle.lastModified());
+  }
+
+
+  @Test
+  public void bundleLocalPackage() throws IOException, TaskRunnerException {
+    URL res = Resources.getResource("helium/webpack.config.js");
+    String resDir = new File(res.getFile()).getParent();
+    String localPkg = resDir + "/../../../src/test/resources/helium/vis1";
+
+    HeliumPackage pkg = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "vis1",
+        "vis1",
+        localPkg,
+        "",
+        null,
+        "license",
+        "fa fa-coffee"
+    );
+    List<HeliumPackage> pkgs = new LinkedList<>();
+    pkgs.add(pkg);
+    File bundle = hbf.buildBundle(pkgs);
+    assertTrue(bundle.isFile());
+  }
+
+  @Test
+  public void bundleErrorPropagation() throws IOException, TaskRunnerException {
+    URL res = Resources.getResource("helium/webpack.config.js");
+    String resDir = new File(res.getFile()).getParent();
+    String localPkg = resDir + "/../../../src/test/resources/helium/vis2";
+
+    HeliumPackage pkg = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "vis2",
+        "vis2",
+        localPkg,
+        "",
+        null,
+        "license",
+        "fa fa-coffee"
+    );
+    List<HeliumPackage> pkgs = new LinkedList<>();
+    pkgs.add(pkg);
+    File bundle = null;
+    try {
+      bundle = hbf.buildBundle(pkgs);
+      // should throw exception
+      assertTrue(false);
+    } catch (IOException e) {
+      assertTrue(e.getMessage().contains("error in the package"));
+    }
+    assertNull(bundle);
+  }
+
+  @Test
+  public void switchVersion() throws IOException, TaskRunnerException {
+    URL res = Resources.getResource("helium/webpack.config.js");
+    String resDir = new File(res.getFile()).getParent();
+
+    HeliumPackage pkgV1 = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart@0.0.3",
+        "",
+        null,
+        "license",
+        "icon"
+    );
+
+    HeliumPackage pkgV2 = new HeliumPackage(
+        HeliumType.VISUALIZATION,
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart",
+        "zeppelin-bubblechart@0.0.1",
+        "",
+        null,
+        "license",
+        "icon"
+    );
+    List<HeliumPackage> pkgsV1 = new LinkedList<>();
+    pkgsV1.add(pkgV1);
+
+    List<HeliumPackage> pkgsV2 = new LinkedList<>();
+    pkgsV2.add(pkgV2);
+
+    File bundle1 = hbf.buildBundle(pkgsV1);
+    File bundle2 = hbf.buildBundle(pkgsV2);
+
+    assertNotSame(bundle1.lastModified(), bundle2.lastModified());
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumLocalRegistryTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumLocalRegistryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumLocalRegistryTest.java
index 03d77b7..0f490d1 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumLocalRegistryTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumLocalRegistryTest.java
@@ -50,7 +50,7 @@ public class HeliumLocalRegistryTest {
 
     // when
     Gson gson = new Gson();
-    HeliumPackage pkg1 = new HeliumPackage(HeliumPackage.Type.APPLICATION,
+    HeliumPackage pkg1 = new HeliumPackage(HeliumType.APPLICATION,
         "app1",
         "desc1",
         "artifact1",

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java
index 9db9477..1607c2c 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumTest.java
@@ -24,7 +24,6 @@ import org.junit.Test;
 
 import java.io.File;
 import java.io.IOException;
-import java.net.URI;
 import java.net.URISyntaxException;
 
 import static org.junit.Assert.assertEquals;
@@ -79,7 +78,7 @@ public class HeliumTest {
 
     // when
     registry1.add(new HeliumPackage(
-        HeliumPackage.Type.APPLICATION,
+        HeliumType.APPLICATION,
         "name1",
         "desc1",
         "artifact1",
@@ -89,7 +88,7 @@ public class HeliumTest {
         ""));
 
     registry2.add(new HeliumPackage(
-        HeliumPackage.Type.APPLICATION,
+        HeliumType.APPLICATION,
         "name2",
         "desc2",
         "artifact2",

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java
deleted file mode 100644
index e5a61ed..0000000
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * 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.
- */
-package org.apache.zeppelin.helium;
-
-import com.github.eirslett.maven.plugins.frontend.lib.InstallationException;
-import com.github.eirslett.maven.plugins.frontend.lib.TaskRunnerException;
-import com.google.common.io.Resources;
-import org.apache.commons.io.FileUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.util.LinkedList;
-import java.util.List;
-
-import static org.junit.Assert.*;
-
-public class HeliumVisualizationFactoryTest {
-  private File tmpDir;
-  private HeliumVisualizationFactory hvf;
-
-  @Before
-  public void setUp() throws InstallationException, TaskRunnerException {
-    tmpDir = new File(System.getProperty("java.io.tmpdir") + "/ZeppelinLTest_" + System.currentTimeMillis());
-    tmpDir.mkdirs();
-
-    // get module dir
-    URL res = Resources.getResource("helium/webpack.config.js");
-    String resDir = new File(res.getFile()).getParent();
-    File moduleDir = new File(resDir + "/../../../../zeppelin-web/src/app/");
-
-    hvf = new HeliumVisualizationFactory(tmpDir,
-        new File(moduleDir, "tabledata"),
-        new File(moduleDir, "visualization"));
-  }
-
-  @After
-  public void tearDown() throws IOException {
-    FileUtils.deleteDirectory(tmpDir);
-  }
-
-  @Test
-  public void testInstallNpm() throws InstallationException {
-    assertTrue(new File(tmpDir, "vis/node/npm").isFile());
-    assertTrue(new File(tmpDir, "vis/node/node").isFile());
-  }
-
-  @Test
-  public void downloadPackage() throws TaskRunnerException {
-    HeliumPackage pkg = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "lodash",
-        "lodash",
-        "lodash@3.9.3",
-        "",
-        null,
-        "license",
-        "icon"
-    );
-    hvf.install(pkg);
-    assertTrue(new File(tmpDir, "vis/node_modules/lodash").isDirectory());
-  }
-
-  @Test
-  public void bundlePackage() throws IOException, TaskRunnerException {
-    HeliumPackage pkg = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart@0.0.3",
-        "",
-        null,
-        "license",
-        "icon"
-    );
-    List<HeliumPackage> pkgs = new LinkedList<>();
-    pkgs.add(pkg);
-    File bundle = hvf.bundle(pkgs);
-    assertTrue(bundle.isFile());
-    long lastModified = bundle.lastModified();
-
-    // bundle again and check if it served from cache
-    bundle = hvf.bundle(pkgs);
-    assertEquals(lastModified, bundle.lastModified());
-  }
-
-
-  @Test
-  public void bundleLocalPackage() throws IOException, TaskRunnerException {
-    URL res = Resources.getResource("helium/webpack.config.js");
-    String resDir = new File(res.getFile()).getParent();
-    String localPkg = resDir + "/../../../src/test/resources/helium/vis1";
-
-    HeliumPackage pkg = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "vis1",
-        "vis1",
-        localPkg,
-        "",
-        null,
-        "license",
-        "fa fa-coffee"
-    );
-    List<HeliumPackage> pkgs = new LinkedList<>();
-    pkgs.add(pkg);
-    File bundle = hvf.bundle(pkgs);
-    assertTrue(bundle.isFile());
-  }
-
-  @Test
-  public void bundleErrorPropagation() throws IOException, TaskRunnerException {
-    URL res = Resources.getResource("helium/webpack.config.js");
-    String resDir = new File(res.getFile()).getParent();
-    String localPkg = resDir + "/../../../src/test/resources/helium/vis2";
-
-    HeliumPackage pkg = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "vis2",
-        "vis2",
-        localPkg,
-        "",
-        null,
-        "license",
-        "fa fa-coffee"
-    );
-    List<HeliumPackage> pkgs = new LinkedList<>();
-    pkgs.add(pkg);
-    File bundle = null;
-    try {
-      bundle = hvf.bundle(pkgs);
-      // should throw exception
-      assertTrue(false);
-    } catch (IOException e) {
-      assertTrue(e.getMessage().contains("error in the package"));
-    }
-    assertNull(bundle);
-  }
-
-  @Test
-  public void switchVersion() throws IOException, TaskRunnerException {
-    URL res = Resources.getResource("helium/webpack.config.js");
-    String resDir = new File(res.getFile()).getParent();
-
-    HeliumPackage pkgV1 = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart@0.0.3",
-        "",
-        null,
-        "license",
-        "icon"
-    );
-
-    HeliumPackage pkgV2 = new HeliumPackage(
-        HeliumPackage.Type.VISUALIZATION,
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart",
-        "zeppelin-bubblechart@0.0.1",
-        "",
-        null,
-        "license",
-        "icon"
-    );
-    List<HeliumPackage> pkgsV1 = new LinkedList<>();
-    pkgsV1.add(pkgV1);
-
-    List<HeliumPackage> pkgsV2 = new LinkedList<>();
-    pkgsV2.add(pkgV2);
-
-    File bundle1 = hvf.bundle(pkgsV1);
-    File bundle2 = hvf.bundle(pkgsV2);
-
-    assertNotSame(bundle1.lastModified(), bundle2.lastModified());
-  }
-}


[3/3] zeppelin git commit: [ZEPPELIN-2008] Introduce Spell

Posted by mo...@apache.org.
[ZEPPELIN-2008] Introduce Spell

### What is this PR for?

Implemented **Spell** as one of Helium categories. *Technically, it's the frontend interpreter* runs on browser not backend.

Spell can provide many benefits.

1. Anyone can install, remove easily using helium package registry by #1936
2. Implementing spell is extremely easier rather than adding backend interpreter
3. Can use existing javsacript libraries. (e.g [flowchart.js](http://flowchart.js.org/), [sequence diagram js](https://github.com/bramp/js-sequence-diagrams), ...). This enable us to add many visualization tools. Imagine that you can implement some custom interpreters with few lines of code like [flowchart-spell-example](https://github.com/apache/zeppelin/compare/master...1ambda:ZEPPELIN-2008/introduce-spell?expand=1#diff-364845b20d68e4d94688e44fef03da98)
4. The most important thing is, spell is not only interpreter but also display system. Because it runs on browser. So we can use spell display system with another spell **Display System with Spell** (see the screenshot section below)

 **In future**, we will be able to combine existing backend interpreters with spell like (**not supported in this PR cause we need to modify backend code a lot**)

```
// if we have markdown spell, we can use `%markdown` display in the spark interpreter

%spark

val calculated = doSomething()
println(s"%markdown _${calculated})
```

I added some examples. Checkout `echo`, `markdown`, `translator`, `flowchart` spells.

### What type of PR is it?
[Feature]

### Todos
* [x] - Add `SPELL` as one of Helium categories.
* [x] - Implement framework code (`zeppelin-spell`)
* [x] - Make some examples (flowchart, google translator, markdown, echo)
* [x] - Support custom display system
* [x] - Fix some bugs in `HeliumBundleFactory`
* [x] - Save spell rendering result into `note.json` while broadcasting to other websocket clients
* [x] - Fix `renderText` for stream output

### What is the Jira issue?

[ZEPPELIN-2008](https://issues.apache.org/jira/browse/ZEPPELIN-2008)

### How should this be tested?

- Build `mvn clean package -Phelium-dev -Pexamples -DskipTests;`
- Go to helium page `http://localhost:8080/#/helium`
- Enable all spells
- Go to a notebook and refresh
- Follow actions in the screenshots below.

### Screenshots (if appropriate)

#### Flowchart Spell (Sample)

![flowchart-spell](https://cloud.githubusercontent.com/assets/4968473/22275041/305f0eb8-e2ed-11e6-846a-9f1263ae46bc.gif)

#### Google Translator Spell (Sample)

![translator-spell](https://cloud.githubusercontent.com/assets/4968473/22280993/9820c238-e317-11e6-90f4-0e483312a09a.gif)

#### Display System with Spell

![display-spell](https://cloud.githubusercontent.com/assets/4968473/22275044/33694b78-e2ed-11e6-9ef0-188f4038381f.gif)

### Questions:
* Does the licenses files need update - NO
* Is there breaking changes for older versions? - NO
* Does this needs documentation? - YES, but framework can be enhanced so i would like to defer to write document right now.

Author: 1ambda <1a...@gmail.com>

Closes #1940 from 1ambda/ZEPPELIN-2008/introduce-spell and squashes the following commits:

c1b5356 [1ambda] fix: RAT issues
e07ecd3 [1ambda] fix: Set width for spell usage
6c91892 [1ambda] feat: Display magic, usage for spell
5be2890 [1ambda] feat: Support spell info
822a1d8 [1ambda] style: Remove useless func wrap for helium
35d0fcc [1ambda] fix: Update desc for spell examples
49e03fc [1ambda] fix: List visualziation bundles only in order
4029c02 [1ambda] fix: ParagraphIT, parameterizedQueryForm
08eba10 [1ambda] refactor: renderGraph in result.controller.js
69ce880 [1ambda] fix: Resolve append (stream) output
0f2d8b6 [1ambda] fix: Resolve output issue
fc4389e [1ambda] fix: Resolve RAT issues
c8c8f0e [1ambda] fix: Add setErrorMessage method to Job
4fec44c [1ambda] refactor: NotebookServer.java
1227d7d [1ambda] refactor: result controller retry
9fb7438 [1ambda] feat: Save spell result and propagate
3cdf2da [1ambda] fix: NPM installation error
72aadbf [1ambda] feat: Enhance translator spell
bd2b3ef [1ambda] style: Rename generator -> data
cac0667 [1ambda] style: Rename to Spell
e81cb03 [1ambda] example: Add echo, markdown
0fa7eda [1ambda] feat: Support custom display
c906da6 [1ambda] feat: Update examples to use single FrontIntpRes
5c49e6e [1ambda] feat: Automated display type checking in result
5810bf1 [1ambda] feat: Apply frontend interpreter to paragraph
a163044 [1ambda] feat: Add flowchart, translator examples
247d00f [1ambda] feat: Add frontend interpreter framework
e925967 [1ambda] feat: Support FRONTEND_INTERPRETER type in frontend
c02d00a [1ambda] feat: Support FRONTEND_INTERPRETER type in backend


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/0589e27e
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/0589e27e
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/0589e27e

Branch: refs/heads/master
Commit: 0589e27e7bb84ec81e1438bcbf3f2fd80ee5a963
Parents: 019df1f
Author: 1ambda <1a...@gmail.com>
Authored: Mon Jan 30 12:44:55 2017 +0900
Committer: Lee moon soo <mo...@apache.org>
Committed: Thu Feb 2 08:33:48 2017 +0900

----------------------------------------------------------------------
 pom.xml                                         |   1 +
 .../src/assemble/distribution.xml               |   4 +
 zeppelin-examples/pom.xml                       |   4 +
 .../zeppelin-example-spell-echo/index.js        |  32 ++
 .../zeppelin-example-spell-echo/package.json    |  15 +
 .../zeppelin-example-spell-echo/pom.xml         | 116 ++++
 .../zeppelin-example-spell-echo.json            |  28 +
 .../zeppelin-example-spell-flowchart/index.js   | 108 ++++
 .../package.json                                |  17 +
 .../zeppelin-example-spell-flowchart/pom.xml    | 116 ++++
 .../zeppelin-example-spell-flowchart.json       |  28 +
 .../zeppelin-example-spell-markdown/index.js    |  42 ++
 .../package.json                                |  16 +
 .../zeppelin-example-spell-markdown/pom.xml     | 116 ++++
 .../zeppelin-example-spell-markdown.json        |  28 +
 .../zeppelin-example-spell-translator/index.js  |  93 ++++
 .../package.json                                |  16 +
 .../zeppelin-example-spell-translator/pom.xml   | 116 ++++
 .../zeppelin-example-spell-translator.json      |  28 +
 .../zeppelin/helium/ApplicationLoader.java      |   2 +-
 .../apache/zeppelin/helium/HeliumPackage.java   |  25 +-
 .../org/apache/zeppelin/helium/HeliumType.java  |  29 +
 .../zeppelin/helium/SpellPackageInfo.java       |  34 ++
 .../java/org/apache/zeppelin/scheduler/Job.java |  12 +-
 .../zeppelin/helium/ApplicationLoaderTest.java  |   4 +-
 .../zeppelin/helium/HeliumPackageTest.java      |  48 ++
 .../org/apache/zeppelin/rest/HeliumRestApi.java |  21 +-
 .../apache/zeppelin/server/ZeppelinServer.java  |  24 +-
 .../apache/zeppelin/socket/NotebookServer.java  | 260 ++++++---
 .../integration/ParagraphActionsIT.java         |   3 +-
 zeppelin-web/package.json                       |   2 +-
 .../src/app/helium/helium.controller.js         | 379 +++++++------
 zeppelin-web/src/app/helium/helium.css          |  34 +-
 zeppelin-web/src/app/helium/helium.html         |  32 +-
 .../src/app/notebook/notebook.controller.js     |   6 +-
 .../notebook/paragraph/paragraph-control.html   |   2 +-
 .../paragraph-parameterizedQueryForm.html       |   4 +-
 .../notebook/paragraph/paragraph.controller.js  | 367 +++++++++----
 .../src/app/notebook/paragraph/paragraph.html   |   4 +-
 .../paragraph/result/result.controller.js       | 534 +++++++++++--------
 .../app/notebook/paragraph/result/result.html   |  17 +-
 zeppelin-web/src/app/spell/.npmignore           |   1 +
 zeppelin-web/src/app/spell/index.js             |  25 +
 zeppelin-web/src/app/spell/package.json         |  13 +
 zeppelin-web/src/app/spell/spell-base.js        |  48 ++
 zeppelin-web/src/app/spell/spell-result.js      | 275 ++++++++++
 .../src/components/helium/helium-type.js        |  18 +
 .../src/components/helium/helium.service.js     | 115 ++--
 .../websocketEvents/websocketEvents.factory.js  |   2 +
 .../websocketEvents/websocketMsg.service.js     |  25 +
 zeppelin-web/test/spec/controllers/paragraph.js |   4 -
 zeppelin-web/webpack.config.js                  |   2 +-
 .../java/org/apache/zeppelin/helium/Helium.java |  73 +--
 .../zeppelin/helium/HeliumBundleFactory.java    | 415 ++++++++++++++
 .../org/apache/zeppelin/helium/HeliumConf.java  |  12 +-
 .../helium/HeliumVisualizationFactory.java      | 376 -------------
 .../zeppelin/notebook/socket/Message.java       |   4 +-
 .../src/main/resources/helium/package.json      |   2 +-
 .../src/main/resources/helium/webpack.config.js |   2 +-
 .../helium/HeliumApplicationFactoryTest.java    |   8 +-
 .../helium/HeliumBundleFactoryTest.java         | 197 +++++++
 .../helium/HeliumLocalRegistryTest.java         |   2 +-
 .../org/apache/zeppelin/helium/HeliumTest.java  |   5 +-
 .../helium/HeliumVisualizationFactoryTest.java  | 193 -------
 64 files changed, 3243 insertions(+), 1341 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 7772369..a677443 100644
--- a/pom.xml
+++ b/pom.xml
@@ -893,6 +893,7 @@
               <exclude>conf/notebook-authorization.json</exclude>
               <exclude>conf/credentials.json</exclude>
               <exclude>conf/zeppelin-env.sh</exclude>
+              <exclude>conf/helium.json</exclude>
               <exclude>spark-*-bin*/**</exclude>
               <exclude>.spark-dist/**</exclude>
               <exclude>**/interpreter-setting.json</exclude>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-distribution/src/assemble/distribution.xml
----------------------------------------------------------------------
diff --git a/zeppelin-distribution/src/assemble/distribution.xml b/zeppelin-distribution/src/assemble/distribution.xml
index e8188e8..5c369e2 100644
--- a/zeppelin-distribution/src/assemble/distribution.xml
+++ b/zeppelin-distribution/src/assemble/distribution.xml
@@ -103,5 +103,9 @@
       <outputDirectory>/lib/node_modules/zeppelin-tabledata</outputDirectory>
       <directory>../zeppelin-web/src/app/tabledata</directory>
     </fileSet>
+    <fileSet>
+      <outputDirectory>/lib/node_modules/zeppelin-spell</outputDirectory>
+      <directory>../zeppelin-web/src/app/spell</directory>
+    </fileSet>
   </fileSets>
 </assembly>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml
index 300ba57..e9f0473 100644
--- a/zeppelin-examples/pom.xml
+++ b/zeppelin-examples/pom.xml
@@ -36,6 +36,10 @@
   <modules>
     <module>zeppelin-example-clock</module>
     <module>zeppelin-example-horizontalbar</module>
+    <module>zeppelin-example-spell-flowchart</module>
+    <module>zeppelin-example-spell-translator</module>
+    <module>zeppelin-example-spell-markdown</module>
+    <module>zeppelin-example-spell-echo</module>
   </modules>
   
   <build>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-echo/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-echo/index.js b/zeppelin-examples/zeppelin-example-spell-echo/index.js
new file mode 100644
index 0000000..955178e
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-echo/index.js
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+import {
+    SpellBase,
+    SpellResult,
+    DefaultDisplayType,
+} from 'zeppelin-spell';
+
+export default class EchoSpell extends SpellBase {
+    constructor() {
+        super("%echo");
+    }
+
+    interpret(paragraphText) {
+        return new SpellResult(paragraphText);
+    }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-echo/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-echo/package.json b/zeppelin-examples/zeppelin-example-spell-echo/package.json
new file mode 100644
index 0000000..2d9710e
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-echo/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "echo-spell",
+  "description" : "Return just what receive (example)",
+  "version": "1.0.0",
+  "main": "index",
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "zeppelin-spell": "*"
+  },
+  "spell": {
+    "magic": "%echo",
+    "usage": "%echo <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-echo/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml
new file mode 100644
index 0000000..348abd2
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>zeppelin-examples</artifactId>
+    <groupId>org.apache.zeppelin</groupId>
+    <version>0.8.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+
+  <groupId>org.apache.zeppelin</groupId>
+  <artifactId>zeppelin-example-spell-echo</artifactId>
+  <packaging>jar</packaging>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>Zeppelin: Example Spell - Echo</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>zeppelin-interpreter</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>helium-dev</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <version>2.7</version>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.basedir}/../../helium</directory>
+              <includes>
+                <include>${project.artifactId}.json</include>
+              </includes>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>2.7</version>
+        <executions>
+          <execution>
+            <phase>generate-resources</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+
+            <configuration>
+              <outputDirectory>${project.basedir}/../../helium/</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${project.basedir}</directory>
+                  <includes>
+                    <include>${project.artifactId}.json</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json b/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json
new file mode 100644
index 0000000..f267b97
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+{
+  "type" : "SPELL",
+  "name" : "echo-spell",
+  "description" : "Return just what receive (example)",
+  "artifact" : "./zeppelin-examples/zeppelin-example-spell-echo",
+  "license" : "Apache-2.0",
+  "icon" : "<i class='fa fa-repeat'></i>",
+  "spell": {
+    "magic": "%echo",
+    "usage": "%echo <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-flowchart/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/index.js b/zeppelin-examples/zeppelin-example-spell-flowchart/index.js
new file mode 100644
index 0000000..655814a
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-flowchart/index.js
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+import {
+    SpellBase,
+    SpellResult,
+    DefaultDisplayType,
+} from 'zeppelin-spell';
+
+import flowchart from 'flowchart.js';
+
+export default class FlowchartSpell extends SpellBase {
+    constructor() {
+        super("%flowchart");
+    }
+
+    interpret(paragraphText) {
+        /**
+         * `flowchart` library requires an existing DOM to render.
+         * but the DOM is not created yet when `interpret` is called.
+         * so Zeppelin allows to return callback function which accept a DOM element id.
+         * the callback function will executed when the DOM is ready.
+         */
+        const callback = (targetElemId) => {
+            let diagram = flowchart.parse(paragraphText);
+            diagram.drawSVG(targetElemId, this.getOption());
+        };
+
+        /**
+         * `interpret` method can return multiple results using `add()`
+         * but now, we return just 1 result
+         */
+        return new SpellResult(
+            callback
+        );
+    }
+
+    getOption() {
+       return {
+           'x': 0,
+           'y': 0,
+           'line-width': 3,
+           'line-length': 50,
+           'text-margin': 10,
+           'font-size': 14,
+           'font-color': 'black',
+           'line-color': 'black',
+           'element-color': 'black',
+           'fill': 'white',
+           'yes-text': 'yes',
+           'no-text': 'no',
+           'arrow-end': 'block',
+           'scale': 1,
+           // style symbol types
+           'symbols': {
+               'start': {
+                   'font-color': 'red',
+                   'element-color': 'green',
+                   'fill': 'yellow'
+               },
+               'end':{
+                   'class': 'end-element'
+               }
+           },
+           // even flowstate support ;-)
+           'flowstate' : {
+               'past' : { 'fill' : '#CCCCCC', 'font-size' : 12},
+               'current' : {'fill' : 'yellow', 'font-color' : 'red', 'font-weight' : 'bold'},
+               'future' : { 'fill' : '#FFFF99'},
+               'request' : { 'fill' : 'blue'},
+               'invalid': {'fill' : '#444444'},
+               'approved' : { 'fill' : '#58C4A3', 'font-size' : 12, 'yes-text' : 'APPROVED', 'no-text' : 'n/a' },
+               'rejected' : { 'fill' : '#C45879', 'font-size' : 12, 'yes-text' : 'n/a', 'no-text' : 'REJECTED' }
+           }
+       }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-flowchart/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/package.json b/zeppelin-examples/zeppelin-example-spell-flowchart/package.json
new file mode 100644
index 0000000..24be73b
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-flowchart/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "flowchart-spell",
+  "description" : "Draw flowchart using http://flowchart.js.org (example)",
+  "version": "1.0.0",
+  "main": "index",
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "raphael": "2.2.0",
+    "flowchart.js": "^1.6.5",
+    "zeppelin-spell": "*"
+  },
+  "spell": {
+    "magic": "%flowchart",
+    "usage": "%flowchart <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml
new file mode 100644
index 0000000..b3575c9
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>zeppelin-examples</artifactId>
+    <groupId>org.apache.zeppelin</groupId>
+    <version>0.8.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+
+  <groupId>org.apache.zeppelin</groupId>
+  <artifactId>zeppelin-example-spell-flowchart</artifactId>
+  <packaging>jar</packaging>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>Zeppelin: Example Spell - Flowchart</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>zeppelin-interpreter</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>helium-dev</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <version>2.7</version>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.basedir}/../../helium</directory>
+              <includes>
+                <include>${project.artifactId}.json</include>
+              </includes>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>2.7</version>
+        <executions>
+          <execution>
+            <phase>generate-resources</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+
+            <configuration>
+              <outputDirectory>${project.basedir}/../../helium/</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${project.basedir}</directory>
+                  <includes>
+                    <include>${project.artifactId}.json</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json b/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json
new file mode 100644
index 0000000..0ea6e41
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+{
+  "type" : "SPELL",
+  "name" : "flowchart-spell",
+  "description" : "Draw flowchart using http://flowchart.js.org (example)",
+  "artifact" : "./zeppelin-examples/zeppelin-example-spell-flowchart",
+  "license" : "Apache-2.0",
+  "icon" : "<i class='fa fa-random'></i>",
+  "spell": {
+    "magic": "%flowchart",
+    "usage": "%flowchart <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-markdown/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/index.js b/zeppelin-examples/zeppelin-example-spell-markdown/index.js
new file mode 100644
index 0000000..db7959f
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-markdown/index.js
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+import {
+    SpellBase,
+    SpellResult,
+    DefaultDisplayType,
+} from 'zeppelin-spell';
+
+import md from 'markdown';
+
+const markdown = md.markdown;
+
+export default class MarkdownSpell extends SpellBase {
+    constructor() {
+        super("%markdown");
+    }
+
+    interpret(paragraphText) {
+        const parsed = markdown.toHTML(paragraphText);
+
+        /**
+         * specify `DefaultDisplayType.HTML` since `parsed` will contain DOM
+         * otherwise it will be rendered as `DefaultDisplayType.TEXT` (default)
+         */
+        return new SpellResult(parsed, DefaultDisplayType.HTML);
+    }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-markdown/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/package.json b/zeppelin-examples/zeppelin-example-spell-markdown/package.json
new file mode 100644
index 0000000..997a2a2
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-markdown/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "markdown-spell",
+  "description" : "Parse markdown using https://github.com/evilstreak/markdown-js (example)",
+  "version": "1.0.0",
+  "main": "index",
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "markdown": "0.5.0",
+    "zeppelin-spell": "*"
+  },
+  "spell": {
+    "magic": "%markdown",
+    "usage": "%markdown <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml
new file mode 100644
index 0000000..b615ead
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>zeppelin-examples</artifactId>
+    <groupId>org.apache.zeppelin</groupId>
+    <version>0.8.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+
+  <groupId>org.apache.zeppelin</groupId>
+  <artifactId>zeppelin-example-spell-markdown</artifactId>
+  <packaging>jar</packaging>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>Zeppelin: Example Spell - Markdown</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>zeppelin-interpreter</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>helium-dev</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <version>2.7</version>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.basedir}/../../helium</directory>
+              <includes>
+                <include>${project.artifactId}.json</include>
+              </includes>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>2.7</version>
+        <executions>
+          <execution>
+            <phase>generate-resources</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+
+            <configuration>
+              <outputDirectory>${project.basedir}/../../helium/</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${project.basedir}</directory>
+                  <includes>
+                    <include>${project.artifactId}.json</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json b/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json
new file mode 100644
index 0000000..48ad246
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+{
+  "type" : "SPELL",
+  "name" : "markdown-spell",
+  "description" : "Parse markdown using https://github.com/evilstreak/markdown-js (example)",
+  "artifact" : "./zeppelin-examples/zeppelin-example-spell-markdown",
+  "license" : "Apache-2.0",
+  "icon" : "<i class='fa fa-bold'></i>",
+  "spell": {
+    "magic": "%markdown",
+    "usage": "%markdown <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-translator/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-translator/index.js b/zeppelin-examples/zeppelin-example-spell-translator/index.js
new file mode 100644
index 0000000..834e707
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-translator/index.js
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+import {
+    SpellBase,
+    SpellResult,
+    DefaultDisplayType,
+} from 'zeppelin-spell';
+
+import 'whatwg-fetch';
+
+export default class TranslatorSpell extends SpellBase {
+    constructor() {
+        super("%translator");
+    }
+
+    interpret(paragraphText) {
+        const parsed = this.parseConfig(paragraphText);
+        const source = parsed.source;
+        const target = parsed.target;
+        const auth = parsed.auth;
+        const text = parsed.text;
+
+        /**
+         * SpellResult.add()
+         * - accepts not only `string` but also `promise` as a parameter
+         * - allows to add multiple output using the `add()` function
+         */
+        const result = new SpellResult()
+            .add('<h4>Translation Result</h4>', DefaultDisplayType.HTML)
+            // or use display system implicitly like
+            // .add('%html <h4>Translation From English To Korean</h4>')
+            .add(this.translate(source, target, auth, text));
+        return result;
+    }
+
+    parseConfig(text) {
+        const pattern = /^\s*(\S+)-(\S+)\s*(\S+)([\S\s]*)/g;
+        const match = pattern.exec(text);
+
+        if (!match) {
+            throw new Error(`Failed to parse configuration. See README`);
+        }
+
+        return {
+            source: match[1],
+            target: match[2],
+            auth: match[3],
+            text: match[4],
+        }
+    }
+
+    translate(source, target, auth, text) {
+        return fetch('https://translation.googleapis.com/language/translate/v2', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                'Authorization': `Bearer ${auth}`,
+            },
+            body: JSON.stringify({
+                'q': text,
+                'source': source,
+                'target': target,
+                'format': 'text'
+            })
+        }).then(response => {
+            if (response.status === 200) {
+                return response.json()
+            }
+            throw new Error(`https://translation.googleapis.com/language/translate/v2 ${response.status} (${response.statusText})`);
+        }).then((json) => {
+            const extracted = json.data.translations.map(t => {
+                return t.translatedText;
+            });
+            return extracted.join('\n');
+        });
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-translator/package.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-translator/package.json b/zeppelin-examples/zeppelin-example-spell-translator/package.json
new file mode 100644
index 0000000..90624f8
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-translator/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "translator-spell",
+  "description" : "Translate langauges using Google API (examaple)",
+  "version": "1.0.0",
+  "main": "index",
+  "author": "",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "whatwg-fetch": "^2.0.1",
+    "zeppelin-spell": "*"
+  },
+  "spell": {
+    "magic": "%translator",
+    "usage": "%translator <source>-<target> <access-key> <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-translator/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml
new file mode 100644
index 0000000..09e6daa
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <artifactId>zeppelin-examples</artifactId>
+    <groupId>org.apache.zeppelin</groupId>
+    <version>0.8.0-SNAPSHOT</version>
+    <relativePath>..</relativePath>
+  </parent>
+
+  <groupId>org.apache.zeppelin</groupId>
+  <artifactId>zeppelin-example-spell-translator</artifactId>
+  <packaging>jar</packaging>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>Zeppelin: Example Spell - Translator</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>zeppelin-interpreter</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>helium-dev</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <version>2.7</version>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <configuration>
+          <filesets>
+            <fileset>
+              <directory>${project.basedir}/../../helium</directory>
+              <includes>
+                <include>${project.artifactId}.json</include>
+              </includes>
+            </fileset>
+          </filesets>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>2.7</version>
+        <executions>
+          <execution>
+            <phase>generate-resources</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+
+            <configuration>
+              <outputDirectory>${project.basedir}/../../helium/</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${project.basedir}</directory>
+                  <includes>
+                    <include>${project.artifactId}.json</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json
----------------------------------------------------------------------
diff --git a/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json b/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json
new file mode 100644
index 0000000..8f99783
--- /dev/null
+++ b/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+{
+  "type" : "SPELL",
+  "name" : "translator-spell",
+  "description" : "Translate langauges using Google API (examaple)",
+  "artifact" : "./zeppelin-examples/zeppelin-example-spell-translator",
+  "license" : "Apache-2.0",
+  "icon" : "<i class='fa fa-globe '></i>",
+  "spell": {
+    "magic": "%translator",
+    "usage": "%translator <source>-<target> <access-key> <TEXT>"
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationLoader.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationLoader.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationLoader.java
index eacef51..ddd061c 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationLoader.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/ApplicationLoader.java
@@ -102,7 +102,7 @@ public class ApplicationLoader {
    */
   public Application load(HeliumPackage packageInfo, ApplicationContext context)
       throws Exception {
-    if (packageInfo.getType() != HeliumPackage.Type.APPLICATION) {
+    if (packageInfo.getType() != HeliumType.APPLICATION) {
       throw new ApplicationException(
           "Can't instantiate " + packageInfo.getType() + " package using ApplicationLoader");
     }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumPackage.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumPackage.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumPackage.java
index 84a2ab3..e8e6b7c 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumPackage.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumPackage.java
@@ -23,7 +23,7 @@ import org.apache.zeppelin.annotation.Experimental;
  */
 @Experimental
 public class HeliumPackage {
-  private Type type;
+  private HeliumType type;
   private String name;           // user friendly name of this application
   private String description;    // description
   private String artifact;       // artifact name e.g) groupId:artifactId:versionId
@@ -33,17 +33,9 @@ public class HeliumPackage {
   private String license;
   private String icon;
 
-  /**
-   * Type of package
-   */
-  public static enum Type {
-    INTERPRETER,
-    NOTEBOOK_REPO,
-    APPLICATION,
-    VISUALIZATION
-  }
+  public SpellPackageInfo spell;
 
-  public HeliumPackage(Type type,
+  public HeliumPackage(HeliumType type,
                        String name,
                        String description,
                        String artifact,
@@ -76,10 +68,15 @@ public class HeliumPackage {
     return type == info.type && artifact.equals(info.artifact) && className.equals(info.className);
   }
 
-  public Type getType() {
+  public HeliumType getType() {
     return type;
   }
 
+  public static boolean isBundleType(HeliumType type) {
+    return (type == HeliumType.VISUALIZATION ||
+        type == HeliumType.SPELL);
+  }
+
   public String getName() {
     return name;
   }
@@ -106,4 +103,8 @@ public class HeliumPackage {
   public String getIcon() {
     return icon;
   }
+
+  public SpellPackageInfo getSpellInfo() {
+    return spell;
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumType.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumType.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumType.java
new file mode 100644
index 0000000..53360a0
--- /dev/null
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/HeliumType.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+package org.apache.zeppelin.helium;
+
+/**
+ * Type of Helium Package
+ */
+public enum HeliumType {
+  INTERPRETER,
+  NOTEBOOK_REPO,
+  APPLICATION,
+  VISUALIZATION,
+  SPELL
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/SpellPackageInfo.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/SpellPackageInfo.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/SpellPackageInfo.java
new file mode 100644
index 0000000..519d09d
--- /dev/null
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/helium/SpellPackageInfo.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+package org.apache.zeppelin.helium;
+
+/**
+ * Info for Helium Spell Package.
+ */
+public class SpellPackageInfo {
+  private String magic;
+  private String usage;
+
+  public String getMagic() {
+    return magic;
+  }
+
+  public String getUsage() {
+    return usage;
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java
index a690bef..76d90b9 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/scheduler/Job.java
@@ -64,7 +64,6 @@ public abstract class Job {
     }
   }
 
-
   private String jobName;
   String id;
 
@@ -135,6 +134,13 @@ public abstract class Job {
     return status;
   }
 
+  /**
+   * just set status without notifying to listeners for spell.
+   */
+  public void setStatusWithoutNotification(Status status) {
+    this.status = status;
+  }
+
   public void setStatus(Status status) {
     if (this.status == status) {
       return;
@@ -257,4 +263,8 @@ public abstract class Job {
   }
 
   public abstract void setResult(Object results);
+
+  public void setErrorMessage(String errorMessage) {
+    this.errorMessage = errorMessage;
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java
index 3924e28..acb4d7f 100644
--- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java
+++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/ApplicationLoaderTest.java
@@ -20,8 +20,6 @@ package org.apache.zeppelin.helium;
 import org.apache.commons.io.FileUtils;
 import org.apache.zeppelin.dep.DependencyResolver;
 import org.apache.zeppelin.interpreter.InterpreterOutput;
-import org.apache.zeppelin.interpreter.InterpreterOutputListener;
-import org.apache.zeppelin.interpreter.InterpreterResultMessageOutput;
 import org.apache.zeppelin.resource.LocalResourcePool;
 import org.junit.After;
 import org.junit.Before;
@@ -74,7 +72,7 @@ public class ApplicationLoaderTest {
 
   public HeliumPackage createPackageInfo(String className, String artifact) {
     HeliumPackage app1 = new HeliumPackage(
-        HeliumPackage.Type.APPLICATION,
+        HeliumType.APPLICATION,
         "name1",
         "desc1",
         artifact,

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/HeliumPackageTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/HeliumPackageTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/HeliumPackageTest.java
new file mode 100644
index 0000000..aadae41
--- /dev/null
+++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/helium/HeliumPackageTest.java
@@ -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.
+ */
+
+package org.apache.zeppelin.helium;
+
+import com.google.gson.Gson;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class HeliumPackageTest {
+
+  private Gson gson = new Gson();
+
+  @Test
+  public void parseSpellPackageInfo() {
+    String exampleSpell = "{\n" +
+        "  \"type\" : \"SPELL\",\n" +
+        "  \"name\" : \"echo-spell\",\n" +
+        "  \"description\" : \"'%echo' - return just what receive (example)\",\n" +
+        "  \"artifact\" : \"./zeppelin-examples/zeppelin-example-spell-echo\",\n" +
+        "  \"license\" : \"Apache-2.0\",\n" +
+        "  \"icon\" : \"<i class='fa fa-repeat'></i>\",\n" +
+        "  \"spell\": {\n" +
+        "    \"magic\": \"%echo\",\n" +
+        "    \"usage\": \"%echo <TEXT>\"\n" +
+        "  }\n" +
+        "}";
+
+    HeliumPackage p = gson.fromJson(exampleSpell, HeliumPackage.class);
+    assertEquals(p.getSpellInfo().getMagic(), "%echo");
+    assertEquals(p.getSpellInfo().getUsage(), "%echo <TEXT>");
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java
index e5cf70d..c318be5 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/HeliumRestApi.java
@@ -17,7 +17,6 @@
 
 package org.apache.zeppelin.rest;
 
-import com.github.eirslett.maven.plugins.frontend.lib.TaskRunnerException;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import org.apache.commons.io.FileUtils;
@@ -107,22 +106,22 @@ public class HeliumRestApi {
   }
 
   @GET
-  @Path("visualizations/load")
+  @Path("bundle/load")
   @Produces("text/javascript")
-  public Response visualizationLoad(@QueryParam("refresh") String refresh) {
+  public Response bundleLoad(@QueryParam("refresh") String refresh) {
     try {
       File bundle;
       if (refresh != null && refresh.equals("true")) {
-        bundle = helium.recreateVisualizationBundle();
+        bundle = helium.recreateBundle();
       } else {
-        bundle = helium.getVisualizationFactory().getCurrentBundle();
+        bundle = helium.getBundleFactory().getCurrentCacheBundle();
       }
 
       if (bundle == null) {
         return Response.ok().build();
       } else {
-        String visBundle = FileUtils.readFileToString(bundle);
-        return Response.ok(visBundle).build();
+        String stringifiedBundle = FileUtils.readFileToString(bundle);
+        return Response.ok(stringifiedBundle).build();
       }
     } catch (Exception e) {
       logger.error(e.getMessage(), e);
@@ -160,15 +159,15 @@ public class HeliumRestApi {
   }
 
   @GET
-  @Path("visualizationOrder")
+  @Path("order/visualization")
   public Response getVisualizationPackageOrder() {
-    List<String> order = helium.getVisualizationPackageOrder();
+    List<String> order = helium.setVisualizationPackageOrder();
     return new JsonResponse(Response.Status.OK, order).build();
   }
 
   @POST
-  @Path("visualizationOrder")
-  public Response setVisualizationPackageOrder(String orderedPackageNameList) {
+  @Path("order/visualization")
+  public Response getVisualizationPackageOrder(String orderedPackageNameList) {
     List<String> orderedList = gson.fromJson(
         orderedPackageNameList, new TypeToken<List<String>>(){}.getType());
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
index 6c4fcd8..371d0a1 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
@@ -35,7 +35,7 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars;
 import org.apache.zeppelin.dep.DependencyResolver;
 import org.apache.zeppelin.helium.Helium;
 import org.apache.zeppelin.helium.HeliumApplicationFactory;
-import org.apache.zeppelin.helium.HeliumVisualizationFactory;
+import org.apache.zeppelin.helium.HeliumBundleFactory;
 import org.apache.zeppelin.interpreter.InterpreterFactory;
 import org.apache.zeppelin.interpreter.InterpreterOutput;
 import org.apache.zeppelin.notebook.Notebook;
@@ -102,7 +102,7 @@ public class ZeppelinServer extends Application {
     InterpreterOutput.limit = conf.getInt(ConfVars.ZEPPELIN_INTERPRETER_OUTPUT_LIMIT);
 
     HeliumApplicationFactory heliumApplicationFactory = new HeliumApplicationFactory();
-    HeliumVisualizationFactory heliumVisualizationFactory;
+    HeliumBundleFactory heliumBundleFactory;
 
     if (isBinaryPackage(conf)) {
       /* In binary package, zeppelin-web/src/app/visualization and zeppelin-web/src/app/tabledata
@@ -110,28 +110,30 @@ public class ZeppelinServer extends Application {
        * Check zeppelin/zeppelin-distribution/src/assemble/distribution.xml to see how they're
        * packaged into binary package.
        */
-      heliumVisualizationFactory = new HeliumVisualizationFactory(
+      heliumBundleFactory = new HeliumBundleFactory(
           new File(conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)),
           new File(conf.getRelativeDir("lib/node_modules/zeppelin-tabledata")),
-          new File(conf.getRelativeDir("lib/node_modules/zeppelin-vis")));
+          new File(conf.getRelativeDir("lib/node_modules/zeppelin-vis")),
+          new File(conf.getRelativeDir("lib/node_modules/zeppelin-spell")));
     } else {
-      heliumVisualizationFactory = new HeliumVisualizationFactory(
+      heliumBundleFactory = new HeliumBundleFactory(
           new File(conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO)),
           new File(conf.getRelativeDir("zeppelin-web/src/app/tabledata")),
-          new File(conf.getRelativeDir("zeppelin-web/src/app/visualization")));
+          new File(conf.getRelativeDir("zeppelin-web/src/app/visualization")),
+          new File(conf.getRelativeDir("zeppelin-web/src/app/spell")));
     }
 
     this.helium = new Helium(
         conf.getHeliumConfPath(),
         conf.getHeliumRegistry(),
-        new File(
-            conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO), "helium_registry_cache"),
-        heliumVisualizationFactory,
+        new File(conf.getRelativeDir(ConfVars.ZEPPELIN_DEP_LOCALREPO),
+            "helium-registry-cache"),
+        heliumBundleFactory,
         heliumApplicationFactory);
 
-    // create visualization bundle
+    // create bundle
     try {
-      heliumVisualizationFactory.bundle(helium.getVisualizationPackagesToBundle());
+      heliumBundleFactory.buildBundle(helium.getBundlePackagesToBundle());
     } catch (Exception e) {
       LOG.error(e.getMessage(), e);
     }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 6e58e3d..68b015d 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -262,6 +262,9 @@ public class NotebookServer extends WebSocketServlet
           case RUN_PARAGRAPH:
             runParagraph(conn, userAndRoles, notebook, messagereceived);
             break;
+          case PARAGRAPH_EXECUTED_BY_SPELL:
+            broadcastSpellExecution(conn, userAndRoles, notebook, messagereceived);
+            break;
           case RUN_ALL_PARAGRAPHS:
             runAllParagraphs(conn, userAndRoles, notebook, messagereceived);
             break;
@@ -698,6 +701,63 @@ public class NotebookServer extends WebSocketServlet
             .toString())));
   }
 
+  /**
+   * @return false if user doesn't have reader permission for this paragraph
+   */
+  private boolean hasParagraphReaderPermission(NotebookSocket conn,
+                                              Notebook notebook, String noteId,
+                                              HashSet<String> userAndRoles,
+                                              String principal, String op)
+      throws IOException {
+
+    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
+    if (!notebookAuthorization.isReader(noteId, userAndRoles)) {
+      permissionError(conn, op, principal, userAndRoles,
+          notebookAuthorization.getOwners(noteId));
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * @return false if user doesn't have writer permission for this paragraph
+   */
+  private boolean hasParagraphWriterPermission(NotebookSocket conn,
+                                               Notebook notebook, String noteId,
+                                               HashSet<String> userAndRoles,
+                                               String principal, String op)
+      throws IOException {
+
+    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
+    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
+      permissionError(conn, op, principal, userAndRoles,
+          notebookAuthorization.getOwners(noteId));
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * @return false if user doesn't have owner permission for this paragraph
+   */
+  private boolean hasParagraphOwnerPermission(NotebookSocket conn,
+                                              Notebook notebook, String noteId,
+                                              HashSet<String> userAndRoles,
+                                              String principal, String op)
+      throws IOException {
+
+    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
+    if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
+      permissionError(conn, op, principal, userAndRoles,
+          notebookAuthorization.getOwners(noteId));
+      return false;
+    }
+
+    return true;
+  }
+
   private void sendNote(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
       Message fromMessage) throws IOException {
 
@@ -713,13 +773,13 @@ public class NotebookServer extends WebSocketServlet
     String user = fromMessage.principal;
 
     Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
     if (note != null) {
-      if (!notebookAuthorization.isReader(noteId, userAndRoles)) {
-        permissionError(conn, "read", fromMessage.principal, userAndRoles,
-            notebookAuthorization.getReaders(noteId));
+
+      if (!hasParagraphReaderPermission(conn, notebook, noteId,
+          userAndRoles, fromMessage.principal, "read")) {
         return;
       }
+
       addConnectionToNote(note.getId(), conn);
 
       if (note.isPersonalizedMode()) {
@@ -743,12 +803,11 @@ public class NotebookServer extends WebSocketServlet
     }
 
     if (note != null) {
-      NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-      if (!notebookAuthorization.isReader(noteId, userAndRoles)) {
-        permissionError(conn, "read", fromMessage.principal, userAndRoles,
-            notebookAuthorization.getReaders(noteId));
+      if (!hasParagraphReaderPermission(conn, notebook, noteId,
+          userAndRoles, fromMessage.principal, "read")) {
         return;
       }
+
       addConnectionToNote(note.getId(), conn);
       conn.send(serializeMessage(new Message(OP.NOTE).put("note", note)));
       sendAllAngularObjects(note, user, conn);
@@ -770,10 +829,8 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "update", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "update")) {
       return;
     }
 
@@ -804,10 +861,8 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-      permissionError(conn, "persoanlized ", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getOwners(noteId));
+    if (!hasParagraphOwnerPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "persoanlized")) {
       return;
     }
 
@@ -836,10 +891,8 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-      permissionError(conn, "rename", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getOwners(noteId));
+    if (!hasParagraphOwnerPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "rename")) {
       return;
     }
 
@@ -870,12 +923,10 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
     for (Note note : notebook.getNotesUnderFolder(oldFolderId)) {
       String noteId = note.getId();
-      if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-        permissionError(conn, op + " folder of '" + note.getName() + "'", fromMessage.principal,
-                userAndRoles, notebookAuthorization.getOwners(noteId));
+      if (!hasParagraphOwnerPermission(conn, notebook, noteId,
+          userAndRoles, fromMessage.principal, op + " folder of '" + note.getName() + "'")) {
         return;
       }
     }
@@ -960,11 +1011,8 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-      permissionError(conn, "remove", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getOwners(noteId));
+    if (!hasParagraphOwnerPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "remove")) {
       return;
     }
 
@@ -982,13 +1030,12 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
     List<Note> notes = notebook.getNotesUnderFolder(folderId);
     for (Note note : notes) {
       String noteId = note.getId();
-      if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-        permissionError(conn, "remove folder of '" + note.getName() + "'", fromMessage.principal,
-                userAndRoles, notebookAuthorization.getOwners(noteId));
+
+      if (!hasParagraphOwnerPermission(conn, notebook, noteId,
+          userAndRoles, fromMessage.principal, "remove folder of '" + note.getName() + "'")) {
         return;
       }
     }
@@ -1107,17 +1154,16 @@ public class NotebookServer extends WebSocketServlet
     Map<String, Object> params = (Map<String, Object>) fromMessage.get("params");
     Map<String, Object> config = (Map<String, Object>) fromMessage.get("config");
     String noteId = getOpenNoteId(conn);
-    final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
 
+    final Note note = notebook.getNote(noteId);
     Paragraph p = note.getParagraph(paragraphId);
 
+    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
     if (note.isPersonalizedMode()) {
       p = p.getUserParagraphMap().get(subject.getUser());
     }
@@ -1154,14 +1200,13 @@ public class NotebookServer extends WebSocketServlet
     if (StringUtils.isBlank(noteId)) {
       return;
     }
-    Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "clear output", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getOwners(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "clear output")) {
       return;
     }
 
+    Note note = notebook.getNote(noteId);
     note.clearAllParagraphOutput();
     broadcastNote(note);
   }
@@ -1193,17 +1238,16 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
     String noteId = getOpenNoteId(conn);
-    final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
 
     /** We dont want to remove the last paragraph */
+    final Note note = notebook.getNote(noteId);
     if (!note.isLastParagraph(paragraphId)) {
+      AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
       Paragraph para = note.removeParagraph(subject.getUser(), paragraphId);
       note.persist(subject);
       if (para != null) {
@@ -1219,14 +1263,14 @@ public class NotebookServer extends WebSocketServlet
     if (paragraphId == null) {
       return;
     }
+
     String noteId = getOpenNoteId(conn);
-    final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
+
+    final Note note = notebook.getNote(noteId);
     note.clearParagraphOutput(paragraphId);
     Paragraph paragraph = note.getParagraph(paragraphId);
     broadcastParagraph(note, paragraph);
@@ -1470,14 +1514,13 @@ public class NotebookServer extends WebSocketServlet
     final int newIndex = (int) Double.parseDouble(fromMessage.get("index").toString());
     String noteId = getOpenNoteId(conn);
     final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
 
+    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
     note.moveParagraph(paragraphId, newIndex);
     note.persist(subject);
     broadcast(note.getId(),
@@ -1489,11 +1532,10 @@ public class NotebookServer extends WebSocketServlet
     final int index = (int) Double.parseDouble(fromMessage.get("index").toString());
     String noteId = getOpenNoteId(conn);
     final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
     AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return null;
     }
 
@@ -1524,14 +1566,13 @@ public class NotebookServer extends WebSocketServlet
     }
 
     String noteId = getOpenNoteId(conn);
-    final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
 
+    final Note note = notebook.getNote(noteId);
     Paragraph p = note.getParagraph(paragraphId);
     p.abort();
   }
@@ -1544,11 +1585,8 @@ public class NotebookServer extends WebSocketServlet
       return;
     }
 
-    Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "run all paragraphs", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getOwners(noteId));
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "run all paragraphs")) {
       return;
     }
 
@@ -1567,6 +1605,7 @@ public class NotebookServer extends WebSocketServlet
       Map<String, Object> params = (Map<String, Object>) raw.get("params");
       Map<String, Object> config = (Map<String, Object>) raw.get("config");
 
+      Note note = notebook.getNote(noteId);
       Paragraph p = setParagraphUsingMessage(note, fromMessage,
           paragraphId, text, title, params, config);
 
@@ -1574,6 +1613,45 @@ public class NotebookServer extends WebSocketServlet
     }
   }
 
+  private void broadcastSpellExecution(NotebookSocket conn, HashSet<String> userAndRoles,
+                                       Notebook notebook, Message fromMessage)
+      throws IOException {
+
+    final String paragraphId = (String) fromMessage.get("id");
+    if (paragraphId == null) {
+      return;
+    }
+
+    String noteId = getOpenNoteId(conn);
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
+      return;
+    }
+
+    String text = (String) fromMessage.get("paragraph");
+    String title = (String) fromMessage.get("title");
+    Status status = Status.valueOf((String) fromMessage.get("status"));
+    Map<String, Object> params = (Map<String, Object>) fromMessage.get("params");
+    Map<String, Object> config = (Map<String, Object>) fromMessage.get("config");
+
+    final Note note = notebook.getNote(noteId);
+    Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId,
+        text, title, params, config);
+    p.setResult(fromMessage.get("results"));
+    p.setErrorMessage((String) fromMessage.get("errorMessage"));
+    p.setStatusWithoutNotification(status);
+
+    addNewParagraphIfLastParagraphIsExecuted(note, p);
+    if (!persistNoteWithAuthInfo(conn, note, p)) {
+      return;
+    }
+
+    // broadcast to other clients only
+    broadcastExcept(note.getId(),
+        new Message(OP.RUN_PARAGRAPH_USING_SPELL).put("paragraph", p), conn);
+  }
+
   private void runParagraph(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
                             Message fromMessage) throws IOException {
     final String paragraphId = (String) fromMessage.get("id");
@@ -1582,11 +1660,9 @@ public class NotebookServer extends WebSocketServlet
     }
 
     String noteId = getOpenNoteId(conn);
-    final Note note = notebook.getNote(noteId);
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "write", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "write")) {
       return;
     }
 
@@ -1594,14 +1670,15 @@ public class NotebookServer extends WebSocketServlet
     String title = (String) fromMessage.get("title");
     Map<String, Object> params = (Map<String, Object>) fromMessage.get("params");
     Map<String, Object> config = (Map<String, Object>) fromMessage.get("config");
+
+    final Note note = notebook.getNote(noteId);
     Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId,
         text, title, params, config);
 
     persistAndExecuteSingleParagraph(conn, note, p);
   }
 
-  private void persistAndExecuteSingleParagraph(NotebookSocket conn,
-                                                Note note, Paragraph p) throws IOException {
+  private void addNewParagraphIfLastParagraphIsExecuted(Note note, Paragraph p) {
     // if it's the last paragraph and empty, let's add a new one
     boolean isTheLastParagraph = note.isLastParagraph(p.getId());
     if (!(p.getText().trim().equals(p.getMagic()) ||
@@ -1610,15 +1687,30 @@ public class NotebookServer extends WebSocketServlet
       Paragraph newPara = note.addParagraph(p.getAuthenticationInfo());
       broadcastNewParagraph(note, newPara);
     }
+  }
 
+  /**
+   * @return false if failed to save a note
+   */
+  private boolean persistNoteWithAuthInfo(NotebookSocket conn,
+                                          Note note, Paragraph p) throws IOException {
     try {
       note.persist(p.getAuthenticationInfo());
+      return true;
     } catch (FileSystemException ex) {
       LOG.error("Exception from run", ex);
       conn.send(serializeMessage(new Message(OP.ERROR_INFO).put("info",
           "Oops! There is something wrong with the notebook file system. "
               + "Please check the logs for more details.")));
       // don't run the paragraph when there is error on persisting the note information
+      return false;
+    }
+  }
+
+  private void persistAndExecuteSingleParagraph(NotebookSocket conn,
+                                                Note note, Paragraph p) throws IOException {
+    addNewParagraphIfLastParagraphIsExecuted(note, p);
+    if (!persistNoteWithAuthInfo(conn, note, p)) {
       return;
     }
 
@@ -1701,10 +1793,8 @@ public class NotebookServer extends WebSocketServlet
     String revisionId = (String) fromMessage.get("revisionId");
     AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
 
-    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
-    if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
-      permissionError(conn, "update", fromMessage.principal, userAndRoles,
-          notebookAuthorization.getWriters(noteId));
+    if (!hasParagraphWriterPermission(conn, notebook, noteId,
+        userAndRoles, fromMessage.principal, "update")) {
       return;
     }
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/0589e27e/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java
index feade7f..458b8d4 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java
@@ -270,7 +270,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
       collector.checkThat("Before Run Output field contains ",
           driver.findElements(By.xpath(xpathToOutputField)).size(),
           CoreMatchers.equalTo(0));
-      driver.findElement(By.xpath(getParagraphXPath(1) + "//span[@ng-click='runParagraph(getEditorValue())']")).click();
+      runParagraph(1);
       waitForParagraph(1, "FINISHED");
       collector.checkThat("After Run Output field contains  ",
           driver.findElement(By.xpath(xpathToOutputField)).getText(),
@@ -286,7 +286,6 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
     } catch (Exception e) {
       handleException("Exception in ParagraphActionsIT while testClearOutputButton ", e);
     }
-
   }
 
   @Test


[2/3] zeppelin git commit: [ZEPPELIN-2008] Introduce Spell

Posted by mo...@apache.org.
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',