From c02d00a473410070cb20db55718dfa8567585c28 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 17 Jan 2017 19:40:12 +0900 Subject: [PATCH 01/29] feat: Support FRONTEND_INTERPRETER type in backend --- .../src/assemble/distribution.xml | 4 + .../apache/zeppelin/helium/HeliumPackage.java | 8 +- .../apache/zeppelin/rest/HeliumRestApi.java | 22 ++-- .../zeppelin/server/ZeppelinServer.java | 24 ++-- zeppelin-web/package.json | 2 +- zeppelin-web/webpack.config.js | 2 +- .../org/apache/zeppelin/helium/Helium.java | 67 +++++----- ...nFactory.java => HeliumBundleFactory.java} | 115 +++++++++++------- .../apache/zeppelin/helium/HeliumConf.java | 12 +- .../src/main/resources/helium/package.json | 2 +- .../main/resources/helium/webpack.config.js | 2 +- ...Test.java => HeliumBundleFactoryTest.java} | 34 +++--- 12 files changed, 171 insertions(+), 123 deletions(-) rename zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/{HeliumVisualizationFactory.java => HeliumBundleFactory.java} (81%) rename zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/{HeliumVisualizationFactoryTest.java => HeliumBundleFactoryTest.java} (85%) diff --git a/zeppelin-distribution/src/assemble/distribution.xml b/zeppelin-distribution/src/assemble/distribution.xml index e8188e878d3..913b19f13dd 100644 --- a/zeppelin-distribution/src/assemble/distribution.xml +++ b/zeppelin-distribution/src/assemble/distribution.xml @@ -103,5 +103,9 @@ /lib/node_modules/zeppelin-tabledata ../zeppelin-web/src/app/tabledata + + /lib/node_modules/zeppelin-frontend-interpreter + ../zeppelin-web/src/app/frontend-interpreter + 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 84a2ab33b3d..03d063474c3 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 @@ -40,7 +40,8 @@ public static enum Type { INTERPRETER, NOTEBOOK_REPO, APPLICATION, - VISUALIZATION + VISUALIZATION, + FRONTEND_INTERPRETER } public HeliumPackage(Type type, @@ -80,6 +81,11 @@ public Type getType() { return type; } + public static boolean isBundleType(Type type) { + return (type == Type.VISUALIZATION || + type == Type.FRONTEND_INTERPRETER); + } + public String getName() { return name; } 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 e5cf70d395e..a3d54e4062c 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 @@ -107,22 +107,22 @@ public Response suggest(@PathParam("noteId") String noteId, } @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 +160,15 @@ public Response enablePackage(@PathParam("packageName") String packageName) { } @GET - @Path("visualizationOrder") - public Response getVisualizationPackageOrder() { - List order = helium.getVisualizationPackageOrder(); + @Path("bundleOrder") + public Response getBundlePackageOrder() { + List order = helium.getBundlePackageOrder(); return new JsonResponse(Response.Status.OK, order).build(); } @POST - @Path("visualizationOrder") - public Response setVisualizationPackageOrder(String orderedPackageNameList) { + @Path("bundleOrder") + public Response setBundlePackageOrder(String orderedPackageNameList) { List orderedList = gson.fromJson( orderedPackageNameList, new TypeToken>(){}.getType()); 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 6c4fcd83e91..eab9eb77edf 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.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 ZeppelinServer() throws Exception { 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 ZeppelinServer() throws Exception { * 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-frontend-interpreter"))); } 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/frontend-interpreter"))); } 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); } diff --git a/zeppelin-web/package.json b/zeppelin-web/package.json index 0ad639815c1..c9bba37c6e3 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", diff --git a/zeppelin-web/webpack.config.js b/zeppelin-web/webpack.config.js index d3a2681dfce..8198b5530aa 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 } }) ) 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 0ef3237fec6..49fba33975e 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 HeliumApplicationFactory getApplicationFactory() { return applicationFactory; } - public HeliumVisualizationFactory getVisualizationFactory() { - return visualizationFactory; + public HeliumBundleFactory getBundleFactory() { + return bundleFactory; } private synchronized HeliumConf loadConf(String path) throws IOException { @@ -145,14 +145,14 @@ private void clearNotExistsPackages() { Map> all = getAllPackageInfo(); // clear visualization display order - List packageOrder = heliumConf.getVisualizationDisplayOrder(); + List packageOrder = heliumConf.getBundleDisplayOrder(); List clearedOrder = new LinkedList<>(); for (String pkgName : packageOrder) { if (all.containsKey(pkgName)) { clearedOrder.add(pkgName); } } - heliumConf.setVisualizationDisplayOrder(clearedOrder); + heliumConf.setBundleDisplayOrder(clearedOrder); // clear enabled package Map enabledPackages = heliumConf.getEnabledPackages(); @@ -215,8 +215,8 @@ public HeliumPackageSearchResult getPackageInfo(String name, String artifact) { 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 void enable(String name, String artifact) throws IOException { 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 void disable(String name) throws IOException { 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(); @@ -299,15 +299,15 @@ public HeliumPackageSuggestion suggestApp(Paragraph paragraph) { } /** - * Get enabled visualization packages + * Get enabled buildBundle packages * - * @return ordered list of enabled visualization package + * @return ordered list of enabled buildBundle package */ - public List getVisualizationPackagesToBundle() { + public List getBundlePackagesToBundle() { Map> allPackages = getAllPackageInfo(); - List visOrder = heliumConf.getVisualizationDisplayOrder(); + List visOrder = heliumConf.getBundleDisplayOrder(); - List orderedVisualizationPackages = new LinkedList<>(); + List orderedBundlePackages = new LinkedList<>(); // add enabled packages in visOrder for (String name : visOrder) { @@ -316,8 +316,8 @@ public List getVisualizationPackagesToBundle() { 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,25 +325,30 @@ public List getVisualizationPackagesToBundle() { } // add enabled packages not in visOrder - for (List pkgs : allPackages.values()) { - for (HeliumPackageSearchResult pkg : pkgs) { - if (pkg.getPkg().getType() == HeliumPackage.Type.VISUALIZATION && pkg.isEnabled()) { - orderedVisualizationPackages.add(pkg.getPkg()); + for (List 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 getVisualizationPackageOrder() { + public List getBundlePackageOrder() { List orderedPackageList = new LinkedList<>(); - List packages = getVisualizationPackagesToBundle(); + List packages = getBundlePackagesToBundle(); for (HeliumPackage pkg : packages) { orderedPackageList.add(pkg.getName()); @@ -354,10 +359,10 @@ public List getVisualizationPackageOrder() { public void setVisualizationPackageOrder(List 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(); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java similarity index 81% rename from zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java rename to zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java index 624f12aafda..23998c77e34 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumVisualizationFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java @@ -36,40 +36,47 @@ /** * Load helium visualization */ -public class HeliumVisualizationFactory { - Logger logger = LoggerFactory.getLogger(HeliumVisualizationFactory.class); +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 frontendInterpreterModulePath; private Gson gson; String bundleCacheKey = ""; - File currentBundle; + File currentCacheBundle; ByteArrayOutputStream out = new ByteArrayOutputStream(); - public HeliumVisualizationFactory( + public HeliumBundleFactory( File moduleDownloadPath, File tabledataModulePath, - File visualizationModulePath) throws TaskRunnerException { + File visualizationModulePath, + File frontendInterpreterModulePath) throws TaskRunnerException { this(moduleDownloadPath); this.tabledataModulePath = tabledataModulePath; this.visualizationModulePath = visualizationModulePath; + this.frontendInterpreterModulePath = frontendInterpreterModulePath; } - public HeliumVisualizationFactory(File moduleDownloadPath) throws TaskRunnerException { - this.workingDirectory = new File(moduleDownloadPath, "vis"); + public HeliumBundleFactory(File moduleDownloadPath) throws TaskRunnerException { + this.workingDirectory = new File(moduleDownloadPath, HELIUM_LOCAL_REPO); File installDirectory = workingDirectory; frontEndPluginFactory = new FrontendPluginFactory( workingDirectory, installDirectory); - currentBundle = new File(workingDirectory, "vis.bundle.cache.js"); + currentCacheBundle = new File(workingDirectory, HELIUM_BUNDLE_CACHE); gson = new Gson(); installNodeAndNpm(); configureLogger(); @@ -94,11 +101,11 @@ private ProxyConfig getProxyConfig() { return new ProxyConfig(proxy); } - public File bundle(List pkgs) throws IOException { - return bundle(pkgs, false); + public File buildBundle(List pkgs) throws IOException { + return buildBundle(pkgs, false); } - public synchronized File bundle(List pkgs, boolean forceRefresh) + public synchronized File buildBundle(List pkgs, boolean forceRefresh) throws IOException { // package.json URL pkgUrl = Resources.getResource("helium/package.json"); @@ -144,10 +151,10 @@ public boolean accept(File pathname) { } 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; + // check if we can use previous buildBundle or not + if (cacheKeyBuilder.toString().equals(bundleCacheKey) && + currentCacheBundle.isFile() && !forceRefresh) { + return currentCacheBundle; } // webpack.config.js @@ -165,14 +172,15 @@ public boolean accept(File pathname) { continue; } - String className = "vis" + idx++; + String className = "bundles" + idx++; loadJsImport.append( "import " + className + " from \"" + moduleNameVersion[0] + "\"\n"); - loadJsRegister.append("visualizations.push({\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"); } @@ -182,6 +190,38 @@ public boolean accept(File pathname) { FileUtils.write(new File(workingDirectory, "load.js"), loadJsImport.append(loadJsRegister).toString()); + copyFrameworkModuleToInstallPath(npmPackageCopyFilter); + + out.reset(); + try { + npmCommand("install"); + npmCommand("run bundle"); + } catch (TaskRunnerException e) { + throw new IOException(new String(out.toByteArray())); + } + + File heliumBundle = new File(workingDirectory, HELIUM_BUNDLE); + if (!heliumBundle.isFile()) { + throw new IOException( + "Can't create bundle: \n" + new String(out.toByteArray())); + } + + WebpackResult result = getWebpackResultFromOutput(new String(out.toByteArray())); + 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"); @@ -212,32 +252,19 @@ public boolean accept(File pathname) { 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]); - } + // install frontend-interpreter module + File frontendInterpreterModuleInstallPath = new File(workingDirectory, + "node_modules/zeppelin-frontend-interpreter"); + if (frontendInterpreterModulePath != null) { + if (frontendInterpreterModuleInstallPath.exists()) { + FileUtils.deleteDirectory(frontendInterpreterModuleInstallPath); + } - synchronized (this) { - currentBundle.delete(); - FileUtils.moveFile(visBundleJs, currentBundle); - bundleCacheKey = cacheKeyBuilder.toString(); + FileUtils.copyDirectory( + frontendInterpreterModulePath, + frontendInterpreterModuleInstallPath, + npmPackageCopyFilter); } - return currentBundle; } private WebpackResult getWebpackResultFromOutput(String output) { @@ -277,10 +304,10 @@ private WebpackResult getWebpackResultFromOutput(String output) { } } - public File getCurrentBundle() { + public File getCurrentCacheBundle() { synchronized (this) { - if (currentBundle.isFile()) { - return currentBundle; + if (currentCacheBundle.isFile()) { + return currentCacheBundle; } else { return null; } 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 5094934bb6f..d60aec76b49 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 enabled = Collections.synchronizedMap(new HashMap()); // enabled visualization package display order - List visualizationDisplayOrder = new LinkedList<>(); + List bundleDisplayOrder = new LinkedList<>(); public Map getEnabledPackages() { return new HashMap<>(enabled); @@ -48,15 +48,15 @@ public void disablePackage(String name) { enabled.remove(name); } - public List getVisualizationDisplayOrder() { - if (visualizationDisplayOrder == null) { + public List getBundleDisplayOrder() { + if (bundleDisplayOrder == null) { return new LinkedList(); } else { - return visualizationDisplayOrder; + return bundleDisplayOrder; } } - public void setVisualizationDisplayOrder(List orderedPackageList) { - visualizationDisplayOrder = orderedPackageList; + public void setBundleDisplayOrder(List orderedPackageList) { + bundleDisplayOrder = orderedPackageList; } } diff --git a/zeppelin-zengine/src/main/resources/helium/package.json b/zeppelin-zengine/src/main/resources/helium/package.json index e6ec612aa36..cd4e470f287 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" diff --git a/zeppelin-zengine/src/main/resources/helium/webpack.config.js b/zeppelin-zengine/src/main/resources/helium/webpack.config.js index 2b5015e47a4..c318c107d30 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" diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java similarity index 85% rename from zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java rename to zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java index e5a61edb381..8cac5ed4c43 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumVisualizationFactoryTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java @@ -32,9 +32,9 @@ import static org.junit.Assert.*; -public class HeliumVisualizationFactoryTest { +public class HeliumBundleFactoryTest { private File tmpDir; - private HeliumVisualizationFactory hvf; + private HeliumBundleFactory hbf; @Before public void setUp() throws InstallationException, TaskRunnerException { @@ -46,9 +46,10 @@ public void setUp() throws InstallationException, TaskRunnerException { String resDir = new File(res.getFile()).getParent(); File moduleDir = new File(resDir + "/../../../../zeppelin-web/src/app/"); - hvf = new HeliumVisualizationFactory(tmpDir, + hbf = new HeliumBundleFactory(tmpDir, new File(moduleDir, "tabledata"), - new File(moduleDir, "visualization")); + new File(moduleDir, "visualization"), + new File(moduleDir, "frontend-interpreter")); } @After @@ -58,8 +59,10 @@ public void tearDown() throws IOException { @Test public void testInstallNpm() throws InstallationException { - assertTrue(new File(tmpDir, "vis/node/npm").isFile()); - assertTrue(new File(tmpDir, "vis/node/node").isFile()); + assertTrue(new File(tmpDir, + HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node/npm").isFile()); + assertTrue(new File(tmpDir, + HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node/node").isFile()); } @Test @@ -74,8 +77,9 @@ public void downloadPackage() throws TaskRunnerException { "license", "icon" ); - hvf.install(pkg); - assertTrue(new File(tmpDir, "vis/node_modules/lodash").isDirectory()); + hbf.install(pkg); + assertTrue(new File(tmpDir, + HeliumBundleFactory.HELIUM_LOCAL_REPO + "/node_modules/lodash").isDirectory()); } @Test @@ -92,12 +96,12 @@ public void bundlePackage() throws IOException, TaskRunnerException { ); List pkgs = new LinkedList<>(); pkgs.add(pkg); - File bundle = hvf.bundle(pkgs); + File bundle = hbf.buildBundle(pkgs); assertTrue(bundle.isFile()); long lastModified = bundle.lastModified(); - // bundle again and check if it served from cache - bundle = hvf.bundle(pkgs); + // buildBundle again and check if it served from cache + bundle = hbf.buildBundle(pkgs); assertEquals(lastModified, bundle.lastModified()); } @@ -120,7 +124,7 @@ public void bundleLocalPackage() throws IOException, TaskRunnerException { ); List pkgs = new LinkedList<>(); pkgs.add(pkg); - File bundle = hvf.bundle(pkgs); + File bundle = hbf.buildBundle(pkgs); assertTrue(bundle.isFile()); } @@ -144,7 +148,7 @@ public void bundleErrorPropagation() throws IOException, TaskRunnerException { pkgs.add(pkg); File bundle = null; try { - bundle = hvf.bundle(pkgs); + bundle = hbf.buildBundle(pkgs); // should throw exception assertTrue(false); } catch (IOException e) { @@ -185,8 +189,8 @@ public void switchVersion() throws IOException, TaskRunnerException { List pkgsV2 = new LinkedList<>(); pkgsV2.add(pkgV2); - File bundle1 = hvf.bundle(pkgsV1); - File bundle2 = hvf.bundle(pkgsV2); + File bundle1 = hbf.buildBundle(pkgsV1); + File bundle2 = hbf.buildBundle(pkgsV2); assertNotSame(bundle1.lastModified(), bundle2.lastModified()); } From e9259673868f02d04ebd8636d625f32f6746a3c8 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 17 Jan 2017 19:41:10 +0900 Subject: [PATCH 02/29] feat: Support FRONTEND_INTERPRETER type in frontend --- .../src/app/helium/helium.controller.js | 24 +++---- zeppelin-web/src/app/helium/helium.css | 8 +-- zeppelin-web/src/app/helium/helium.html | 16 ++--- .../src/components/helium/helium-type.js | 4 ++ .../src/components/helium/helium.service.js | 70 ++++++++++++++++--- 5 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 zeppelin-web/src/components/helium/helium-type.js diff --git a/zeppelin-web/src/app/helium/helium.controller.js b/zeppelin-web/src/app/helium/helium.controller.js index a344e801691..8b91ed58352 100644 --- a/zeppelin-web/src/app/helium/helium.controller.js +++ b/zeppelin-web/src/app/helium/helium.controller.js @@ -22,8 +22,8 @@ $scope.packageInfos = {}; $scope.defaultVersions = {}; $scope.showVersions = {}; - $scope.visualizationOrder = []; - $scope.visualizationOrderChanged = false; + $scope.bundleOrder = []; + $scope.bundleOrderChanged = false; var buildDefaultVersionListToDisplay = function(packageInfos) { var defaultVersions = {}; @@ -60,33 +60,33 @@ }); }; - var getVisualizationOrder = function() { - heliumService.getVisualizationOrder(). + var getBundleOrder = function() { + heliumService.getBundleOrder(). success(function(data, status) { - $scope.visualizationOrder = data.body; + $scope.bundleOrder = data.body; }). error(function(data, status) { - console.log('Can not get visualization order %o %o', status, data); + console.log('Can not get bundle order %o %o', status, data); }); }; - $scope.visualizationOrderListeners = { + $scope.bundleOrderListeners = { accept: function(sourceItemHandleScope, destSortableScope) {return true;}, itemMoved: function(event) {}, orderChanged: function(event) { - $scope.visualizationOrderChanged = true; + $scope.bundleOrderChanged = true; } }; var init = function() { getAllPackageInfo(); - getVisualizationOrder(); - $scope.visualizationOrderChanged = false; + getBundleOrder(); + $scope.bundleOrderChanged = false; }; init(); - $scope.saveVisualizationOrder = function() { + $scope.saveBundleOrder = function() { var confirm = BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, @@ -98,7 +98,7 @@ confirm.$modalFooter.find('button').addClass('disabled'); confirm.$modalFooter.find('button:contains("OK")') .html(' Enabling'); - heliumService.setVisualizationOrder($scope.visualizationOrder). + heliumService.setBundleOrder($scope.bundleOrder). success(function(data, status) { init(); confirm.close(); diff --git a/zeppelin-web/src/app/helium/helium.css b/zeppelin-web/src/app/helium/helium.css index f17d6bd31e0..0ba4f37eb60 100644 --- a/zeppelin-web/src/app/helium/helium.css +++ b/zeppelin-web/src/app/helium/helium.css @@ -77,12 +77,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 +97,7 @@ height: 100%; } -.heliumVisualizationOrder .saveLink { +.heliumBundleOrder .saveLink { margin-left:10px; margin-top:5px; cursor:pointer; diff --git a/zeppelin-web/src/app/helium/helium.html b/zeppelin-web/src/app/helium/helium.html index 546995cb44b..393be3fb8bd 100644 --- a/zeppelin-web/src/app/helium/helium.html +++ b/zeppelin-web/src/app/helium/helium.html @@ -20,13 +20,13 @@

-
-
Visualization package display order (drag and drop to reorder)
+
+
Bundle package display order (drag and drop to reorder)
-
+
+ ng-show="bundleOrderChanged" + ng-click="saveBundleOrder()"> save
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 00000000000..7608aa31dd2 --- /dev/null +++ b/zeppelin-web/src/components/helium/helium-type.js @@ -0,0 +1,4 @@ +export const HeliumType = { + VISUALIZATION: 'VISUALIZATION', + FRONTEND_INTERPRETER: 'FRONTEND_INTERPRETER', +} diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index ae44425acae..e202aedbd67 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -12,39 +12,89 @@ * limitations under the License. */ -(function() { +import { HeliumType, } from './helium-type'; +import { + AbstractFrontendInterpreter, + DefaultDisplayType +} from '../../app/frontend-interpreter' +(function() { angular.module('zeppelinWebApp').service('heliumService', heliumService); heliumService.$inject = ['$http', 'baseUrlSrv', 'ngToast']; function heliumService($http, baseUrlSrv, ngToast) { - var url = baseUrlSrv.getRestApiBase() + '/helium/visualizations/load'; - if (process.env.HELIUM_VIS_DEV) { + var url = baseUrlSrv.getRestApiBase() + '/helium/bundle/load'; + if (process.env.HELIUM_BUNDLE_DEV) { url = url + '?refresh=true'; } - var visualizations = []; + // name `heliumBundles` should be same as `HelumBundleFactory.HELIUM_BUNDLES_VAR` + var heliumBundles = []; + // map for `{ magic: interpreter }` + let frontendIntpWithMagic = {}; + // map for `{ displayType: interpreter }` + let frontendIntpWithDisplayType = {}; + 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.FRONTEND_INTERPRETER) { + const interpreter = new b.class(); // eslint-disable-line new-cap + + // add frontend interpreter if `interpret` method is available + if (AbstractFrontendInterpreter.useInterpret(interpreter)) { + frontendIntpWithMagic[interpreter.getMagic()] = interpreter; + } + + // add frontend interpreter if `display` method is available + // and its display type is in default display types. + if (AbstractFrontendInterpreter.useDisplay(interpreter) && + DefaultDisplayType[interpreter.getDisplayType()]) { + frontendIntpWithDisplayType[interpreter.getDisplayType()] = interpreter; + } + + } else if (b.type === HeliumType.VISUALIZATION) { + visualizationBundles.push(b); + } + }); } else { console.error(response); } }); - this.get = function() { - return visualizations; + /** + * @param magic {string} e.g `%flowchart` + * @returns {FrontendInterpreterBase} undefined for non-available magic + */ + this.getFrontendInterpreterUsingMagic = function(magic) { + return frontendIntpWithMagic[magic]; + }; + + /** + * @param displayType {string} e.g `ELEMENT` + * @returns {FrontendInterpreterBase} undefined for non-available displayType + */ + this.getFrontendInterpreterWithDisplayType = function(displayType) { + return frontendIntpWithDisplayType[displayType]; + }; + + this.getVisualizationBundles = function() { + return visualizationBundles; }; - this.getVisualizationOrder = function() { - return $http.get(baseUrlSrv.getRestApiBase() + '/helium/visualizationOrder'); + this.getBundleOrder = function() { + return $http.get(baseUrlSrv.getRestApiBase() + '/helium/bundleOrder'); }; - this.setVisualizationOrder = function(list) { - return $http.post(baseUrlSrv.getRestApiBase() + '/helium/visualizationOrder', list); + this.setBundleOrder = function(list) { + return $http.post(baseUrlSrv.getRestApiBase() + '/helium/bundleOrder', list); }; this.getAllPackageInfo = function() { From 247d00f316407c86aea51328d5682036b67b218f Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 17 Jan 2017 19:41:32 +0900 Subject: [PATCH 03/29] feat: Add frontend interpreter framework --- .../src/app/frontend-interpreter/.npmignore | 1 + .../frontend-interpreter-framework.js | 97 +++++++++++++++++++ .../frontend-interpreter-result.js | 69 +++++++++++++ .../src/app/frontend-interpreter/index.js | 25 +++++ .../src/app/frontend-interpreter/package.json | 13 +++ 5 files changed, 205 insertions(+) create mode 100644 zeppelin-web/src/app/frontend-interpreter/.npmignore create mode 100644 zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js create mode 100644 zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js create mode 100644 zeppelin-web/src/app/frontend-interpreter/index.js create mode 100644 zeppelin-web/src/app/frontend-interpreter/package.json diff --git a/zeppelin-web/src/app/frontend-interpreter/.npmignore b/zeppelin-web/src/app/frontend-interpreter/.npmignore new file mode 100644 index 00000000000..0b84df0f025 --- /dev/null +++ b/zeppelin-web/src/app/frontend-interpreter/.npmignore @@ -0,0 +1 @@ +*.html \ No newline at end of file diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js new file mode 100644 index 00000000000..0e3c4d57a7d --- /dev/null +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -0,0 +1,97 @@ +/* + * 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, + FrontendInterpreterResult, +} from './frontend-interpreter-result'; +/*eslint-enable no-unused-vars */ + +export class AbstractFrontendInterpreter { + constructor(magic, displayType) { + this.magic = magic; + this.displayType = displayType; + } + + /** + * @param paragraphText which includes magic + * @returns {string} if magic exists, otherwise return undefined + */ + static extractMagic(allParagraphText) { + const intpNameRegexp = /^\s*%(\S+)\s*/g; + try { + let match = intpNameRegexp.exec(allParagraphText); + if (match) { return `%${match[1].trim()}`; } + } catch (error) { + // failed to parse, ignore + } + + return undefined; +} + + static useInterpret(interpreter) { + return (interpreter.__proto__.hasOwnProperty('interpret')); + } + + static useDisplay(interpreter) { + return (interpreter.__proto__.hasOwnProperty('display')); + } + + /** + * Consumes text and return multiple interpreter results. + * This method should handle error properly to provide precise error message. + * + * @param paragraphText {string} which doesn't include magic + * @return {Array} + */ + interpret(paragraphText) { + /** implement this if you want to add a frontend interpreter */ + } + + /** + * Consumes text and return a single interpreter result. + * This method should handle error properly to provide precise error message. + * + * Currently, `display` only allows DefaultDisplayType + * as a type of result to avoid to recursive evaluation of display results. + * + * @param paragraphText {string} + * @return {FrontendInterpreterResult} + */ + display(paragraphText) { + /** implement this if you want to add a display system */ + } + + /** + * return magic for this frontend interpreter. + * (e.g `%flowchart`) + * @return {string} + */ + getMagic() { + return this.magic; + } + + /** + * return display type for this frontend interpreter. + * (e.g `DefaultDisplayType.TEXT`) + * @return {string} + */ + getDisplayType() { + return this.displayType; + } +} diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js new file mode 100644 index 00000000000..2af6efc4790 --- /dev/null +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js @@ -0,0 +1,69 @@ +/* + * 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 class FrontendInterpreterResult { + constructor(resultDisplayType, resultDataGenerator) { + /** + * backend interpreter uses `data` and `type` as field names. + * let's use the same field to keep consistency. + */ + this.type = resultDisplayType; + this.data = resultDataGenerator; + } + + static isFunctionGenerator(generator) { + return (generator && typeof generator === 'function'); + } + + static isPromiseGenerator(generator) { + return (generator && typeof generator.then === 'function'); + } + + static isObjectGenerator(generator) { + return ( + !FrontendInterpreterResult.isFunctionGenerator(generator) && + !FrontendInterpreterResult.isPromiseGenerator(generator)); + } + + /** + * @returns {string} display type for this frontend interpreter result. + */ + getType() { + return this.type; + } + + /** + * generator (`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; + } +} diff --git a/zeppelin-web/src/app/frontend-interpreter/index.js b/zeppelin-web/src/app/frontend-interpreter/index.js new file mode 100644 index 00000000000..7390041c811 --- /dev/null +++ b/zeppelin-web/src/app/frontend-interpreter/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, + FrontendInterpreterResult, +} from './frontend-interpreter-result'; + +export { + AbstractFrontendInterpreter +} from './frontend-interpreter-framework'; diff --git a/zeppelin-web/src/app/frontend-interpreter/package.json b/zeppelin-web/src/app/frontend-interpreter/package.json new file mode 100644 index 00000000000..bc7b19fca1e --- /dev/null +++ b/zeppelin-web/src/app/frontend-interpreter/package.json @@ -0,0 +1,13 @@ +{ + "name": "zeppelin-frontend-interpreter", + "description": "Frontend Interpreter API", + "version": "0.7.0-SNAPSHOT", + "main": "index", + "dependencies": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/apache/zeppelin" + }, + "license": "Apache-2.0" +} From a163044d61de3db8c5c20e2007a417afb7964b97 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 17 Jan 2017 19:41:59 +0900 Subject: [PATCH 04/29] feat: Add flowchart, translator examples --- zeppelin-examples/pom.xml | 2 + .../index.js | 109 ++++++++++++++++ .../package.json | 13 ++ .../pom.xml | 116 ++++++++++++++++++ ...xample-frontend-interpreter-flowchart.json | 24 ++++ .../index.js | 64 ++++++++++ .../package.json | 12 ++ .../pom.xml | 116 ++++++++++++++++++ ...ample-frontend-interpreter-translator.json | 24 ++++ 9 files changed, 480 insertions(+) create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 300ba57808a..52e631752b8 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -36,6 +36,8 @@ zeppelin-example-clock zeppelin-example-horizontalbar + zeppelin-example-frontend-interpreter-flowchart + zeppelin-example-frontend-interpreter-translator diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js new file mode 100644 index 00000000000..25ef89eb2d1 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js @@ -0,0 +1,109 @@ +/* + * 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 { + AbstractFrontendInterpreter, + FrontendInterpreterResult, + DefaultDisplayType, +} from 'zeppelin-frontend-interpreter'; + +import flowchart from 'flowchart.js'; + +export default class FlowchartInterpreter extends AbstractFrontendInterpreter { + constructor() { + super("%flowchart", DefaultDisplayType.ELEMENT); + } + + 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. + * But now, we return just 1 result which is wrapped in an array. + */ + return [new FrontendInterpreterResult( + DefaultDisplayType.ELEMENT, + 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' } + } + } + } +} + + + + + + + + + + + + + + + + + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json new file mode 100644 index 00000000000..b64cbe25de4 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json @@ -0,0 +1,13 @@ +{ + "name": "flowchart-frontend-interpreter", + "description": "Frontend Flowchart Interpreter (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "raphael": "2.2.0", + "flowchart.js": "^1.6.5", + "zeppelin-frontend-interpreter": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml new file mode 100644 index 00000000000..8f465172166 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml @@ -0,0 +1,116 @@ + + + + + 4.0.0 + + + zeppelin-examples + org.apache.zeppelin + 0.7.0-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-example-frontend-interpreter-flowchart + jar + 0.7.0-SNAPSHOT + Zeppelin: Example application - Frontend Flowchart Interpreter + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + ${project.groupId} + helium-dev + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + true + + + + + maven-clean-plugin + + + + ${project.basedir}/../../helium + + ${project.artifactId}.json + + + + + + + + maven-resources-plugin + 2.7 + + + generate-resources + + copy-resources + + + + ${project.basedir}/../../helium/ + + + ${project.basedir} + + ${project.artifactId}.json + + + + + + + + + + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json new file mode 100644 index 00000000000..73e1150636d --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json @@ -0,0 +1,24 @@ +/* + * 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" : "FRONTEND_INTERPRETER", + "name" : "flowchart-frontend-interpreter", + "description" : "with Flowchart.js (example)", + "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart", + "license" : "Apache-2.0", + "icon" : "" +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js new file mode 100644 index 00000000000..60db4712a80 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js @@ -0,0 +1,64 @@ +/* + * 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 { + AbstractFrontendInterpreter, + FrontendInterpreterResult, + DefaultDisplayType, +} from 'zeppelin-frontend-interpreter'; + +import 'whatwg-fetch'; + +export default class TranslatorInterpreter extends AbstractFrontendInterpreter { + constructor() { + super("%translator", DefaultDisplayType.TEXT); + } + + interpret(paragraphText) { + return [new FrontendInterpreterResult( + DefaultDisplayType.TEXT, + this.translate(paragraphText) + )]; + } + + translate(text) { + return fetch('https://translation.googleapis.com/language/translate/v2', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_ACCESS_KEY_HERE', + }, + body: JSON.stringify({ + 'q': text, + 'source': 'en', + 'target': 'ko', + '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'); + }); + } +} + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json new file mode 100644 index 00000000000..838a65525ec --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json @@ -0,0 +1,12 @@ +{ + "name": "translator-frontend-interpreter", + "description": "Frontend Translator Interpreter (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "whatwg-fetch": "^2.0.1", + "zeppelin-frontend-interpreter": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml new file mode 100644 index 00000000000..84ec74e58b8 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml @@ -0,0 +1,116 @@ + + + + + 4.0.0 + + + zeppelin-examples + org.apache.zeppelin + 0.7.0-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-example-frontend-interpreter-translator + jar + 0.7.0-SNAPSHOT + Zeppelin: Example application - Frontend Translator Interpreter + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + ${project.groupId} + helium-dev + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + true + + + + + maven-clean-plugin + + + + ${project.basedir}/../../helium + + ${project.artifactId}.json + + + + + + + + maven-resources-plugin + 2.7 + + + generate-resources + + copy-resources + + + + ${project.basedir}/../../helium/ + + + ${project.basedir} + + ${project.artifactId}.json + + + + + + + + + + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json new file mode 100644 index 00000000000..9c447a532fd --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json @@ -0,0 +1,24 @@ +/* + * 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" : "FRONTEND_INTERPRETER", + "name" : "translator-frontend-interpreter", + "description" : "with Google Translation API (examaple)", + "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-translator", + "license" : "Apache-2.0", + "icon" : "" +} From 5810bf1de5cd5b7e00c502da57cd1862b837b3b9 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 17 Jan 2017 19:42:26 +0900 Subject: [PATCH 05/29] feat: Apply frontend interpreter to paragraph --- .../index.js | 12 +- .../frontend-interpreter-framework.js | 2 +- .../src/app/notebook/notebook.controller.js | 6 +- .../notebook/paragraph/paragraph-control.html | 2 +- .../paragraph/paragraph.controller.js | 103 +++++--- .../src/app/notebook/paragraph/paragraph.html | 4 +- .../paragraph/result/result.controller.js | 241 ++++++++++++------ .../app/notebook/paragraph/result/result.html | 10 +- .../src/components/helium/helium.service.js | 2 +- .../test/spec/controllers/paragraph.js | 4 - 10 files changed, 254 insertions(+), 132 deletions(-) diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js index 60db4712a80..5e2106ba373 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js @@ -40,7 +40,7 @@ export default class TranslatorInterpreter extends AbstractFrontendInterpreter { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': 'Bearer YOUR_ACCESS_KEY_HERE', + 'Authorization': 'Bearer YOUR_ACCESS_KEY', }, body: JSON.stringify({ 'q': text, @@ -48,16 +48,16 @@ export default class TranslatorInterpreter extends AbstractFrontendInterpreter { 'target': 'ko', 'format': 'text' }) - }).then((response) => { + }).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'); + const extracted = json.data.translations.map(t => { + return t.translatedText; + }); + return extracted.join('\n'); }); } } diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js index 0e3c4d57a7d..a7013d1c291 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -70,7 +70,7 @@ export class AbstractFrontendInterpreter { * Currently, `display` only allows DefaultDisplayType * as a type of result to avoid to recursive evaluation of display results. * - * @param paragraphText {string} + * @param paragraphText {string} which doesn't include magic * @return {FrontendInterpreterResult} */ display(paragraphText) { diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index ccf64b7b907..b434b642113 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(); diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-control.html index 644761ea3f8..351fb5ffecc 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 @@
-
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 6d56fe40a37..d17b42f536f 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, + FrontendInterpreterResult, +} from '../../../frontend-interpreter' angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl); @@ -149,14 +153,12 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // image data $scope.imageData; + $scope.textRendererInitialized = false; $scope.init = function(result, config, paragraph, index) { - console.log('result controller init %o %o %o', result, config, 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, @@ -166,7 +168,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location class: vis.class }; }); - + updateData(result, config, paragraph, index); renderResult($scope.type); }; @@ -259,92 +261,170 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } if (activeApp) { - var app = _.find($scope.apps, {id: activeApp}); - renderApp(app); + const app = _.find($scope.apps, {id: activeApp}); + renderApp(app, `p${appState.id}`); } else { - if (type === 'TABLE') { + + /** + * find proper interpreter which can handle the non-default display type + * the displayed type which is generated from `display()` + * should be one of the default interpreter type + */ + // const isDefaultDisplayType = DefaultDisplayType[type]; + // if (!isDefaultDisplayType) { + // const intp = heliumService.getFrontendInterpreterWithDisplayType(type); + // + // if (intp) { + // // currently display only accepts `Object` data not + // // doesn't support function and promise + // const result = intp.display(data); + // type = result.getType(); + // data = result.getData(); + // } + // } + + if (type === DefaultDisplayType.TABLE) { $scope.renderGraph($scope.graphMode, refresh); - } else if (type === 'HTML') { - renderHtml(); - } else if (type === 'ANGULAR') { - renderAngular(); - } else if (type === 'TEXT') { - renderText(); + } else if (type === DefaultDisplayType.HTML) { + renderHtml(`p${$scope.id}_html`, data); + } else if (type === DefaultDisplayType.ANGULAR) { + renderAngular(`p${$scope.id}_angular`, data); + } else if (type === DefaultDisplayType.TEXT) { + renderText(`p${$scope.id}_text`, data); + } else if (type === DefaultDisplayType.ELEMENT) { + renderElem(`p${$scope.id}_elem`, data); + } else { + console.error(`Unknown Display Type: ${type}`); } } }; - var renderHtml = function() { - var retryRenderer = function() { - var htmlEl = angular.element('#p' + $scope.id + '_html'); - if (htmlEl.length) { - try { - htmlEl.html(data); + /** + * generates actually object which will be consumed from `data` property + * feed it to the success callback. + * if error occurs, the error is passed to the failure callback + * + * @param generator {Object or Promise or Function} + * @param type {string} Display Type + * @param successCallback + * @param failureCallback + */ + const generateData = function(generator, type, successCallback, failureCallback) { + if (FrontendInterpreterResult.isFunctionGenerator(generator)) { + try { + successCallback(generator()); + } catch (error) { + failureCallback(error); + console.error(`Failed to handle ${type} type, function generator\n`, error); + } + } else if (FrontendInterpreterResult.isPromiseGenerator(generator)) { + generator + .then((generated) => { successCallback(generated); }) + .catch((error) => { + failureCallback(error); + console.error(`Failed to handle ${type} type, promise generator\n`, error); + }); + } else if (FrontendInterpreterResult.isObjectGenerator(generator)) { + try { + successCallback(generator); + } catch (error) { + console.error(`Failed to handle ${type} type, object generator\n`, error); + } + } + }; - 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); - } - } else { + const renderElem = function(targetElemId, generator) { + function retryRenderer() { + const elem = angular.element(`#${targetElemId}`); + if (!elem.length) { $timeout(retryRenderer, 10); + return; } - }; + + generateData(() => { generator(targetElemId) }, DefaultDisplayType.ELEMENT, + () => {}, /** HTML element will be filled in generator . thus pass empty success callback */ + (error) => { elem.html(`${error.stack}`); } + ); + } + $timeout(retryRenderer); }; - var renderAngular = function() { - var retryRenderer = function() { - if (angular.element('#p' + $scope.id + '_angular').length) { - try { - angular.element('#p' + $scope.id + '_angular').html(data); - - 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 { + const renderHtml = function(targetElemId, generator) { + function retryRenderer() { + const elem = angular.element(`#${targetElemId}`); + if (!elem.length) { $timeout(retryRenderer, 10); + return; } - }; + + generateData(generator, 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}`); } + ); + } + $timeout(retryRenderer); }; - var getTextEl = function (paragraphId) { - return angular.element('#p' + $scope.id + '_text'); - } + const renderAngular = function(targetElemId, generator) { + function retryRenderer() { + const elem = angular.element(`#${targetElemId}`); + if (!elem.length) { + $timeout(retryRenderer, 10); + return; + } - 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; + const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`); + generateData(generator, DefaultDisplayType.ANGULAR, + (generated) => { + elem.html(generated); + $compile(elem.contents())(paragraphScope); + }, + (error) => { elem.html(`${error.stack}`); } + ); + } - if (data) { - appendTextOutput(data); - } else { - flushAppendQueue(); - } + $timeout(retryRenderer); + }; - getTextEl($scope.id).bind('mousewheel', function(e) { - $scope.keepScrollDown = false; - }); - } else { + const getTextResultElem = function (resultId) { + return angular.element('#p' + resultId + '_text'); + }; + + const renderText = function(targetElemId, generator) { + function retryRenderer() { + const elem = angular.element(`#${targetElemId}`); + if (!elem.length) { $timeout(retryRenderer, 10); + return; } - }; + + generateData(generator, DefaultDisplayType.TEXT, + (generated) => { + // clear all lines before render + clearTextOutput(); + $scope.textRendererInitialized = true; + if (generated) { appendTextOutput(generated); } + else { flushAppendQueue(); } + elem.bind('mousewheel', (e) => { $scope.keepScrollDown = false; }); + }, + (error) => { elem.html(`${error.stack}`); } + ); + } + $timeout(retryRenderer); - }; + } var clearTextOutput = function() { - var textEl = getTextEl($scope.id); + var textEl = getTextResultElem($scope.id); if (textEl.length) { textEl.children().remove(); } @@ -359,11 +439,11 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; var appendTextOutput = function(msg) { - if (!textRendererInitialized) { + if (!$scope.textRendererInitialized) { textAppendQueueBeforeInitialize.push(msg); } else { flushAppendQueue(); - var textEl = getTextEl($scope.id); + var textEl = getTextResultElem($scope.id); if (textEl.length) { var lines = msg.split('\n'); for (var i = 0; i < lines.length; i++) { @@ -371,7 +451,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } } if ($scope.keepScrollDown) { - var doc = getTextEl($scope.id); + var doc = getTextResultElem($scope.id); doc[0].scrollTop = doc[0].scrollHeight; } } @@ -725,22 +805,23 @@ 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) { + const renderApp = function(targetElemId, appState) { + function retryRenderer() { + var elem = angular.element(document.getElementById(elememId)); + console.log('retry renderApp %o', elem); + if (!elem.length) { + $timeout(retryRenderer, 1000); + } else { try { console.log('renderApp %o', appState); - targetEl.html(appState.output); - $compile(targetEl.contents())(getAppScope(appState)); + elem.html(appState.output); + $compile(elem.contents())(getAppScope(appState)); } catch (err) { console.log('App rendering error %o', err); } - } else { - $timeout(retryRenderer, 1000); } - }; + } + $timeout(retryRenderer); }; diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index df09c4d5f59..90fa9bc6c0e 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -67,13 +67,15 @@ tooltip="Scroll Top">
-
+
+ +
-
diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index e202aedbd67..7e23af65091 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -108,5 +108,5 @@ import { this.disable = function(name) { return $http.post(baseUrlSrv.getRestApiBase() + '/helium/disable/' + name); }; - }; + } })(); diff --git a/zeppelin-web/test/spec/controllers/paragraph.js b/zeppelin-web/test/spec/controllers/paragraph.js index 4089d2d1435..1aa4a4af87b 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]); }); From 5c49e6e3d3c82905ce32703ca596404b2ccd980f Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Sat, 21 Jan 2017 08:22:22 +0900 Subject: [PATCH 06/29] feat: Automated display type checking in result --- .../frontend-interpreter-framework.js | 16 -- .../frontend-interpreter-result.js | 205 ++++++++++++++++-- .../paragraph/paragraph.controller.js | 27 ++- .../src/components/helium/helium.service.js | 9 +- 4 files changed, 212 insertions(+), 45 deletions(-) diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js index a7013d1c291..d33889bc179 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -28,22 +28,6 @@ export class AbstractFrontendInterpreter { this.displayType = displayType; } - /** - * @param paragraphText which includes magic - * @returns {string} if magic exists, otherwise return undefined - */ - static extractMagic(allParagraphText) { - const intpNameRegexp = /^\s*%(\S+)\s*/g; - try { - let match = intpNameRegexp.exec(allParagraphText); - if (match) { return `%${match[1].trim()}`; } - } catch (error) { - // failed to parse, ignore - } - - return undefined; -} - static useInterpret(interpreter) { return (interpreter.__proto__.hasOwnProperty('interpret')); } diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js index 2af6efc4790..0437751755f 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js @@ -23,14 +23,146 @@ export const DefaultDisplayType = { TEXT: 'TEXT', }; +export const DefaultDisplayMagic = { + '%element': DefaultDisplayType.ELEMENT, + '%table': DefaultDisplayType.TABLE, + '%html': DefaultDisplayType.HTML, + '%angular': DefaultDisplayType.ANGULAR, + '%text': DefaultDisplayType.TEXT, +}; + +export class GeneratorWithType { + constructor(generator, type) { + // use variable name `data` to keep consistency with backend + this.data = generator; + this.type = type; + } + + 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 parseStringGenerator(generator, customDisplayMagic) { + function availableMagic(magic) { + return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); + } + + const splited = generator.split("\n"); + + const gensWithTypes = []; + let mergedGens = []; + let previousMagic = DefaultDisplayType.TEXT; + + // create `GeneratorWithType` whenever see available display type. + for(let i = 0; i < splited.length; i++) { + const g = splited[i]; + const magic = FrontendInterpreterResult.extractMagic(g); + + // create `GeneratorWithType` only if see new magic + if (availableMagic(magic) && mergedGens.length > 0) { + gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic)); + mergedGens = []; + } + + // accumulate `generator` to mergedGens + if (availableMagic(magic)) { + const withoutMagic = g.split(magic)[1]; + mergedGens.push(`${withoutMagic}\n`); + previousMagic = GeneratorWithType.handleDefaultMagic(magic); + } else { + mergedGens.push(`${g}\n`); + } + } + + // cleanup the last `GeneratorWithType` + if (mergedGens.length > 0) { + previousMagic = GeneratorWithType.handleDefaultMagic(previousMagic); + gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic)); + } + + return gensWithTypes; + } + + /** + * get 1 `GeneratorWithType` and produce multiples using available displays + * return an wrapped with a promise to generalize result output which can be + * object, function or promise + * @param generatorWithType {GeneratorWithType} + * @param availableDisplays {Object} Map for available displays + * @return {Promise>} + */ + static produceMultipleGenerator(generatorWithType, customDisplayType) { + const generator = generatorWithType.getGenerator(); + const type = generatorWithType.getType(); + + // if the type is specified, just return it + // handle non-specified generatorWithTypes only + if (type) { + return new Promise((resolve) => { resolve([generatorWithType]); }); + } + + let wrapped; + + if (FrontendInterpreterResult.isFunctionGenerator(generator)) { + // if generator is a function, we consider it as ELEMENT type. + wrapped = new Promise((resolve) => { + const result = [new GeneratorWithType(generator, DefaultDisplayType.ELEMENT)]; + return resolve(result); + }); + } else if (FrontendInterpreterResult.isPromiseGenerator(generator)) { + // if generator is a promise, + wrapped = generator.then(generated => { + const result = + GeneratorWithType.parseStringGenerator(generated, customDisplayType); + return result; + }) + + } else { + // if generator is a object, parse it to multiples + wrapped = new Promise((resolve) => { + const result = + GeneratorWithType.parseStringGenerator(generator, customDisplayType); + return resolve(result); + }); + } + + return wrapped; + } + + /** + * generator (`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. + */ + getGenerator() { + return this.data; + } + + /** + * Value of `type` might be empty which means + * generator can be splited into multiple generators + * by `FrontendInterpreterResult.parseMultipleGenerators()` + * @returns {string} + */ + getType() { + return this.type; + } +} + export class FrontendInterpreterResult { - constructor(resultDisplayType, resultDataGenerator) { - /** - * backend interpreter uses `data` and `type` as field names. - * let's use the same field to keep consistency. - */ - this.type = resultDisplayType; - this.data = resultDataGenerator; + constructor(resultGenerator, resultType) { + this.generatorsWithTypes = []; + this.add(resultGenerator, resultType); } static isFunctionGenerator(generator) { @@ -42,28 +174,61 @@ export class FrontendInterpreterResult { } static isObjectGenerator(generator) { - return ( + return (generator && !FrontendInterpreterResult.isFunctionGenerator(generator) && !FrontendInterpreterResult.isPromiseGenerator(generator)); } + static extractMagic(allParagraphText) { + const intpNameRegexp = /^\s*%(\S+)\s*/g; + try { + let match = intpNameRegexp.exec(allParagraphText); + if (match) { + return `%${match[1].trim()}`; + } + } catch (error) { + // failed to parse, ignore + } + + return undefined; + } + /** - * @returns {string} display type for this frontend interpreter result. + * consume 1 generator and produce multiple + * @param generator {string} + * @param customDisplayType + * @return {Array} */ - getType() { - return this.type; + + add(resultGenerator, resultType) { + if (resultGenerator) { + this.generatorsWithTypes.push( + new GeneratorWithType(resultGenerator, resultType)); + } + + return this; } /** - * generator (`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. + * @param customDisplayType + * @return {Promise>} */ - getData() { - return this.data; + getAllParsedGeneratorsWithTypes(customDisplayType) { + const promises = this.generatorsWithTypes.map(gt => { + return GeneratorWithType.produceMultipleGenerator(gt, customDisplayType); + }); + + // 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; } } diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 710d4393f6d..09e0eafd383 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -12,7 +12,7 @@ * limitations under the License. */ -import { AbstractFrontendInterpreter } from '../../frontend-interpreter' +import { FrontendInterpreterResult } from '../../frontend-interpreter' angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); @@ -225,9 +225,16 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat websocketMsgSrv.cancelParagraphRun(paragraph.id); }; + $scope.handleFrontendInterpreterError = function(error) { + $scope.paragraph.status = 'ERROR'; + $scope.paragraph.errorMessage = error.stack; + console.error('Failed to execute FrontendInterpreter.interpret\n', error); + $scope.$digest(); + } + $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, magic) { // clear results which is watched by `ng-repeat`. without this, - // frontend interpreter results cannot be rendered in view twice more + // frontend interpreter results cannot be rendered in view more than once $scope.paragraph.results = {}; $scope.$digest(); @@ -236,13 +243,17 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const splited = paragraphText.split(magic); const textWithoutMagic = splited[1]; $scope.paragraph.status = 'FINISHED'; - $scope.paragraph.results.msg = intp.interpret(textWithoutMagic); - $scope.paragraph.config.tableHide = false; + const frontIntpResult = intp.interpret(textWithoutMagic); + const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( + heliumService.getAvailableFrontendInterpreterDisplay()); + parsed.then(resultsMsg => { + $scope.paragraph.results.msg = resultsMsg; + $scope.paragraph.config.tableHide = false; + $scope.$digest(); + }).catch($scope.handleFrontendInterpreterError); // TODO(1ambda): broadcast to other clients } catch (error) { - $scope.paragraph.status = 'ERROR'; - $scope.paragraph.errorMessage = error.stack; - console.error('Failed to execute FrontendInterpreter.interpret\n', error); + $scope.handleFrontendInterpreterError(error); } }; @@ -271,7 +282,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat return; } - const magic = AbstractFrontendInterpreter.extractMagic(paragraphText); + const magic = FrontendInterpreterResult.extractMagic(paragraphText); const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic); if (frontendIntp) { diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index 7e23af65091..b555b1a7875 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -81,10 +81,17 @@ import { * @param displayType {string} e.g `ELEMENT` * @returns {FrontendInterpreterBase} undefined for non-available displayType */ - this.getFrontendInterpreterWithDisplayType = function(displayType) { + this.getFrontendInterpreterUsingDisplayType = function(displayType) { return frontendIntpWithDisplayType[displayType]; }; + /** + * @returns {Object} + */ + this.getAvailableFrontendInterpreterDisplay = function() { + return frontendIntpWithDisplayType; + } + this.getVisualizationBundles = function() { return visualizationBundles; }; From c906da62308c80cc510eb9e811e91f3f97ea3696 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Sat, 21 Jan 2017 09:18:26 +0900 Subject: [PATCH 07/29] feat: Update examples to use single FrontIntpRes --- .../index.js | 9 +++--- .../pom.xml | 4 +-- .../index.js | 13 +++++--- .../pom.xml | 4 +-- .../frontend-interpreter-framework.js | 22 -------------- .../paragraph/paragraph.controller.js | 2 +- .../src/components/helium/helium.service.js | 30 +++---------------- 7 files changed, 22 insertions(+), 62 deletions(-) diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js index 25ef89eb2d1..1b280b2e1dc 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js @@ -41,13 +41,12 @@ export default class FlowchartInterpreter extends AbstractFrontendInterpreter { }; /** - * `interpret` method can return multiple results. - * But now, we return just 1 result which is wrapped in an array. + * `interpret` method can return multiple results using `add()` + * but now, we return just 1 result */ - return [new FrontendInterpreterResult( - DefaultDisplayType.ELEMENT, + return new FrontendInterpreterResult( callback - )]; + ); } getOption() { diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml index 8f465172166..56d3d2df9ff 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.7.0-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-frontend-interpreter-flowchart jar - 0.7.0-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example application - Frontend Flowchart Interpreter diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js index 5e2106ba373..168b42acb4f 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js @@ -29,10 +29,15 @@ export default class TranslatorInterpreter extends AbstractFrontendInterpreter { } interpret(paragraphText) { - return [new FrontendInterpreterResult( - DefaultDisplayType.TEXT, - this.translate(paragraphText) - )]; + /** + * FrontendInterpreterResult + * - accepts not only `string` but also `promise` + * - allows multiple output using the `add()` function + */ + const result = new FrontendInterpreterResult() + .add('%html

Translation From English To Korean

') + .add(this.translate(paragraphText)); + return result; } translate(text) { diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml index 84ec74e58b8..d572b5a830b 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml @@ -22,14 +22,14 @@ zeppelin-examples org.apache.zeppelin - 0.7.0-SNAPSHOT + 0.8.0-SNAPSHOT .. org.apache.zeppelin zeppelin-example-frontend-interpreter-translator jar - 0.7.0-SNAPSHOT + 0.8.0-SNAPSHOT Zeppelin: Example application - Frontend Translator Interpreter diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js index d33889bc179..172d7e1dcb0 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -28,14 +28,6 @@ export class AbstractFrontendInterpreter { this.displayType = displayType; } - static useInterpret(interpreter) { - return (interpreter.__proto__.hasOwnProperty('interpret')); - } - - static useDisplay(interpreter) { - return (interpreter.__proto__.hasOwnProperty('display')); - } - /** * Consumes text and return multiple interpreter results. * This method should handle error properly to provide precise error message. @@ -47,20 +39,6 @@ export class AbstractFrontendInterpreter { /** implement this if you want to add a frontend interpreter */ } - /** - * Consumes text and return a single interpreter result. - * This method should handle error properly to provide precise error message. - * - * Currently, `display` only allows DefaultDisplayType - * as a type of result to avoid to recursive evaluation of display results. - * - * @param paragraphText {string} which doesn't include magic - * @return {FrontendInterpreterResult} - */ - display(paragraphText) { - /** implement this if you want to add a display system */ - } - /** * return magic for this frontend interpreter. * (e.g `%flowchart`) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 09e0eafd383..e91873ff9db 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -245,7 +245,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat $scope.paragraph.status = 'FINISHED'; const frontIntpResult = intp.interpret(textWithoutMagic); const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( - heliumService.getAvailableFrontendInterpreterDisplay()); + heliumService.getAvailableFrontendInterpreters()); parsed.then(resultsMsg => { $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index b555b1a7875..c438d355de3 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -33,8 +33,6 @@ import { var heliumBundles = []; // map for `{ magic: interpreter }` let frontendIntpWithMagic = {}; - // map for `{ displayType: interpreter }` - let frontendIntpWithDisplayType = {}; let visualizationBundles = []; // load should be promise @@ -47,19 +45,7 @@ import { heliumBundles.map(b => { if (b.type === HeliumType.FRONTEND_INTERPRETER) { const interpreter = new b.class(); // eslint-disable-line new-cap - - // add frontend interpreter if `interpret` method is available - if (AbstractFrontendInterpreter.useInterpret(interpreter)) { - frontendIntpWithMagic[interpreter.getMagic()] = interpreter; - } - - // add frontend interpreter if `display` method is available - // and its display type is in default display types. - if (AbstractFrontendInterpreter.useDisplay(interpreter) && - DefaultDisplayType[interpreter.getDisplayType()]) { - frontendIntpWithDisplayType[interpreter.getDisplayType()] = interpreter; - } - + frontendIntpWithMagic[interpreter.getMagic()] = interpreter; } else if (b.type === HeliumType.VISUALIZATION) { visualizationBundles.push(b); } @@ -78,20 +64,12 @@ import { }; /** - * @param displayType {string} e.g `ELEMENT` - * @returns {FrontendInterpreterBase} undefined for non-available displayType + * @returns {Object} map for `{ magic : interpreter }` */ - this.getFrontendInterpreterUsingDisplayType = function(displayType) { - return frontendIntpWithDisplayType[displayType]; + this.getAvailableFrontendInterpreters = function() { + return frontendIntpWithMagic; }; - /** - * @returns {Object} - */ - this.getAvailableFrontendInterpreterDisplay = function() { - return frontendIntpWithDisplayType; - } - this.getVisualizationBundles = function() { return visualizationBundles; }; From 0fa7eda609bacdb70f85ff050da5d19bf8833ea7 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 02:52:40 +0900 Subject: [PATCH 08/29] feat: Support custom display --- .../frontend-interpreter-framework.js | 13 +- .../frontend-interpreter-result.js | 2 +- .../paragraph/paragraph.controller.js | 28 ++-- .../paragraph/result/result.controller.js | 135 +++++++++++------- .../app/notebook/paragraph/result/result.html | 4 + .../src/components/helium/helium.service.js | 4 - 6 files changed, 108 insertions(+), 78 deletions(-) diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js index 172d7e1dcb0..22663ba6ed1 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js @@ -23,9 +23,8 @@ import { /*eslint-enable no-unused-vars */ export class AbstractFrontendInterpreter { - constructor(magic, displayType) { + constructor(magic) { this.magic = magic; - this.displayType = displayType; } /** @@ -37,6 +36,7 @@ export class AbstractFrontendInterpreter { */ interpret(paragraphText) { /** implement this if you want to add a frontend interpreter */ + throw new Error('AbstractFrontendInterpreter.interpret is not overrided'); } /** @@ -47,13 +47,4 @@ export class AbstractFrontendInterpreter { getMagic() { return this.magic; } - - /** - * return display type for this frontend interpreter. - * (e.g `DefaultDisplayType.TEXT`) - * @return {string} - */ - getDisplayType() { - return this.displayType; - } } diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js index 0437751755f..e06459b5cf7 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js +++ b/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js @@ -53,7 +53,7 @@ export class GeneratorWithType { return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); } - const splited = generator.split("\n"); + const splited = generator.split('\n'); const gensWithTypes = []; let mergedGens = []; diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index e91873ff9db..d1a93158f41 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -230,18 +230,18 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat $scope.paragraph.errorMessage = error.stack; console.error('Failed to execute FrontendInterpreter.interpret\n', error); $scope.$digest(); - } + }; - $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, magic) { - // clear results which is watched by `ng-repeat`. without this, - // frontend interpreter results cannot be rendered in view more than once + $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, + magic, digestRequired) { $scope.paragraph.results = {}; - $scope.$digest(); + if (digestRequired) { $scope.$digest(); } try { // remove magic from paragraphText const splited = paragraphText.split(magic); - const textWithoutMagic = splited[1]; + // remove leading spaces + const textWithoutMagic = splited[1].replace(/^\s+/g, ''); $scope.paragraph.status = 'FINISHED'; const frontIntpResult = intp.interpret(textWithoutMagic); const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( @@ -249,9 +249,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat parsed.then(resultsMsg => { $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; - $scope.$digest(); + if (digestRequired) { $scope.$digest(); } }).catch($scope.handleFrontendInterpreterError); - // TODO(1ambda): broadcast to other clients } catch (error) { $scope.handleFrontendInterpreterError(error); } @@ -277,7 +276,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat commitParagraph(paragraph); }; - $scope.runParagraph = function(paragraphText) { + $scope.runParagraph = function(paragraphText, digestRequired) { if (!paragraphText || $scope.isRunning($scope.paragraph)) { return; } @@ -286,7 +285,8 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic); if (frontendIntp) { - $scope.runParagraphUsingFrontendInterpreter(frontendIntp, paragraphText, magic); + $scope.runParagraphUsingFrontendInterpreter( + frontendIntp, paragraphText, magic, digestRequired); } else { $scope.runParagraphUsingBackendInterpreter(paragraphText); } @@ -306,14 +306,14 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat }; $scope.runParagraphFromShortcut = function(paragraphText) { - // we need to call `$digest()` to update view immediately - $scope.runParagraph(paragraphText); - $scope.$digest(); + // passing `digestRequired` as true to update view immediately + // without this, results cannot be rendered in view more than once + $scope.runParagraph(paragraphText, true); }; $scope.runParagraphFromButton = function(paragraphText) { // we come here from `$scope.on`, so we don't need to call `$digest()` - $scope.runParagraph(paragraphText) + $scope.runParagraph(paragraphText, false) }; $scope.moveUp = function(paragraph) { diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index d17b42f536f..cb32a3200a3 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -252,8 +252,40 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; - var renderResult = function(type, refresh) { - var activeApp; + $scope.createDisplayDOMId = function(baseDOMId, type) { + if (type === DefaultDisplayType.TABLE) { + return `${baseDOMId}_graph`; + } else if (type === DefaultDisplayType.HTML) { + return `${baseDOMId}_html`; + } else if (type === DefaultDisplayType.ANGULAR) { + return `${baseDOMId}_angular`; + } else if (type === DefaultDisplayType.TEXT) { + return `${baseDOMId}_text`; + } else if (type === DefaultDisplayType.ELEMENT) { + return `${baseDOMId}_elem`; + } else { + console.error(`Cannot create display DOM Id due to unknown display type: ${type}`); + } + }; + + $scope.renderDefaultDisplay = function(targetElemId, type, data, refresh) { + if (type === DefaultDisplayType.TABLE) { + $scope.renderGraph(targetElemId, $scope.graphMode, refresh); + } else if (type === DefaultDisplayType.HTML) { + renderHtml(targetElemId, data); + } else if (type === DefaultDisplayType.ANGULAR) { + renderAngular(targetElemId, data); + } else if (type === DefaultDisplayType.TEXT) { + renderText(targetElemId, data); + } else if (type === DefaultDisplayType.ELEMENT) { + renderElem(targetElemId, data); + } else { + console.error(`Unknown Display Type: ${type}`); + } + }; + + const renderResult = function(type, refresh) { + let activeApp; if (enableHelium) { getSuggestions(); getApplicationStates(); @@ -261,50 +293,64 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } if (activeApp) { - const app = _.find($scope.apps, {id: activeApp}); - renderApp(app, `p${appState.id}`); + const appState = _.find($scope.apps, {id: activeApp}); + renderApp(`p${appState.id}`, appState); } else { - - /** - * find proper interpreter which can handle the non-default display type - * the displayed type which is generated from `display()` - * should be one of the default interpreter type - */ - // const isDefaultDisplayType = DefaultDisplayType[type]; - // if (!isDefaultDisplayType) { - // const intp = heliumService.getFrontendInterpreterWithDisplayType(type); - // - // if (intp) { - // // currently display only accepts `Object` data not - // // doesn't support function and promise - // const result = intp.display(data); - // type = result.getType(); - // data = result.getData(); - // } - // } - - if (type === DefaultDisplayType.TABLE) { - $scope.renderGraph($scope.graphMode, refresh); - } else if (type === DefaultDisplayType.HTML) { - renderHtml(`p${$scope.id}_html`, data); - } else if (type === DefaultDisplayType.ANGULAR) { - renderAngular(`p${$scope.id}_angular`, data); - } else if (type === DefaultDisplayType.TEXT) { - renderText(`p${$scope.id}_text`, data); - } else if (type === DefaultDisplayType.ELEMENT) { - renderElem(`p${$scope.id}_elem`, data); + if (!DefaultDisplayType[type]) { + const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(type); + if (!frontendIntp) { + console.error(`Unknown Display Type: ${type}`); + return; + } + $scope.renderCustomDisplay(type, data, frontendIntp); } else { - console.error(`Unknown Display Type: ${type}`); + const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type); + $scope.renderDefaultDisplay(targetElemId, type, data, refresh); } } }; + $scope.isDefaultDisplay = function() { + return DefaultDisplayType[$scope.type]; + }; + + /** + * Render multiple sub results for custom display + */ + $scope.renderCustomDisplay = function(type, data, frontendIntp) { + // get result from intp + + const frontIntpResult = frontendIntp.interpret(data.trim()); + const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( + heliumService.getAvailableFrontendInterpreters()); + + // custom display result can include multiple subset results + parsed.then(dataWithTypes => { + const containerDOM = document.getElementById(`p${$scope.id}_custom`); + for(let i = 0; i < dataWithTypes.length; i++) { + const dt = dataWithTypes[i]; + const data = dt.data; + const type = dt.type; + + // prepare DOM to be filled + const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom`, type); + const subResultDOM = document.createElement('div'); + containerDOM.appendChild(subResultDOM); + subResultDOM.setAttribute('id', subResultDOMId); + + $scope.renderDefaultDisplay(subResultDOMId, type, data, true); + } + }).catch(error => { + console.error(`Failed to render custom display: ${$scope.type}\n` + error); + }); + }; + /** * generates actually object which will be consumed from `data` property * feed it to the success callback. * if error occurs, the error is passed to the failure callback * - * @param generator {Object or Promise or Function} + * @param generator {Object or Function} * @param type {string} Display Type * @param successCallback * @param failureCallback @@ -317,13 +363,6 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location failureCallback(error); console.error(`Failed to handle ${type} type, function generator\n`, error); } - } else if (FrontendInterpreterResult.isPromiseGenerator(generator)) { - generator - .then((generated) => { successCallback(generated); }) - .catch((error) => { - failureCallback(error); - console.error(`Failed to handle ${type} type, promise generator\n`, error); - }); } else if (FrontendInterpreterResult.isObjectGenerator(generator)) { try { successCallback(generator); @@ -343,7 +382,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location generateData(() => { generator(targetElemId) }, DefaultDisplayType.ELEMENT, () => {}, /** HTML element will be filled in generator . thus pass empty success callback */ - (error) => { elem.html(`${error.stack}`); } + (error) => { elem.html(`${error.stack}`); } ); } @@ -421,7 +460,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } $timeout(retryRenderer); - } + }; var clearTextOutput = function() { var textEl = getTextResultElem($scope.id); @@ -457,11 +496,11 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; - $scope.renderGraph = function(type, refresh) { + $scope.renderGraph = function(targetElemId, type, refresh) { // set graph height var height = $scope.config.graph.height; - var graphContainerEl = angular.element('#p' + $scope.id + '_graph'); - graphContainerEl.height(height); + var targetElem = angular.element(`#${targetElemId}`); + targetElem.height(height); if (!type) { type = 'table'; @@ -807,7 +846,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location const renderApp = function(targetElemId, appState) { function retryRenderer() { - var elem = angular.element(document.getElementById(elememId)); + var elem = angular.element(document.getElementById(targetElemId)); console.log('retry renderApp %o', elem); if (!elem.length) { $timeout(retryRenderer, 1000); diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index 90fa9bc6c0e..287cb65379a 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -67,6 +67,10 @@ tooltip="Scroll Top"> +
+
+
diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index c438d355de3..8f3c70a131d 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -13,10 +13,6 @@ */ import { HeliumType, } from './helium-type'; -import { - AbstractFrontendInterpreter, - DefaultDisplayType -} from '../../app/frontend-interpreter' (function() { angular.module('zeppelinWebApp').service('heliumService', heliumService); From e81cb032858b83128fe74ef9b0a823c8f95af014 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 02:54:36 +0900 Subject: [PATCH 09/29] example: Add echo, markdown --- .../index.js | 32 +++++ .../package.json | 11 ++ .../pom.xml | 116 ++++++++++++++++++ ...lin-example-frontend-interpreter-echo.json | 24 ++++ .../index.js | 2 +- .../index.js | 42 +++++++ .../package.json | 12 ++ .../pom.xml | 116 ++++++++++++++++++ ...example-frontend-interpreter-markdown.json | 24 ++++ .../index.js | 2 +- 10 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml create mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js new file mode 100644 index 00000000000..08189364c6e --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-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 { + AbstractFrontendInterpreter, + FrontendInterpreterResult, + DefaultDisplayType, +} from 'zeppelin-frontend-interpreter'; + +export default class MarkdownInterpreter extends AbstractFrontendInterpreter { + constructor() { + super("%echo"); + } + + interpret(paragraphText) { + return new FrontendInterpreterResult(paragraphText); + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json new file mode 100644 index 00000000000..3815baf160e --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json @@ -0,0 +1,11 @@ +{ + "name": "echo-frontend-interpreter", + "description": "Frontend Echo Interpreter (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "zeppelin-frontend-interpreter": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml new file mode 100644 index 00000000000..1381648447a --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml @@ -0,0 +1,116 @@ + + + + + 4.0.0 + + + zeppelin-examples + org.apache.zeppelin + 0.8.0-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-example-frontend-interpreter-echo + jar + 0.8.0-SNAPSHOT + Zeppelin: Example application - Frontend Echo Interpreter + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + ${project.groupId} + helium-dev + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + true + + + + + maven-clean-plugin + + + + ${project.basedir}/../../helium + + ${project.artifactId}.json + + + + + + + + maven-resources-plugin + 2.7 + + + generate-resources + + copy-resources + + + + ${project.basedir}/../../helium/ + + + ${project.basedir} + + ${project.artifactId}.json + + + + + + + + + + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json new file mode 100644 index 00000000000..36f87d4ef77 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json @@ -0,0 +1,24 @@ +/* + * 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" : "FRONTEND_INTERPRETER", + "name" : "echo-frontend-interpreter", + "description" : "Return just what receive (example)", + "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-echo", + "license" : "Apache-2.0", + "icon" : "" +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js index 1b280b2e1dc..38248e94812 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js @@ -25,7 +25,7 @@ import flowchart from 'flowchart.js'; export default class FlowchartInterpreter extends AbstractFrontendInterpreter { constructor() { - super("%flowchart", DefaultDisplayType.ELEMENT); + super("%flowchart"); } interpret(paragraphText) { diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js new file mode 100644 index 00000000000..5c1be0e4f02 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-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 { + AbstractFrontendInterpreter, + FrontendInterpreterResult, + DefaultDisplayType, +} from 'zeppelin-frontend-interpreter'; + +import md from 'markdown'; + +const markdown = md.markdown; + +export default class MarkdownInterpreter extends AbstractFrontendInterpreter { + 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 FrontendInterpreterResult(parsed, DefaultDisplayType.HTML); + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json new file mode 100644 index 00000000000..d4b5e31f305 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json @@ -0,0 +1,12 @@ +{ + "name": "markdown-frontend-interpreter", + "description": "Frontend Markdown Interpreter (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "markdown": "0.5.0", + "zeppelin-frontend-interpreter": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml new file mode 100644 index 00000000000..e1d44ae700f --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml @@ -0,0 +1,116 @@ + + + + + 4.0.0 + + + zeppelin-examples + org.apache.zeppelin + 0.8.0-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-example-frontend-interpreter-markdown + jar + 0.8.0-SNAPSHOT + Zeppelin: Example application - Frontend Markdown Interpreter + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + ${project.groupId} + helium-dev + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + true + + + + + maven-clean-plugin + + + + ${project.basedir}/../../helium + + ${project.artifactId}.json + + + + + + + + maven-resources-plugin + 2.7 + + + generate-resources + + copy-resources + + + + ${project.basedir}/../../helium/ + + + ${project.basedir} + + ${project.artifactId}.json + + + + + + + + + + diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json new file mode 100644 index 00000000000..b329d4c3f44 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json @@ -0,0 +1,24 @@ +/* + * 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" : "FRONTEND_INTERPRETER", + "name" : "markdown-frontend-interpreter", + "description" : "with markdown-js (example)", + "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-markdown", + "license" : "Apache-2.0", + "icon" : "" +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js index 168b42acb4f..26f2fd2df9b 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js +++ b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js @@ -25,7 +25,7 @@ import 'whatwg-fetch'; export default class TranslatorInterpreter extends AbstractFrontendInterpreter { constructor() { - super("%translator", DefaultDisplayType.TEXT); + super("%translator"); } interpret(paragraphText) { From cac0667b44a3be48b22fd31bbbedc2d8e529e37e Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 05:09:27 +0900 Subject: [PATCH 10/29] style: Rename to Spell --- .../src/assemble/distribution.xml | 4 +-- zeppelin-examples/pom.xml | 6 ++-- .../package.json | 11 ------- .../package.json | 12 -------- .../package.json | 12 -------- .../index.js | 10 +++---- .../zeppelin-example-spell-echo/package.json | 11 +++++++ .../pom.xml | 4 +-- .../zeppelin-example-spell-echo.json} | 6 ++-- .../index.js | 10 +++---- .../package.json | 6 ++-- .../pom.xml | 4 +-- .../zeppelin-example-spell-flowchart.json} | 6 ++-- .../index.js | 10 +++---- .../package.json | 12 ++++++++ .../pom.xml | 4 +-- .../zeppelin-example-spell-markdown.json} | 6 ++-- .../index.js | 18 ++++++----- .../package.json | 12 ++++++++ .../pom.xml | 4 +-- .../zeppelin-example-spell-translator.json} | 6 ++-- .../apache/zeppelin/helium/HeliumPackage.java | 4 +-- .../zeppelin/server/ZeppelinServer.java | 4 +-- .../paragraph/paragraph.controller.js | 30 +++++++++---------- .../paragraph/result/result.controller.js | 24 +++++++-------- .../.npmignore | 0 .../{frontend-interpreter => spell}/index.js | 8 ++--- .../package.json | 6 ++-- .../spell-base.js} | 16 +++++----- .../spell-result.js} | 14 ++++----- .../src/components/helium/helium-type.js | 2 +- .../src/components/helium/helium.service.js | 20 ++++++------- .../zeppelin/helium/HeliumBundleFactory.java | 22 +++++++------- .../helium/HeliumBundleFactoryTest.java | 2 +- 34 files changed, 164 insertions(+), 162 deletions(-) delete mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json delete mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json delete mode 100644 zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json rename zeppelin-examples/{zeppelin-example-frontend-interpreter-echo => zeppelin-example-spell-echo}/index.js (79%) create mode 100644 zeppelin-examples/zeppelin-example-spell-echo/package.json rename zeppelin-examples/{zeppelin-example-frontend-interpreter-echo => zeppelin-example-spell-echo}/pom.xml (96%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json => zeppelin-example-spell-echo/zeppelin-example-spell-echo.json} (85%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-flowchart => zeppelin-example-spell-flowchart}/index.js (92%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-flowchart => zeppelin-example-spell-flowchart}/package.json (53%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-markdown => zeppelin-example-spell-flowchart}/pom.xml (95%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json => zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json} (84%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-markdown => zeppelin-example-spell-markdown}/index.js (82%) create mode 100644 zeppelin-examples/zeppelin-example-spell-markdown/package.json rename zeppelin-examples/{zeppelin-example-frontend-interpreter-flowchart => zeppelin-example-spell-markdown}/pom.xml (95%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json => zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json} (84%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-translator => zeppelin-example-spell-translator}/index.js (81%) create mode 100644 zeppelin-examples/zeppelin-example-spell-translator/package.json rename zeppelin-examples/{zeppelin-example-frontend-interpreter-translator => zeppelin-example-spell-translator}/pom.xml (95%) rename zeppelin-examples/{zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json => zeppelin-example-spell-translator/zeppelin-example-spell-translator.json} (84%) rename zeppelin-web/src/app/{frontend-interpreter => spell}/.npmignore (100%) rename zeppelin-web/src/app/{frontend-interpreter => spell}/index.js (85%) rename zeppelin-web/src/app/{frontend-interpreter => spell}/package.json (58%) rename zeppelin-web/src/app/{frontend-interpreter/frontend-interpreter-framework.js => spell/spell-base.js} (70%) rename zeppelin-web/src/app/{frontend-interpreter/frontend-interpreter-result.js => spell/spell-result.js} (93%) diff --git a/zeppelin-distribution/src/assemble/distribution.xml b/zeppelin-distribution/src/assemble/distribution.xml index 913b19f13dd..5c369e256a3 100644 --- a/zeppelin-distribution/src/assemble/distribution.xml +++ b/zeppelin-distribution/src/assemble/distribution.xml @@ -104,8 +104,8 @@ ../zeppelin-web/src/app/tabledata - /lib/node_modules/zeppelin-frontend-interpreter - ../zeppelin-web/src/app/frontend-interpreter + /lib/node_modules/zeppelin-spell + ../zeppelin-web/src/app/spell diff --git a/zeppelin-examples/pom.xml b/zeppelin-examples/pom.xml index 52e631752b8..e9f04731b80 100644 --- a/zeppelin-examples/pom.xml +++ b/zeppelin-examples/pom.xml @@ -36,8 +36,10 @@ zeppelin-example-clock zeppelin-example-horizontalbar - zeppelin-example-frontend-interpreter-flowchart - zeppelin-example-frontend-interpreter-translator + zeppelin-example-spell-flowchart + zeppelin-example-spell-translator + zeppelin-example-spell-markdown + zeppelin-example-spell-echo diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json deleted file mode 100644 index 3815baf160e..00000000000 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "echo-frontend-interpreter", - "description": "Frontend Echo Interpreter (example)", - "version": "1.0.0", - "main": "index", - "author": "", - "license": "Apache-2.0", - "dependencies": { - "zeppelin-frontend-interpreter": "*" - } -} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json deleted file mode 100644 index d4b5e31f305..00000000000 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "markdown-frontend-interpreter", - "description": "Frontend Markdown Interpreter (example)", - "version": "1.0.0", - "main": "index", - "author": "", - "license": "Apache-2.0", - "dependencies": { - "markdown": "0.5.0", - "zeppelin-frontend-interpreter": "*" - } -} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json b/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json deleted file mode 100644 index 838a65525ec..00000000000 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "translator-frontend-interpreter", - "description": "Frontend Translator Interpreter (example)", - "version": "1.0.0", - "main": "index", - "author": "", - "license": "Apache-2.0", - "dependencies": { - "whatwg-fetch": "^2.0.1", - "zeppelin-frontend-interpreter": "*" - } -} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js b/zeppelin-examples/zeppelin-example-spell-echo/index.js similarity index 79% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js rename to zeppelin-examples/zeppelin-example-spell-echo/index.js index 08189364c6e..955178e3f84 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/index.js +++ b/zeppelin-examples/zeppelin-example-spell-echo/index.js @@ -16,17 +16,17 @@ */ import { - AbstractFrontendInterpreter, - FrontendInterpreterResult, + SpellBase, + SpellResult, DefaultDisplayType, -} from 'zeppelin-frontend-interpreter'; +} from 'zeppelin-spell'; -export default class MarkdownInterpreter extends AbstractFrontendInterpreter { +export default class EchoSpell extends SpellBase { constructor() { super("%echo"); } interpret(paragraphText) { - return new FrontendInterpreterResult(paragraphText); + return new SpellResult(paragraphText); } } 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 00000000000..dfca708937e --- /dev/null +++ b/zeppelin-examples/zeppelin-example-spell-echo/package.json @@ -0,0 +1,11 @@ +{ + "name": "echo-spell", + "description": "Echo Spell (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "zeppelin-spell": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml similarity index 96% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml rename to zeppelin-examples/zeppelin-example-spell-echo/pom.xml index 1381648447a..348abd20354 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-echo/pom.xml @@ -27,10 +27,10 @@ org.apache.zeppelin - zeppelin-example-frontend-interpreter-echo + zeppelin-example-spell-echo jar 0.8.0-SNAPSHOT - Zeppelin: Example application - Frontend Echo Interpreter + Zeppelin: Example Spell - Echo diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json b/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json similarity index 85% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json rename to zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json index 36f87d4ef77..2759e0cecf2 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-echo/zeppelin-example-frontend-interpreter-echo.json +++ b/zeppelin-examples/zeppelin-example-spell-echo/zeppelin-example-spell-echo.json @@ -15,10 +15,10 @@ * limitations under the License. */ { - "type" : "FRONTEND_INTERPRETER", - "name" : "echo-frontend-interpreter", + "type" : "SPELL", + "name" : "echo-spell", "description" : "Return just what receive (example)", - "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-echo", + "artifact" : "./zeppelin-examples/zeppelin-example-spell-echo", "license" : "Apache-2.0", "icon" : "" } diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js b/zeppelin-examples/zeppelin-example-spell-flowchart/index.js similarity index 92% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js rename to zeppelin-examples/zeppelin-example-spell-flowchart/index.js index 38248e94812..655814a45c3 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/index.js +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/index.js @@ -16,14 +16,14 @@ */ import { - AbstractFrontendInterpreter, - FrontendInterpreterResult, + SpellBase, + SpellResult, DefaultDisplayType, -} from 'zeppelin-frontend-interpreter'; +} from 'zeppelin-spell'; import flowchart from 'flowchart.js'; -export default class FlowchartInterpreter extends AbstractFrontendInterpreter { +export default class FlowchartSpell extends SpellBase { constructor() { super("%flowchart"); } @@ -44,7 +44,7 @@ export default class FlowchartInterpreter extends AbstractFrontendInterpreter { * `interpret` method can return multiple results using `add()` * but now, we return just 1 result */ - return new FrontendInterpreterResult( + return new SpellResult( callback ); } diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json b/zeppelin-examples/zeppelin-example-spell-flowchart/package.json similarity index 53% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json rename to zeppelin-examples/zeppelin-example-spell-flowchart/package.json index b64cbe25de4..f6a21fd4ac6 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/package.json +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/package.json @@ -1,6 +1,6 @@ { - "name": "flowchart-frontend-interpreter", - "description": "Frontend Flowchart Interpreter (example)", + "name": "flowchart-spell", + "description": "Flowchart Spell (example)", "version": "1.0.0", "main": "index", "author": "", @@ -8,6 +8,6 @@ "dependencies": { "raphael": "2.2.0", "flowchart.js": "^1.6.5", - "zeppelin-frontend-interpreter": "*" + "zeppelin-spell": "*" } } diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml similarity index 95% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml rename to zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml index e1d44ae700f..b3575c99c6a 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/pom.xml @@ -27,10 +27,10 @@ org.apache.zeppelin - zeppelin-example-frontend-interpreter-markdown + zeppelin-example-spell-flowchart jar 0.8.0-SNAPSHOT - Zeppelin: Example application - Frontend Markdown Interpreter + Zeppelin: Example Spell - Flowchart diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json b/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json similarity index 84% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json rename to zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json index 73e1150636d..5d63d9a9ccf 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/zeppelin-example-frontend-interpreter-flowchart.json +++ b/zeppelin-examples/zeppelin-example-spell-flowchart/zeppelin-example-spell-flowchart.json @@ -15,10 +15,10 @@ * limitations under the License. */ { - "type" : "FRONTEND_INTERPRETER", - "name" : "flowchart-frontend-interpreter", + "type" : "SPELL", + "name" : "flowchart-spell", "description" : "with Flowchart.js (example)", - "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart", + "artifact" : "./zeppelin-examples/zeppelin-example-spell-flowchart", "license" : "Apache-2.0", "icon" : "" } diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js b/zeppelin-examples/zeppelin-example-spell-markdown/index.js similarity index 82% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js rename to zeppelin-examples/zeppelin-example-spell-markdown/index.js index 5c1be0e4f02..db7959f9a0c 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/index.js +++ b/zeppelin-examples/zeppelin-example-spell-markdown/index.js @@ -16,16 +16,16 @@ */ import { - AbstractFrontendInterpreter, - FrontendInterpreterResult, + SpellBase, + SpellResult, DefaultDisplayType, -} from 'zeppelin-frontend-interpreter'; +} from 'zeppelin-spell'; import md from 'markdown'; const markdown = md.markdown; -export default class MarkdownInterpreter extends AbstractFrontendInterpreter { +export default class MarkdownSpell extends SpellBase { constructor() { super("%markdown"); } @@ -37,6 +37,6 @@ export default class MarkdownInterpreter extends AbstractFrontendInterpreter { * specify `DefaultDisplayType.HTML` since `parsed` will contain DOM * otherwise it will be rendered as `DefaultDisplayType.TEXT` (default) */ - return new FrontendInterpreterResult(parsed, DefaultDisplayType.HTML); + return new SpellResult(parsed, DefaultDisplayType.HTML); } } 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 00000000000..a9efdf1917a --- /dev/null +++ b/zeppelin-examples/zeppelin-example-spell-markdown/package.json @@ -0,0 +1,12 @@ +{ + "name": "markdown-spell", + "description": "Markdown Spell (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "markdown": "0.5.0", + "zeppelin-spell": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml similarity index 95% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml rename to zeppelin-examples/zeppelin-example-spell-markdown/pom.xml index 56d3d2df9ff..b615eadc8a7 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-flowchart/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-markdown/pom.xml @@ -27,10 +27,10 @@ org.apache.zeppelin - zeppelin-example-frontend-interpreter-flowchart + zeppelin-example-spell-markdown jar 0.8.0-SNAPSHOT - Zeppelin: Example application - Frontend Flowchart Interpreter + Zeppelin: Example Spell - Markdown diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json b/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json similarity index 84% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json rename to zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json index b329d4c3f44..edf9a8f55ac 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-markdown/zeppelin-example-frontend-interpreter-markdown.json +++ b/zeppelin-examples/zeppelin-example-spell-markdown/zeppelin-example-spell-markdown.json @@ -15,10 +15,10 @@ * limitations under the License. */ { - "type" : "FRONTEND_INTERPRETER", - "name" : "markdown-frontend-interpreter", + "type" : "SPELL", + "name" : "markdown-spell", "description" : "with markdown-js (example)", - "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-markdown", + "artifact" : "./zeppelin-examples/zeppelin-example-spell-markdown", "license" : "Apache-2.0", "icon" : "" } diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js b/zeppelin-examples/zeppelin-example-spell-translator/index.js similarity index 81% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js rename to zeppelin-examples/zeppelin-example-spell-translator/index.js index 26f2fd2df9b..2b62e98a1b8 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/index.js +++ b/zeppelin-examples/zeppelin-example-spell-translator/index.js @@ -16,26 +16,28 @@ */ import { - AbstractFrontendInterpreter, - FrontendInterpreterResult, + SpellBase, + SpellResult, DefaultDisplayType, -} from 'zeppelin-frontend-interpreter'; +} from 'zeppelin-spell'; import 'whatwg-fetch'; -export default class TranslatorInterpreter extends AbstractFrontendInterpreter { +export default class TranslatorSpell extends SpellBase { constructor() { super("%translator"); } interpret(paragraphText) { /** - * FrontendInterpreterResult + * SpellResult * - accepts not only `string` but also `promise` - * - allows multiple output using the `add()` function + * - allows to add multiple output using the `add()` function */ - const result = new FrontendInterpreterResult() - .add('%html

Translation From English To Korean

') + const result = new SpellResult() + .add('

Translation From English To Korean

', DefaultDisplayType.HTML) + // or use display system implicitly like + // .add('%html

Translation From English To Korean

') .add(this.translate(paragraphText)); return result; } 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 00000000000..376dded9486 --- /dev/null +++ b/zeppelin-examples/zeppelin-example-spell-translator/package.json @@ -0,0 +1,12 @@ +{ + "name": "translator-spell", + "description": "Translator Spell (example)", + "version": "1.0.0", + "main": "index", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "whatwg-fetch": "^2.0.1", + "zeppelin-spell": "*" + } +} diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml similarity index 95% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml rename to zeppelin-examples/zeppelin-example-spell-translator/pom.xml index d572b5a830b..09e6daaad38 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/pom.xml +++ b/zeppelin-examples/zeppelin-example-spell-translator/pom.xml @@ -27,10 +27,10 @@ org.apache.zeppelin - zeppelin-example-frontend-interpreter-translator + zeppelin-example-spell-translator jar 0.8.0-SNAPSHOT - Zeppelin: Example application - Frontend Translator Interpreter + Zeppelin: Example Spell - Translator diff --git a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json b/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json similarity index 84% rename from zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json rename to zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json index 9c447a532fd..bd8d8ed52c2 100644 --- a/zeppelin-examples/zeppelin-example-frontend-interpreter-translator/zeppelin-example-frontend-interpreter-translator.json +++ b/zeppelin-examples/zeppelin-example-spell-translator/zeppelin-example-spell-translator.json @@ -15,10 +15,10 @@ * limitations under the License. */ { - "type" : "FRONTEND_INTERPRETER", - "name" : "translator-frontend-interpreter", + "type" : "SPELL", + "name" : "translator-spell", "description" : "with Google Translation API (examaple)", - "artifact" : "./zeppelin-examples/zeppelin-example-frontend-interpreter-translator", + "artifact" : "./zeppelin-examples/zeppelin-example-spell-translator", "license" : "Apache-2.0", "icon" : "" } 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 03d063474c3..fb9f6c3c757 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 @@ -41,7 +41,7 @@ public static enum Type { NOTEBOOK_REPO, APPLICATION, VISUALIZATION, - FRONTEND_INTERPRETER + SPELL } public HeliumPackage(Type type, @@ -83,7 +83,7 @@ public Type getType() { public static boolean isBundleType(Type type) { return (type == Type.VISUALIZATION || - type == Type.FRONTEND_INTERPRETER); + type == Type.SPELL); } public String getName() { 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 eab9eb77edf..2ce5c8cb364 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 @@ -114,13 +114,13 @@ public ZeppelinServer() throws Exception { 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-frontend-interpreter"))); + new File(conf.getRelativeDir("lib/node_modules/zeppelin-spell"))); } else { 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/frontend-interpreter"))); + new File(conf.getRelativeDir("zeppelin-web/src/app/spell"))); } this.helium = new Helium( diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index d1a93158f41..50c5c81b373 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -12,7 +12,7 @@ * limitations under the License. */ -import { FrontendInterpreterResult } from '../../frontend-interpreter' +import { SpellResult } from '../../spell' angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); @@ -225,15 +225,15 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat websocketMsgSrv.cancelParagraphRun(paragraph.id); }; - $scope.handleFrontendInterpreterError = function(error) { + $scope.handleSpellError = function(error) { $scope.paragraph.status = 'ERROR'; $scope.paragraph.errorMessage = error.stack; - console.error('Failed to execute FrontendInterpreter.interpret\n', error); + console.error('Failed to execute interpret() in spell\n', error); $scope.$digest(); }; - $scope.runParagraphUsingFrontendInterpreter = function(intp, paragraphText, - magic, digestRequired) { + $scope.runParagraphUsingSpell = function(spell, paragraphText, + magic, digestRequired) { $scope.paragraph.results = {}; if (digestRequired) { $scope.$digest(); } @@ -243,16 +243,16 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat // remove leading spaces const textWithoutMagic = splited[1].replace(/^\s+/g, ''); $scope.paragraph.status = 'FINISHED'; - const frontIntpResult = intp.interpret(textWithoutMagic); - const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( - heliumService.getAvailableFrontendInterpreters()); + const spellResult = spell.interpret(textWithoutMagic); + const parsed = spellResult.getAllParsedGeneratorsWithTypes( + heliumService.getAllSpells()); parsed.then(resultsMsg => { $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; if (digestRequired) { $scope.$digest(); } - }).catch($scope.handleFrontendInterpreterError); + }).catch($scope.handleSpellError); } catch (error) { - $scope.handleFrontendInterpreterError(error); + $scope.handleSpellError(error); } }; @@ -281,12 +281,12 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat return; } - const magic = FrontendInterpreterResult.extractMagic(paragraphText); - const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(magic); + const magic = SpellResult.extractMagic(paragraphText); + const spell = heliumService.getSpellByMagic(magic); - if (frontendIntp) { - $scope.runParagraphUsingFrontendInterpreter( - frontendIntp, paragraphText, magic, digestRequired); + if (spell) { + $scope.runParagraphUsingSpell( + spell, paragraphText, magic, digestRequired); } else { $scope.runParagraphUsingBackendInterpreter(paragraphText); } 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 cb32a3200a3..731267eaff6 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -21,8 +21,8 @@ import LinechartVisualization from '../../../visualization/builtins/visualizatio import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart'; import { DefaultDisplayType, - FrontendInterpreterResult, -} from '../../../frontend-interpreter' + SpellResult, +} from '../../../spell' angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl); @@ -297,12 +297,12 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location renderApp(`p${appState.id}`, appState); } else { if (!DefaultDisplayType[type]) { - const frontendIntp = heliumService.getFrontendInterpreterUsingMagic(type); - if (!frontendIntp) { - console.error(`Unknown Display Type: ${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, frontendIntp); + $scope.renderCustomDisplay(type, data, spell); } else { const targetElemId = $scope.createDisplayDOMId(`p${$scope.id}`, type); $scope.renderDefaultDisplay(targetElemId, type, data, refresh); @@ -317,12 +317,12 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location /** * Render multiple sub results for custom display */ - $scope.renderCustomDisplay = function(type, data, frontendIntp) { + $scope.renderCustomDisplay = function(type, data, spell) { // get result from intp - const frontIntpResult = frontendIntp.interpret(data.trim()); - const parsed = frontIntpResult.getAllParsedGeneratorsWithTypes( - heliumService.getAvailableFrontendInterpreters()); + const spellResult = spell.interpret(data.trim()); + const parsed = spellResult.getAllParsedGeneratorsWithTypes( + heliumService.getAllSpells()); // custom display result can include multiple subset results parsed.then(dataWithTypes => { @@ -356,14 +356,14 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location * @param failureCallback */ const generateData = function(generator, type, successCallback, failureCallback) { - if (FrontendInterpreterResult.isFunctionGenerator(generator)) { + if (SpellResult.isFunctionGenerator(generator)) { try { successCallback(generator()); } catch (error) { failureCallback(error); console.error(`Failed to handle ${type} type, function generator\n`, error); } - } else if (FrontendInterpreterResult.isObjectGenerator(generator)) { + } else if (SpellResult.isObjectGenerator(generator)) { try { successCallback(generator); } catch (error) { diff --git a/zeppelin-web/src/app/frontend-interpreter/.npmignore b/zeppelin-web/src/app/spell/.npmignore similarity index 100% rename from zeppelin-web/src/app/frontend-interpreter/.npmignore rename to zeppelin-web/src/app/spell/.npmignore diff --git a/zeppelin-web/src/app/frontend-interpreter/index.js b/zeppelin-web/src/app/spell/index.js similarity index 85% rename from zeppelin-web/src/app/frontend-interpreter/index.js rename to zeppelin-web/src/app/spell/index.js index 7390041c811..8ec47532316 100644 --- a/zeppelin-web/src/app/frontend-interpreter/index.js +++ b/zeppelin-web/src/app/spell/index.js @@ -17,9 +17,9 @@ export { DefaultDisplayType, - FrontendInterpreterResult, -} from './frontend-interpreter-result'; + SpellResult, +} from './spell-result'; export { - AbstractFrontendInterpreter -} from './frontend-interpreter-framework'; + SpellBase, +} from './spell-base'; diff --git a/zeppelin-web/src/app/frontend-interpreter/package.json b/zeppelin-web/src/app/spell/package.json similarity index 58% rename from zeppelin-web/src/app/frontend-interpreter/package.json rename to zeppelin-web/src/app/spell/package.json index bc7b19fca1e..7003e062b5b 100644 --- a/zeppelin-web/src/app/frontend-interpreter/package.json +++ b/zeppelin-web/src/app/spell/package.json @@ -1,7 +1,7 @@ { - "name": "zeppelin-frontend-interpreter", - "description": "Frontend Interpreter API", - "version": "0.7.0-SNAPSHOT", + "name": "zeppelin-spell", + "description": "Zeppelin Spell Framework", + "version": "0.8.0-SNAPSHOT", "main": "index", "dependencies": { }, diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js b/zeppelin-web/src/app/spell/spell-base.js similarity index 70% rename from zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js rename to zeppelin-web/src/app/spell/spell-base.js index 22663ba6ed1..85c85e50b37 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-framework.js +++ b/zeppelin-web/src/app/spell/spell-base.js @@ -18,29 +18,27 @@ /*eslint-disable no-unused-vars */ import { DefaultDisplayType, - FrontendInterpreterResult, -} from './frontend-interpreter-result'; + SpellResult, +} from './spell-result'; /*eslint-enable no-unused-vars */ -export class AbstractFrontendInterpreter { +export class SpellBase { constructor(magic) { this.magic = magic; } /** - * Consumes text and return multiple interpreter results. - * This method should handle error properly to provide precise error message. + * Consumes text and return `SpellResult`. * * @param paragraphText {string} which doesn't include magic - * @return {Array} + * @return {SpellResult} */ interpret(paragraphText) { - /** implement this if you want to add a frontend interpreter */ - throw new Error('AbstractFrontendInterpreter.interpret is not overrided'); + throw new Error('SpellBase.interpret() should be overrided'); } /** - * return magic for this frontend interpreter. + * return magic for this spell. * (e.g `%flowchart`) * @return {string} */ diff --git a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js b/zeppelin-web/src/app/spell/spell-result.js similarity index 93% rename from zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js rename to zeppelin-web/src/app/spell/spell-result.js index e06459b5cf7..4d9c0291825 100644 --- a/zeppelin-web/src/app/frontend-interpreter/frontend-interpreter-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -62,7 +62,7 @@ export class GeneratorWithType { // create `GeneratorWithType` whenever see available display type. for(let i = 0; i < splited.length; i++) { const g = splited[i]; - const magic = FrontendInterpreterResult.extractMagic(g); + const magic = SpellResult.extractMagic(g); // create `GeneratorWithType` only if see new magic if (availableMagic(magic) && mergedGens.length > 0) { @@ -109,13 +109,13 @@ export class GeneratorWithType { let wrapped; - if (FrontendInterpreterResult.isFunctionGenerator(generator)) { + if (SpellResult.isFunctionGenerator(generator)) { // if generator is a function, we consider it as ELEMENT type. wrapped = new Promise((resolve) => { const result = [new GeneratorWithType(generator, DefaultDisplayType.ELEMENT)]; return resolve(result); }); - } else if (FrontendInterpreterResult.isPromiseGenerator(generator)) { + } else if (SpellResult.isPromiseGenerator(generator)) { // if generator is a promise, wrapped = generator.then(generated => { const result = @@ -151,7 +151,7 @@ export class GeneratorWithType { /** * Value of `type` might be empty which means * generator can be splited into multiple generators - * by `FrontendInterpreterResult.parseMultipleGenerators()` + * by `SpellResult.parseMultipleGenerators()` * @returns {string} */ getType() { @@ -159,7 +159,7 @@ export class GeneratorWithType { } } -export class FrontendInterpreterResult { +export class SpellResult { constructor(resultGenerator, resultType) { this.generatorsWithTypes = []; this.add(resultGenerator, resultType); @@ -175,8 +175,8 @@ export class FrontendInterpreterResult { static isObjectGenerator(generator) { return (generator && - !FrontendInterpreterResult.isFunctionGenerator(generator) && - !FrontendInterpreterResult.isPromiseGenerator(generator)); + !SpellResult.isFunctionGenerator(generator) && + !SpellResult.isPromiseGenerator(generator)); } static extractMagic(allParagraphText) { diff --git a/zeppelin-web/src/components/helium/helium-type.js b/zeppelin-web/src/components/helium/helium-type.js index 7608aa31dd2..7b5a966e720 100644 --- a/zeppelin-web/src/components/helium/helium-type.js +++ b/zeppelin-web/src/components/helium/helium-type.js @@ -1,4 +1,4 @@ export const HeliumType = { VISUALIZATION: 'VISUALIZATION', - FRONTEND_INTERPRETER: 'FRONTEND_INTERPRETER', + SPELL: 'SPELL', } diff --git a/zeppelin-web/src/components/helium/helium.service.js b/zeppelin-web/src/components/helium/helium.service.js index 8f3c70a131d..f489f6d6328 100644 --- a/zeppelin-web/src/components/helium/helium.service.js +++ b/zeppelin-web/src/components/helium/helium.service.js @@ -28,7 +28,7 @@ import { HeliumType, } from './helium-type'; // name `heliumBundles` should be same as `HelumBundleFactory.HELIUM_BUNDLES_VAR` var heliumBundles = []; // map for `{ magic: interpreter }` - let frontendIntpWithMagic = {}; + let spellPerMagic = {}; let visualizationBundles = []; // load should be promise @@ -39,9 +39,9 @@ import { HeliumType, } from './helium-type'; // extract bundles by type heliumBundles.map(b => { - if (b.type === HeliumType.FRONTEND_INTERPRETER) { - const interpreter = new b.class(); // eslint-disable-line new-cap - frontendIntpWithMagic[interpreter.getMagic()] = interpreter; + 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); } @@ -53,17 +53,17 @@ import { HeliumType, } from './helium-type'; /** * @param magic {string} e.g `%flowchart` - * @returns {FrontendInterpreterBase} undefined for non-available magic + * @returns {SpellBase} undefined if magic is not registered */ - this.getFrontendInterpreterUsingMagic = function(magic) { - return frontendIntpWithMagic[magic]; + this.getSpellByMagic = function(magic) { + return spellPerMagic[magic]; }; /** - * @returns {Object} map for `{ magic : interpreter }` + * @returns {Object} map for `{ magic : spell }` */ - this.getAvailableFrontendInterpreters = function() { - return frontendIntpWithMagic; + this.getAllSpells = function() { + return spellPerMagic; }; this.getVisualizationBundles = function() { 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 index 23998c77e34..ae5ecd1451e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java @@ -50,7 +50,7 @@ public class HeliumBundleFactory { private final File workingDirectory; private File tabledataModulePath; private File visualizationModulePath; - private File frontendInterpreterModulePath; + private File spellModulePath; private Gson gson; String bundleCacheKey = ""; @@ -62,11 +62,11 @@ public HeliumBundleFactory( File moduleDownloadPath, File tabledataModulePath, File visualizationModulePath, - File frontendInterpreterModulePath) throws TaskRunnerException { + File spellModulePath) throws TaskRunnerException { this(moduleDownloadPath); this.tabledataModulePath = tabledataModulePath; this.visualizationModulePath = visualizationModulePath; - this.frontendInterpreterModulePath = frontendInterpreterModulePath; + this.spellModulePath = spellModulePath; } public HeliumBundleFactory(File moduleDownloadPath) throws TaskRunnerException { @@ -252,17 +252,17 @@ private void copyFrameworkModuleToInstallPath(FileFilter npmPackageCopyFilter) FileUtils.copyDirectory(visualizationModulePath, visModuleInstallPath, npmPackageCopyFilter); } - // install frontend-interpreter module - File frontendInterpreterModuleInstallPath = new File(workingDirectory, - "node_modules/zeppelin-frontend-interpreter"); - if (frontendInterpreterModulePath != null) { - if (frontendInterpreterModuleInstallPath.exists()) { - FileUtils.deleteDirectory(frontendInterpreterModuleInstallPath); + // install spell module + File spellModuleInstallPath = new File(workingDirectory, + "node_modules/zeppelin-spell"); + if (spellModulePath != null) { + if (spellModuleInstallPath.exists()) { + FileUtils.deleteDirectory(spellModuleInstallPath); } FileUtils.copyDirectory( - frontendInterpreterModulePath, - frontendInterpreterModuleInstallPath, + spellModulePath, + spellModuleInstallPath, npmPackageCopyFilter); } } 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 index 8cac5ed4c43..b6c1be16a3e 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/helium/HeliumBundleFactoryTest.java @@ -49,7 +49,7 @@ public void setUp() throws InstallationException, TaskRunnerException { hbf = new HeliumBundleFactory(tmpDir, new File(moduleDir, "tabledata"), new File(moduleDir, "visualization"), - new File(moduleDir, "frontend-interpreter")); + new File(moduleDir, "spell")); } @After From bd2b3ef84ebba1b055219d5d2b9f1ec176dd01d5 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 05:22:26 +0900 Subject: [PATCH 11/29] style: Rename generator -> data --- .../paragraph/paragraph.controller.js | 14 ++- .../paragraph/result/result.controller.js | 36 +++--- zeppelin-web/src/app/spell/spell-result.js | 119 +++++++++--------- 3 files changed, 86 insertions(+), 83 deletions(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 50c5c81b373..404a4bfe60d 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -225,11 +225,11 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat websocketMsgSrv.cancelParagraphRun(paragraph.id); }; - $scope.handleSpellError = function(error) { + $scope.handleSpellError = function(error, digestRequired) { $scope.paragraph.status = 'ERROR'; $scope.paragraph.errorMessage = error.stack; console.error('Failed to execute interpret() in spell\n', error); - $scope.$digest(); + if (digestRequired) { $scope.$digest(); } }; $scope.runParagraphUsingSpell = function(spell, paragraphText, @@ -244,15 +244,19 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const textWithoutMagic = splited[1].replace(/^\s+/g, ''); $scope.paragraph.status = 'FINISHED'; const spellResult = spell.interpret(textWithoutMagic); - const parsed = spellResult.getAllParsedGeneratorsWithTypes( + const parsed = spellResult.getAllParsedDataWithTypes( heliumService.getAllSpells()); + + // handle actual result message in promise parsed.then(resultsMsg => { $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; if (digestRequired) { $scope.$digest(); } - }).catch($scope.handleSpellError); + }).catch(error => { + $scope.handleSpellError(error, digestRequired); + }); } catch (error) { - $scope.handleSpellError(error); + $scope.handleSpellError(error, digestRequired); } }; 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 731267eaff6..471c7902d5d 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -321,7 +321,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // get result from intp const spellResult = spell.interpret(data.trim()); - const parsed = spellResult.getAllParsedGeneratorsWithTypes( + const parsed = spellResult.getAllParsedDataWithTypes( heliumService.getAllSpells()); // custom display result can include multiple subset results @@ -350,29 +350,29 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location * feed it to the success callback. * if error occurs, the error is passed to the failure callback * - * @param generator {Object or Function} + * @param data {Object or Function} * @param type {string} Display Type * @param successCallback * @param failureCallback */ - const generateData = function(generator, type, successCallback, failureCallback) { - if (SpellResult.isFunctionGenerator(generator)) { + const handleData = function(data, type, successCallback, failureCallback) { + if (SpellResult.isFunction(data)) { try { - successCallback(generator()); + successCallback(data()); } catch (error) { failureCallback(error); - console.error(`Failed to handle ${type} type, function generator\n`, error); + console.error(`Failed to handle ${type} type, function data\n`, error); } - } else if (SpellResult.isObjectGenerator(generator)) { + } else if (SpellResult.isObject(data)) { try { - successCallback(generator); + successCallback(data); } catch (error) { - console.error(`Failed to handle ${type} type, object generator\n`, error); + console.error(`Failed to handle ${type} type, object data\n`, error); } } }; - const renderElem = function(targetElemId, generator) { + const renderElem = function(targetElemId, data) { function retryRenderer() { const elem = angular.element(`#${targetElemId}`); if (!elem.length) { @@ -380,8 +380,8 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location return; } - generateData(() => { generator(targetElemId) }, DefaultDisplayType.ELEMENT, - () => {}, /** HTML element will be filled in generator . thus pass empty success callback */ + handleData(() => { data(targetElemId) }, DefaultDisplayType.ELEMENT, + () => {}, /** HTML element will be filled with data. thus pass empty success callback */ (error) => { elem.html(`${error.stack}`); } ); } @@ -389,7 +389,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location $timeout(retryRenderer); }; - const renderHtml = function(targetElemId, generator) { + const renderHtml = function(targetElemId, data) { function retryRenderer() { const elem = angular.element(`#${targetElemId}`); if (!elem.length) { @@ -397,7 +397,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location return; } - generateData(generator, DefaultDisplayType.HTML, + handleData(data, DefaultDisplayType.HTML, (generated) => { elem.html(generated); elem.find('pre code').each(function(i, e) { @@ -413,7 +413,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location $timeout(retryRenderer); }; - const renderAngular = function(targetElemId, generator) { + const renderAngular = function(targetElemId, data) { function retryRenderer() { const elem = angular.element(`#${targetElemId}`); if (!elem.length) { @@ -422,7 +422,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`); - generateData(generator, DefaultDisplayType.ANGULAR, + handleData(data, DefaultDisplayType.ANGULAR, (generated) => { elem.html(generated); $compile(elem.contents())(paragraphScope); @@ -438,7 +438,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location return angular.element('#p' + resultId + '_text'); }; - const renderText = function(targetElemId, generator) { + const renderText = function(targetElemId, data) { function retryRenderer() { const elem = angular.element(`#${targetElemId}`); if (!elem.length) { @@ -446,7 +446,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location return; } - generateData(generator, DefaultDisplayType.TEXT, + handleData(data, DefaultDisplayType.TEXT, (generated) => { // clear all lines before render clearTextOutput(); diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js index 4d9c0291825..7c5ad117c66 100644 --- a/zeppelin-web/src/app/spell/spell-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -31,10 +31,9 @@ export const DefaultDisplayMagic = { '%text': DefaultDisplayType.TEXT, }; -export class GeneratorWithType { - constructor(generator, type) { - // use variable name `data` to keep consistency with backend - this.data = generator; +export class DataWithType { + constructor(data, type) { + this.data = data; this.type = type; } @@ -48,86 +47,92 @@ export class GeneratorWithType { } } - static parseStringGenerator(generator, customDisplayMagic) { + /** + * consume 1 data and produce multiple + * @param data {string} + * @param customDisplayType + * @return {Array} + */ + static parseStringData(data, customDisplayMagic) { function availableMagic(magic) { return magic && (DefaultDisplayMagic[magic] || customDisplayMagic[magic]); } - const splited = generator.split('\n'); + const splited = data.split('\n'); const gensWithTypes = []; let mergedGens = []; let previousMagic = DefaultDisplayType.TEXT; - // create `GeneratorWithType` whenever see available display type. + // 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 `GeneratorWithType` only if see new magic + // create `DataWithType` only if see new magic if (availableMagic(magic) && mergedGens.length > 0) { - gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic)); + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); mergedGens = []; } - // accumulate `generator` to mergedGens + // accumulate `data` to mergedGens if (availableMagic(magic)) { const withoutMagic = g.split(magic)[1]; mergedGens.push(`${withoutMagic}\n`); - previousMagic = GeneratorWithType.handleDefaultMagic(magic); + previousMagic = DataWithType.handleDefaultMagic(magic); } else { mergedGens.push(`${g}\n`); } } - // cleanup the last `GeneratorWithType` + // cleanup the last `DataWithType` if (mergedGens.length > 0) { - previousMagic = GeneratorWithType.handleDefaultMagic(previousMagic); - gensWithTypes.push(new GeneratorWithType(mergedGens.join(''), previousMagic)); + previousMagic = DataWithType.handleDefaultMagic(previousMagic); + gensWithTypes.push(new DataWithType(mergedGens.join(''), previousMagic)); } return gensWithTypes; } /** - * get 1 `GeneratorWithType` and produce multiples using available displays + * 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 generatorWithType {GeneratorWithType} + * @param dataWithType {DataWithType} * @param availableDisplays {Object} Map for available displays - * @return {Promise>} + * @return {Promise>} */ - static produceMultipleGenerator(generatorWithType, customDisplayType) { - const generator = generatorWithType.getGenerator(); - const type = generatorWithType.getType(); + static produceMultipleData(dataWithType, customDisplayType) { + const data = dataWithType.getData(); + const type = dataWithType.getType(); // if the type is specified, just return it - // handle non-specified generatorWithTypes only + // handle non-specified dataWithTypes only if (type) { - return new Promise((resolve) => { resolve([generatorWithType]); }); + return new Promise((resolve) => { resolve([dataWithType]); }); } let wrapped; - if (SpellResult.isFunctionGenerator(generator)) { - // if generator is a function, we consider it as ELEMENT type. + if (SpellResult.isFunction(data)) { + // if data is a function, we consider it as ELEMENT type. wrapped = new Promise((resolve) => { - const result = [new GeneratorWithType(generator, DefaultDisplayType.ELEMENT)]; + const result = [new DataWithType(data, DefaultDisplayType.ELEMENT)]; return resolve(result); }); - } else if (SpellResult.isPromiseGenerator(generator)) { - // if generator is a promise, - wrapped = generator.then(generated => { + } else if (SpellResult.isPromise(data)) { + // if data is a promise, + wrapped = data.then(generated => { const result = - GeneratorWithType.parseStringGenerator(generated, customDisplayType); + DataWithType.parseStringData(generated, customDisplayType); return result; }) } else { - // if generator is a object, parse it to multiples + // if data is a object, parse it to multiples wrapped = new Promise((resolve) => { const result = - GeneratorWithType.parseStringGenerator(generator, customDisplayType); + DataWithType.parseStringData(data, customDisplayType); return resolve(result); }); } @@ -136,7 +141,7 @@ export class GeneratorWithType { } /** - * generator (`data`) can be promise, function or just object + * `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. @@ -144,14 +149,14 @@ export class GeneratorWithType { * will be called in `then()` of this promise. * @returns {*} `data` which can be object, function or promise. */ - getGenerator() { + getData() { return this.data; } /** * Value of `type` might be empty which means - * generator can be splited into multiple generators - * by `SpellResult.parseMultipleGenerators()` + * data can be separated into multiples + * by `SpellResult.parseStringData()` * @returns {string} */ getType() { @@ -160,23 +165,23 @@ export class GeneratorWithType { } export class SpellResult { - constructor(resultGenerator, resultType) { - this.generatorsWithTypes = []; - this.add(resultGenerator, resultType); + constructor(resultData, resultType) { + this.dataWithTypes = []; + this.add(resultData, resultType); } - static isFunctionGenerator(generator) { - return (generator && typeof generator === 'function'); + static isFunction(data) { + return (data && typeof data === 'function'); } - static isPromiseGenerator(generator) { - return (generator && typeof generator.then === 'function'); + static isPromise(data) { + return (data && typeof data.then === 'function'); } - static isObjectGenerator(generator) { - return (generator && - !SpellResult.isFunctionGenerator(generator) && - !SpellResult.isPromiseGenerator(generator)); + static isObject(data) { + return (data && + !SpellResult.isFunction(data) && + !SpellResult.isPromise(data)); } static extractMagic(allParagraphText) { @@ -193,17 +198,11 @@ export class SpellResult { return undefined; } - /** - * consume 1 generator and produce multiple - * @param generator {string} - * @param customDisplayType - * @return {Array} - */ - add(resultGenerator, resultType) { - if (resultGenerator) { - this.generatorsWithTypes.push( - new GeneratorWithType(resultGenerator, resultType)); + add(resultData, resultType) { + if (resultData) { + this.dataWithTypes.push( + new DataWithType(resultData, resultType)); } return this; @@ -211,11 +210,11 @@ export class SpellResult { /** * @param customDisplayType - * @return {Promise>} + * @return {Promise>} */ - getAllParsedGeneratorsWithTypes(customDisplayType) { - const promises = this.generatorsWithTypes.map(gt => { - return GeneratorWithType.produceMultipleGenerator(gt, customDisplayType); + getAllParsedDataWithTypes(customDisplayType) { + const promises = this.dataWithTypes.map(gt => { + return DataWithType.produceMultipleData(gt, customDisplayType); }); // some promises can include an array so we need to flatten them From 72aadbff94bdf33b856b713888090431ac6bd1ef Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 05:48:39 +0900 Subject: [PATCH 12/29] feat: Enhance translator spell --- .../index.js | 38 +++++++++++++++---- zeppelin-web/src/app/spell/spell-result.js | 4 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/zeppelin-examples/zeppelin-example-spell-translator/index.js b/zeppelin-examples/zeppelin-example-spell-translator/index.js index 2b62e98a1b8..834e7078e8e 100644 --- a/zeppelin-examples/zeppelin-example-spell-translator/index.js +++ b/zeppelin-examples/zeppelin-example-spell-translator/index.js @@ -29,30 +29,52 @@ export default class TranslatorSpell extends SpellBase { } interpret(paragraphText) { + const parsed = this.parseConfig(paragraphText); + const source = parsed.source; + const target = parsed.target; + const auth = parsed.auth; + const text = parsed.text; + /** - * SpellResult - * - accepts not only `string` but also `promise` + * 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('

Translation From English To Korean

', DefaultDisplayType.HTML) + .add('

Translation Result

', DefaultDisplayType.HTML) // or use display system implicitly like // .add('%html

Translation From English To Korean

') - .add(this.translate(paragraphText)); + .add(this.translate(source, target, auth, text)); return result; } - translate(text) { + 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 YOUR_ACCESS_KEY', + 'Authorization': `Bearer ${auth}`, }, body: JSON.stringify({ 'q': text, - 'source': 'en', - 'target': 'ko', + 'source': source, + 'target': target, 'format': 'text' }) }).then(response => { diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js index 7c5ad117c66..1e908e5c735 100644 --- a/zeppelin-web/src/app/spell/spell-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -185,9 +185,9 @@ export class SpellResult { } static extractMagic(allParagraphText) { - const intpNameRegexp = /^\s*%(\S+)\s*/g; + const pattern = /^\s*%(\S+)\s*/g; try { - let match = intpNameRegexp.exec(allParagraphText); + let match = pattern.exec(allParagraphText); if (match) { return `%${match[1].trim()}`; } From 3cdf2daebd8c8a4b80dd11b060b28788db53419d Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 07:01:34 +0900 Subject: [PATCH 13/29] fix: NPM installation error --- .../zeppelin/helium/HeliumBundleFactory.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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 index ae5ecd1451e..664030f06c0 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/helium/HeliumBundleFactory.java @@ -192,21 +192,33 @@ public boolean accept(File pathname) { copyFrameworkModuleToInstallPath(npmPackageCopyFilter); - out.reset(); try { - npmCommand("install"); + 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" + new String(out.toByteArray())); + "Can't create bundle: \n" + bundleStdoutResult); } - WebpackResult result = getWebpackResultFromOutput(new String(out.toByteArray())); + WebpackResult result = getWebpackResultFromOutput(bundleStdoutResult); if (result.errors.length > 0) { heliumBundle.delete(); throw new IOException(result.errors[0]); @@ -361,7 +373,7 @@ private String[] getNpmModuleNameAndVersion(HeliumPackage pkg) { } public synchronized void install(HeliumPackage pkg) throws TaskRunnerException { - npmCommand("install " + pkg.getArtifact()); + npmCommand("install " + pkg.getArtifact() + " npm install --loglevel=error"); } private void npmCommand(String args) throws TaskRunnerException { From 9fb74388d3d7de2d4cd027799a43bbbc66498093 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Tue, 24 Jan 2017 22:13:16 +0900 Subject: [PATCH 14/29] feat: Save spell result and propagate --- .../zeppelin/socket/NotebookServer.java | 60 +++- .../paragraph/paragraph.controller.js | 276 ++++++++++++------ .../paragraph/result/result.controller.js | 36 ++- zeppelin-web/src/app/spell/spell-result.js | 54 +++- .../websocketEvents.factory.js | 2 + .../websocketEvents/websocketMsg.service.js | 28 ++ .../zeppelin/notebook/socket/Message.java | 4 +- 7 files changed, 341 insertions(+), 119 deletions(-) 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 6e58e3df0da..42cc1eeb239 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 void onMessage(NotebookSocket conn, String msg) { 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; @@ -1574,6 +1577,45 @@ private void runAllParagraphs(NotebookSocket conn, HashSet userAndRoles, } } + private void broadcastSpellExecution(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) + throws IOException { + + final String paragraphId = (String) fromMessage.get("id"); + 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)); + return; + } + + String text = (String) fromMessage.get("paragraph"); + String title = (String) fromMessage.get("title"); + Status status = Status.valueOf((String) fromMessage.get("status")); + Map params = (Map) fromMessage.get("params"); + Map config = (Map) fromMessage.get("config"); + + Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, + text, title, params, config); + p.setStatus(status); + p.setResult(fromMessage.get("results")); + + 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 userAndRoles, Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); @@ -1600,8 +1642,7 @@ private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Not 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 +1651,30 @@ private void persistAndExecuteSingleParagraph(NotebookSocket conn, 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; } diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 404a4bfe60d..6897a5b6831 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -12,7 +12,9 @@ * limitations under the License. */ -import { SpellResult } from '../../spell' +import { + SpellResult, +} from '../../spell'; angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); @@ -225,15 +227,32 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat websocketMsgSrv.cancelParagraphRun(paragraph.id); }; - $scope.handleSpellError = function(error, digestRequired) { + $scope.propagateSpellResult = function(paragraphId, paragraphTitle, + paragraphText, paragraphResults, paragraphStatus, + paragraphConfig, paragraphSettingsParam) { + websocketMsgSrv.paragraphExecutedBySpell( + paragraphId, paragraphTitle, + paragraphText, paragraphResults, paragraphStatus, + paragraphConfig, paragraphSettingsParam); + }; + + $scope.handleSpellError = function(paragraphText, error, + digestRequired, propagated) { $scope.paragraph.status = 'ERROR'; $scope.paragraph.errorMessage = error.stack; 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, + $scope.paragraph.config, $scope.paragraph.settings.params); + } }; $scope.runParagraphUsingSpell = function(spell, paragraphText, - magic, digestRequired) { + magic, digestRequired, propagated) { $scope.paragraph.results = {}; if (digestRequired) { $scope.$digest(); } @@ -242,21 +261,34 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const splited = paragraphText.split(magic); // remove leading spaces const textWithoutMagic = splited[1].replace(/^\s+/g, ''); - $scope.paragraph.status = 'FINISHED'; const spellResult = spell.interpret(textWithoutMagic); const parsed = spellResult.getAllParsedDataWithTypes( - heliumService.getAllSpells()); + heliumService.getAllSpells(), magic, textWithoutMagic); // handle actual result message in promise parsed.then(resultsMsg => { + const status = 'FINISHED'; + $scope.paragraph.status = status; + $scope.paragraph.errorMessage = ''; + $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(error, digestRequired); + $scope.handleSpellError(paragraphText, error, + digestRequired, propagated); }); } catch (error) { - $scope.handleSpellError(error, digestRequired); + $scope.handleSpellError(paragraphText, error, + digestRequired, propagated); } }; @@ -280,7 +312,12 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat commitParagraph(paragraph); }; - $scope.runParagraph = function(paragraphText, digestRequired) { + /** + * @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; } @@ -290,7 +327,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat if (spell) { $scope.runParagraphUsingSpell( - spell, paragraphText, magic, digestRequired); + spell, paragraphText, magic, digestRequired, propagated); } else { $scope.runParagraphUsingBackendInterpreter(paragraphText); } @@ -312,12 +349,12 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat $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); + $scope.runParagraph(paragraphText, true, false); }; $scope.runParagraphFromButton = function(paragraphText) { - // we come here from `$scope.on`, so we don't need to call `$digest()` - $scope.runParagraph(paragraphText, false) + // we come here from the view, so we don't need to call `$digest()` + $scope.runParagraph(paragraphText, false, false) }; $scope.moveUp = function(paragraph) { @@ -1032,101 +1069,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; + } - 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.updateParagraph(oldPara, newPara, updateCallback) + }); + + $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 = (newPara.results && newPara.results.msg) ? + newPara.results.msg[i] : {}; + const newConfig = newPara.config.results ? newPara.config.results[i] : {}; + const oldConfig = newPara.config.results ? newPara.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) { 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 471c7902d5d..b252aff2eff 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -326,20 +326,30 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // custom display result can include multiple subset results parsed.then(dataWithTypes => { - const containerDOM = document.getElementById(`p${$scope.id}_custom`); - for(let i = 0; i < dataWithTypes.length; i++) { - const dt = dataWithTypes[i]; - const data = dt.data; - const type = dt.type; - - // prepare DOM to be filled - const subResultDOMId = $scope.createDisplayDOMId(`p${$scope.id}_custom`, type); - const subResultDOM = document.createElement('div'); - containerDOM.appendChild(subResultDOM); - subResultDOM.setAttribute('id', subResultDOMId); - - $scope.renderDefaultDisplay(subResultDOMId, type, data, true); + function retry() { + const containerDOM = angular.element(`#p${$scope.id}_custom`); + if (!containerDOM.length) { + $timeout(retry, 10); + return; + } + + // 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); + } } + + $timeout(retry); }).catch(error => { console.error(`Failed to render custom display: ${$scope.type}\n` + error); }); diff --git a/zeppelin-web/src/app/spell/spell-result.js b/zeppelin-web/src/app/spell/spell-result.js index 1e908e5c735..d62e97a8731 100644 --- a/zeppelin-web/src/app/spell/spell-result.js +++ b/zeppelin-web/src/app/spell/spell-result.js @@ -32,9 +32,20 @@ export const DefaultDisplayMagic = { }; export class DataWithType { - constructor(data, type) { + 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) { @@ -47,6 +58,17 @@ export class DataWithType { } } + 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} @@ -100,9 +122,12 @@ export class DataWithType { * object, function or promise * @param dataWithType {DataWithType} * @param availableDisplays {Object} Map for available displays + * @param magic + * @param textWithoutMagic * @return {Promise>} */ - static produceMultipleData(dataWithType, customDisplayType) { + static produceMultipleData(dataWithType, customDisplayType, + magic, textWithoutMagic) { const data = dataWithType.getData(); const type = dataWithType.getType(); @@ -117,7 +142,9 @@ export class DataWithType { if (SpellResult.isFunction(data)) { // if data is a function, we consider it as ELEMENT type. wrapped = new Promise((resolve) => { - const result = [new DataWithType(data, DefaultDisplayType.ELEMENT)]; + const dt = new DataWithType( + data, DefaultDisplayType.ELEMENT, magic, textWithoutMagic); + const result = [dt]; return resolve(result); }); } else if (SpellResult.isPromise(data)) { @@ -162,6 +189,14 @@ export class DataWithType { getType() { return this.type; } + + getMagic() { + return this.magic; + } + + getText() { + return this.text; + } } export class SpellResult { @@ -198,6 +233,11 @@ export class SpellResult { return undefined; } + static createPropagable(resultMsg) { + return resultMsg.map(dt => { + return DataWithType.createPropagable(dt); + }) + } add(resultData, resultType) { if (resultData) { @@ -210,11 +250,13 @@ export class SpellResult { /** * @param customDisplayType + * @param textWithoutMagic * @return {Promise>} */ - getAllParsedDataWithTypes(customDisplayType) { - const promises = this.dataWithTypes.map(gt => { - return DataWithType.produceMultipleData(gt, customDisplayType); + 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 diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 5436f34c495..aceffbbe30f 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') { diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index d597ff4ea0a..8f107fee026 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -12,6 +12,11 @@ * limitations under the License. */ +import { + SpellResult, + DefaultDisplayType, +} from '../../app/spell'; + angular.module('zeppelinWebApp').service('websocketMsgSrv', websocketMsgSrv); websocketMsgSrv.$inject = ['$rootScope', 'websocketEvents']; @@ -159,6 +164,29 @@ function websocketMsgSrv($rootScope, websocketEvents) { websocketEvents.sendNewEvent({op: 'CANCEL_PARAGRAPH', data: {id: paragraphId}}); }, + paragraphExecutedBySpell: function(paragraphId, paragraphTitle, + paragraphText, paragraphResultsMsg, paragraphStatus, + 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, + config: paragraphConfig, + params: paragraphParams + } + }); + }, + runParagraph: function(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams) { websocketEvents.sendNewEvent({ op: 'RUN_PARAGRAPH', 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 162baf8c5cd..a6d15462152 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 static enum OP { 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); From 1227d7dc566efca418564c2be487726056ce424e Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Wed, 25 Jan 2017 08:25:20 +0900 Subject: [PATCH 15/29] refactor: result controller retry --- .../paragraph/result/result.controller.js | 74 ++++++++----------- 1 file changed, 32 insertions(+), 42 deletions(-) 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 b252aff2eff..ce7b694a4ab 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -173,6 +173,20 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location renderResult($scope.type); }; + function retryUntilElemIsLoaded(targetElemId, callback) { + function retry() { + const elem = angular.element(`#${targetElemId}`); + if (!elem.length) { + $timeout(retry, 10); + return; + } + + callback(); + } + + $timeout(retry); + } + $scope.$on('updateResult', function(event, result, newConfig, paragraphRef, index) { if (paragraph.id !== paragraphRef.id || index !== resultIndex) { return; @@ -326,13 +340,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // custom display result can include multiple subset results parsed.then(dataWithTypes => { - function retry() { - const containerDOM = angular.element(`#p${$scope.id}_custom`); - if (!containerDOM.length) { - $timeout(retry, 10); - return; - } - + 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]; @@ -347,9 +357,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location $scope.renderDefaultDisplay(subResultDOMId, type, data, true); } - } + }; - $timeout(retry); + retryUntilElemIsLoaded(containerDOMId, afterLoaded); }).catch(error => { console.error(`Failed to render custom display: ${$scope.type}\n` + error); }); @@ -383,30 +393,20 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; const renderElem = function(targetElemId, data) { - function retryRenderer() { + const afterLoaded = () => { const elem = angular.element(`#${targetElemId}`); - if (!elem.length) { - $timeout(retryRenderer, 10); - return; - } - 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); }; const renderHtml = function(targetElemId, data) { - function retryRenderer() { + const afterLoaded = () => { const elem = angular.element(`#${targetElemId}`); - if (!elem.length) { - $timeout(retryRenderer, 10); - return; - } - handleData(data, DefaultDisplayType.HTML, (generated) => { elem.html(generated); @@ -418,19 +418,14 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }, (error) => { elem.html(`${error.stack}`); } ); - } + }; - $timeout(retryRenderer); + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; const renderAngular = function(targetElemId, data) { - function retryRenderer() { + const afterLoaded = () => { const elem = angular.element(`#${targetElemId}`); - if (!elem.length) { - $timeout(retryRenderer, 10); - return; - } - const paragraphScope = noteVarShareService.get(`${paragraph.id}_paragraphScope`); handleData(data, DefaultDisplayType.ANGULAR, (generated) => { @@ -439,9 +434,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }, (error) => { elem.html(`${error.stack}`); } ); - } + }; - $timeout(retryRenderer); + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; const getTextResultElem = function (resultId) { @@ -449,13 +444,8 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; const renderText = function(targetElemId, data) { - function retryRenderer() { + const afterLoaded = () => { const elem = angular.element(`#${targetElemId}`); - if (!elem.length) { - $timeout(retryRenderer, 10); - return; - } - handleData(data, DefaultDisplayType.TEXT, (generated) => { // clear all lines before render @@ -467,9 +457,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }, (error) => { elem.html(`${error.stack}`); } ); - } + }; - $timeout(retryRenderer); + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; var clearTextOutput = function() { @@ -1054,4 +1044,4 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } } }); -}; +} From 4fec44cc4672d767071f8d63450dccdcee0814cf Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Wed, 25 Jan 2017 09:26:03 +0900 Subject: [PATCH 16/29] refactor: NotebookServer.java --- .../zeppelin/socket/NotebookServer.java | 209 ++++++++++-------- .../websocketEvents/websocketMsg.service.js | 5 - 2 files changed, 121 insertions(+), 93 deletions(-) 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 42cc1eeb239..b92b17d947e 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 @@ -701,6 +701,63 @@ void permissionError(NotebookSocket conn, String op, String userName, Set 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 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 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 userAndRoles, Notebook notebook, Message fromMessage) throws IOException { @@ -716,13 +773,13 @@ private void sendNote(NotebookSocket conn, HashSet userAndRoles, Noteboo 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()) { @@ -746,12 +803,11 @@ private void sendHomeNote(NotebookSocket conn, HashSet userAndRoles, Not } 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); @@ -773,10 +829,8 @@ private void updateNote(NotebookSocket conn, HashSet userAndRoles, Noteb 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; } @@ -807,10 +861,8 @@ private void updatePersonalizedMode(NotebookSocket conn, HashSet userAnd 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; } @@ -839,10 +891,8 @@ private void renameNote(NotebookSocket conn, HashSet userAndRoles, 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; } @@ -873,12 +923,10 @@ private void renameFolder(NotebookSocket conn, HashSet userAndRoles, 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; } } @@ -963,11 +1011,8 @@ private void removeNote(NotebookSocket conn, HashSet userAndRoles, Noteb 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; } @@ -985,13 +1030,12 @@ private void removeFolder(NotebookSocket conn, HashSet userAndRoles, return; } - NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization(); List 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; } } @@ -1110,17 +1154,16 @@ private void updateParagraph(NotebookSocket conn, HashSet userAndRoles, Map params = (Map) fromMessage.get("params"); Map config = (Map) 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()); } @@ -1157,14 +1200,13 @@ private void clearAllParagraphOutput(NotebookSocket conn, HashSet userAn 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); } @@ -1196,17 +1238,16 @@ private void removeParagraph(NotebookSocket conn, HashSet userAndRoles, 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) { @@ -1222,14 +1263,14 @@ private void clearParagraphOutput(NotebookSocket conn, HashSet userAndRo 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); @@ -1473,14 +1514,13 @@ private void moveParagraph(NotebookSocket conn, HashSet userAndRoles, No 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(), @@ -1492,11 +1532,10 @@ private String insertParagraph(NotebookSocket conn, HashSet userAndRoles 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; } @@ -1527,14 +1566,13 @@ private void cancelParagraph(NotebookSocket conn, HashSet userAndRoles, } 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(); } @@ -1547,11 +1585,8 @@ private void runAllParagraphs(NotebookSocket conn, HashSet userAndRoles, 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; } @@ -1570,6 +1605,7 @@ private void runAllParagraphs(NotebookSocket conn, HashSet userAndRoles, Map params = (Map) raw.get("params"); Map config = (Map) raw.get("config"); + Note note = notebook.getNote(noteId); Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); @@ -1587,11 +1623,9 @@ private void broadcastSpellExecution(NotebookSocket conn, HashSet userAn } 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; } @@ -1601,6 +1635,7 @@ private void broadcastSpellExecution(NotebookSocket conn, HashSet userAn Map params = (Map) fromMessage.get("params"); Map config = (Map) fromMessage.get("config"); + final Note note = notebook.getNote(noteId); Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); p.setStatus(status); @@ -1624,11 +1659,9 @@ private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Not } 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; } @@ -1636,6 +1669,8 @@ private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Not String title = (String) fromMessage.get("title"); Map params = (Map) fromMessage.get("params"); Map config = (Map) fromMessage.get("config"); + + final Note note = notebook.getNote(noteId); Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); @@ -1757,10 +1792,8 @@ private void setNoteRevision(NotebookSocket conn, HashSet userAndRoles, 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; } diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index 8f107fee026..c2e382e7a65 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -12,11 +12,6 @@ * limitations under the License. */ -import { - SpellResult, - DefaultDisplayType, -} from '../../app/spell'; - angular.module('zeppelinWebApp').service('websocketMsgSrv', websocketMsgSrv); websocketMsgSrv.$inject = ['$rootScope', 'websocketEvents']; From c8c8f0e1db41f7ab2e922efa55e5e8e3a403d575 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Wed, 25 Jan 2017 10:41:23 +0900 Subject: [PATCH 17/29] fix: Add setErrorMessage method to Job --- .../java/org/apache/zeppelin/scheduler/Job.java | 12 +++++++++++- .../apache/zeppelin/socket/NotebookServer.java | 3 ++- .../notebook/paragraph/paragraph.controller.js | 15 +++++++++------ .../websocketEvents/websocketMsg.service.js | 4 +++- 4 files changed, 25 insertions(+), 9 deletions(-) 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 a690befcb8a..76d90b9877d 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 boolean isPending() { } } - private String jobName; String id; @@ -135,6 +134,13 @@ public Status getStatus() { 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 Date getDateFinished() { } public abstract void setResult(Object results); + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } } 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 b92b17d947e..68b015d0bac 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 @@ -1638,8 +1638,9 @@ private void broadcastSpellExecution(NotebookSocket conn, HashSet userAn final Note note = notebook.getNote(noteId); Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); - p.setStatus(status); p.setResult(fromMessage.get("results")); + p.setErrorMessage((String) fromMessage.get("errorMessage")); + p.setStatusWithoutNotification(status); addNewParagraphIfLastParagraphIsExecuted(note, p); if (!persistNoteWithAuthInfo(conn, note, p)) { diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 6897a5b6831..138a65d54a8 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -228,25 +228,28 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat }; $scope.propagateSpellResult = function(paragraphId, paragraphTitle, - paragraphText, paragraphResults, paragraphStatus, + paragraphText, paragraphResults, + paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphSettingsParam) { websocketMsgSrv.paragraphExecutedBySpell( paragraphId, paragraphTitle, - paragraphText, paragraphResults, paragraphStatus, + paragraphText, paragraphResults, + paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphSettingsParam); }; $scope.handleSpellError = function(paragraphText, error, digestRequired, propagated) { + const errorMessage = error.stack; $scope.paragraph.status = 'ERROR'; - $scope.paragraph.errorMessage = error.stack; + $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, + paragraphText, [], $scope.paragraph.status, errorMessage, $scope.paragraph.config, $scope.paragraph.settings.params); } }; @@ -254,6 +257,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat $scope.runParagraphUsingSpell = function(spell, paragraphText, magic, digestRequired, propagated) { $scope.paragraph.results = {}; + $scope.paragraph.errorMessage = ''; if (digestRequired) { $scope.$digest(); } try { @@ -269,7 +273,6 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat parsed.then(resultsMsg => { const status = 'FINISHED'; $scope.paragraph.status = status; - $scope.paragraph.errorMessage = ''; $scope.paragraph.results.code = status; $scope.paragraph.results.msg = resultsMsg; $scope.paragraph.config.tableHide = false; @@ -279,7 +282,7 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat const propagable = SpellResult.createPropagable(resultsMsg); $scope.propagateSpellResult( $scope.paragraph.id, $scope.paragraph.title, - paragraphText, propagable, status, + paragraphText, propagable, status, '', $scope.paragraph.config, $scope.paragraph.settings.params); } }).catch(error => { diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index c2e382e7a65..4fd4b95687f 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -160,7 +160,8 @@ function websocketMsgSrv($rootScope, websocketEvents) { }, paragraphExecutedBySpell: function(paragraphId, paragraphTitle, - paragraphText, paragraphResultsMsg, paragraphStatus, + paragraphText, paragraphResultsMsg, + paragraphStatus, paragraphErrorMessage, paragraphConfig, paragraphParams) { websocketEvents.sendNewEvent({ op: 'PARAGRAPH_EXECUTED_BY_SPELL', @@ -176,6 +177,7 @@ function websocketMsgSrv($rootScope, websocketEvents) { }) }, status: paragraphStatus, + errorMessage: paragraphErrorMessage, config: paragraphConfig, params: paragraphParams } From fc4389e7e65e2a35b9325672b29f717584593697 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Wed, 25 Jan 2017 12:13:29 +0900 Subject: [PATCH 18/29] fix: Resolve RAT issues --- pom.xml | 1 + zeppelin-web/src/components/helium/helium-type.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index 7772369bd74..a677443787c 100644 --- a/pom.xml +++ b/pom.xml @@ -893,6 +893,7 @@ conf/notebook-authorization.json conf/credentials.json conf/zeppelin-env.sh + conf/helium.json spark-*-bin*/** .spark-dist/** **/interpreter-setting.json diff --git a/zeppelin-web/src/components/helium/helium-type.js b/zeppelin-web/src/components/helium/helium-type.js index 7b5a966e720..0ef4eb67a62 100644 --- a/zeppelin-web/src/components/helium/helium-type.js +++ b/zeppelin-web/src/components/helium/helium-type.js @@ -1,3 +1,17 @@ +/* + * 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', From 0f2d8b6573ef4157fca8eeab2d57082525c863b8 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Thu, 26 Jan 2017 02:56:47 +0900 Subject: [PATCH 19/29] fix: Resolve output issue --- .../app/notebook/paragraph/paragraph.controller.js | 6 +++--- .../notebook/paragraph/result/result.controller.js | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 138a65d54a8..7d4fb129f9d 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -1199,10 +1199,10 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat if (newPara.results && newPara.results.msg) { for (let i in newPara.results.msg) { const newResult = newPara.results.msg ? newPara.results.msg[i] : {}; - const oldResult = (newPara.results && 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 = 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)); 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 ce7b694a4ab..13a7ff501f0 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -155,6 +155,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location $scope.imageData; $scope.textRendererInitialized = false; + // queue for append output + const textAppendQueueBeforeInitialize = []; + $scope.init = function(result, config, paragraph, index) { // register helium plugin vis var visBundles = heliumService.getVisualizationBundles(); @@ -191,6 +194,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location 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) || @@ -451,8 +455,12 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // clear all lines before render clearTextOutput(); $scope.textRendererInitialized = true; - if (generated) { appendTextOutput(generated); } - else { flushAppendQueue(); } + + if (generated) { + const divDOM = angular.element('
').text(generated); + elem.append(divDOM); + } + elem.bind('mousewheel', (e) => { $scope.keepScrollDown = false; }); }, (error) => { elem.html(`${error.stack}`); } @@ -469,8 +477,6 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } }; - var textAppendQueueBeforeInitialize = []; - var flushAppendQueue = function() { while (textAppendQueueBeforeInitialize.length > 0) { appendTextOutput(textAppendQueueBeforeInitialize.pop()); From 69ce880c7654dff7f094dc2594e792c244fecc31 Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Thu, 26 Jan 2017 13:54:08 +0900 Subject: [PATCH 20/29] fix: Resolve append (stream) output --- .../paragraph/paragraph.controller.js | 4 - .../paragraph/result/result.controller.js | 98 +++++++++---------- 2 files changed, 45 insertions(+), 57 deletions(-) diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 7d4fb129f9d..655f049414c 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -555,10 +555,6 @@ function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $locat } }; - // $scope.text = asdasd - // dirtyText = - - // originalText $scope.aceChanged = function(_, editor) { var session = editor.getSession(); var dirtyText = session.getValue(); 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 13a7ff501f0..6bd4f26737f 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -153,10 +153,9 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location // image data $scope.imageData; - $scope.textRendererInitialized = false; // queue for append output - const textAppendQueueBeforeInitialize = []; + const textResultQueueForAppend = []; $scope.init = function(result, config, paragraph, index) { // register helium plugin vis @@ -176,10 +175,14 @@ 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() { - const elem = angular.element(`#${targetElemId}`); - if (!elem.length) { + if (!isDOMLoaded(targetElemId)) { $timeout(retry, 10); return; } @@ -195,7 +198,6 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location 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); @@ -216,14 +218,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); } }); @@ -443,8 +441,8 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location retryUntilElemIsLoaded(targetElemId, afterLoaded); }; - const getTextResultElem = function (resultId) { - return angular.element('#p' + resultId + '_text'); + const getTextResultElemId = function (resultId) { + return `p${resultId}_text`; }; const renderText = function(targetElemId, data) { @@ -453,8 +451,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location handleData(data, DefaultDisplayType.TEXT, (generated) => { // clear all lines before render - clearTextOutput(); - $scope.textRendererInitialized = true; + removeChildrenDOM(targetElemId); if (generated) { const divDOM = angular.element('
').text(generated); @@ -470,37 +467,39 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location retryUntilElemIsLoaded(targetElemId, afterLoaded); }; - var clearTextOutput = function() { - var textEl = getTextResultElem($scope.id); - if (textEl.length) { - textEl.children().remove(); + const removeChildrenDOM = function(targetElemId) { + const elem = angular.element(`#${targetElemId}`); + if (elem.length) { + elem.children().remove(); } }; - var flushAppendQueue = function() { - while (textAppendQueueBeforeInitialize.length > 0) { - appendTextOutput(textAppendQueueBeforeInitialize.pop()); + function appendTextOutput(data) { + const elemId = getTextResultElemId($scope.id); + textResultQueueForAppend.push(data); + + // if DOM is not loaded, just push data and return + if (!isDOMLoaded(elemId)) { + return; } - }; - var appendTextOutput = function(msg) { - if (!$scope.textRendererInitialized) { - textAppendQueueBeforeInitialize.push(msg); - } else { - flushAppendQueue(); - var textEl = getTextResultElem($scope.id); - if (textEl.length) { - var lines = msg.split('\n'); - for (var i = 0; i < lines.length; i++) { - textEl.append(angular.element('
').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('
').text(lines[i])); } + if ($scope.keepScrollDown) { - var doc = getTextResultElem($scope.id); + const doc = angular.element(`#${elemId}`); doc[0].scrollTop = doc[0].scrollHeight; } } - }; + } $scope.renderGraph = function(targetElemId, type, refresh) { // set graph height @@ -851,23 +850,16 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; const renderApp = function(targetElemId, appState) { - function retryRenderer() { - var elem = angular.element(document.getElementById(targetElemId)); - console.log('retry renderApp %o', elem); - if (!elem.length) { - $timeout(retryRenderer, 1000); - } else { - try { - console.log('renderApp %o', appState); - elem.html(appState.output); - $compile(elem.contents())(getAppScope(appState)); - } catch (err) { - console.log('App rendering error %o', err); - } + const afterLoaded = () => { + try { + console.log('renderApp %o', appState); + elem.html(appState.output); + $compile(elem.contents())(getAppScope(appState)); + } catch (err) { + console.log('App rendering error %o', err); } - } - - $timeout(retryRenderer); + }; + retryUntilElemIsLoaded(targetElemId, afterLoaded); }; /* From 08eba10b2d25b234761dfadeb91f9e6154d0ae7b Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Thu, 26 Jan 2017 15:10:22 +0900 Subject: [PATCH 21/29] refactor: renderGraph in result.controller.js --- .../paragraph/result/result.controller.js | 192 +++++++++--------- .../app/notebook/paragraph/result/result.html | 3 +- 2 files changed, 92 insertions(+), 103 deletions(-) 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 6bd4f26737f..85a6a76d8dc 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -187,7 +187,8 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location return; } - callback(); + const elem = angular.element(`#${targetElemId}`); + callback(elem); } $timeout(retry); @@ -219,7 +220,7 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location resultIndex === data.index && (paragraph.status === 'RUNNING' || paragraph.status === 'PENDING')) { - if (DefaultDisplayType.TEXT != $scope.type) { + if (DefaultDisplayType.TEXT !== $scope.type) { $scope.type = DefaultDisplayType.TEXT; } appendTextOutput(data.data); @@ -501,110 +502,99 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location } } - $scope.renderGraph = function(targetElemId, type, refresh) { + $scope.renderGraph = function(graphElemId, graphMode, refresh) { // set graph height - var height = $scope.config.graph.height; - var targetElem = angular.element(`#${targetElemId}`); - targetElem.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); @@ -850,11 +840,11 @@ function ResultCtrl($scope, $rootScope, $route, $window, $routeParams, $location }; const renderApp = function(targetElemId, appState) { - const afterLoaded = () => { + const afterLoaded = (loadedElem) => { try { console.log('renderApp %o', appState); - elem.html(appState.output); - $compile(elem.contents())(getAppScope(appState)); + loadedElem.html(appState.output); + $compile(loadedElem.contents())(getAppScope(appState)); } catch (err) { console.log('App rendering error %o', err); } diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index 287cb65379a..5b251e5f50a 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 @@
+ ng-class="{'noOverflow': graphMode=='table'}">
From 4029c029c6b6f4bbf3757970e5a881ebb4a0081f Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Fri, 27 Jan 2017 03:56:50 +0900 Subject: [PATCH 22/29] fix: ParagraphIT, parameterizedQueryForm --- .../org/apache/zeppelin/integration/ParagraphActionsIT.java | 3 +-- .../notebook/paragraph/paragraph-parameterizedQueryForm.html | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) 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 feade7faa3d..458b8d46b74 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 void testClearOutputButton() throws Exception { 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 void testClearOutputButton() throws Exception { } catch (Exception e) { handleException("Exception in ParagraphActionsIT while testClearOutputButton ", e); } - } @Test diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html index 117e11c2ecc..65d13b704e3 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 @@