From 0c39614c3b98c2590a7ca3f6845b6c7ef6a10f1f Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Mon, 21 Oct 2019 20:40:36 +0800 Subject: [PATCH 01/19] zeppelin-frontend init commit --- zeppelin-frontend/.editorconfig | 15 + zeppelin-frontend/.gitignore | 48 ++ zeppelin-frontend/.prettierignore | 3 + zeppelin-frontend/.prettierrc | 15 + zeppelin-frontend/README.md | 27 + zeppelin-frontend/angular.json | 332 +++++++++++ zeppelin-frontend/browserslist | 12 + zeppelin-frontend/commitlint.config.js | 13 + zeppelin-frontend/e2e/protractor.conf.js | 32 ++ zeppelin-frontend/e2e/src/app.e2e-spec.ts | 23 + zeppelin-frontend/e2e/src/app.po.ts | 11 + zeppelin-frontend/e2e/tsconfig.json | 13 + zeppelin-frontend/karma.conf.js | 32 ++ zeppelin-frontend/package.json | 100 ++++ zeppelin-frontend/proxy.conf.js | 47 ++ zeppelin-frontend/screenshot.png | Bin 0 -> 220058 bytes .../src/app/app-routing.module.ts | 11 + zeppelin-frontend/src/app/app.component.html | 538 ++++++++++++++++++ zeppelin-frontend/src/app/app.component.less | 0 .../src/app/app.component.spec.ts | 35 ++ zeppelin-frontend/src/app/app.component.ts | 10 + zeppelin-frontend/src/app/app.module.ts | 18 + zeppelin-frontend/src/assets/.gitkeep | 0 .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 16 + zeppelin-frontend/src/favicon.ico | Bin 0 -> 948 bytes zeppelin-frontend/src/index.html | 13 + zeppelin-frontend/src/main.ts | 12 + zeppelin-frontend/src/polyfills.ts | 63 ++ zeppelin-frontend/src/styles.less | 1 + zeppelin-frontend/src/test.ts | 20 + zeppelin-frontend/tsconfig.app.json | 18 + zeppelin-frontend/tsconfig.json | 47 ++ zeppelin-frontend/tsconfig.spec.json | 18 + zeppelin-frontend/tslint.json | 141 +++++ zeppelin-frontend/webpack.partial.js | 15 + 36 files changed, 1702 insertions(+) create mode 100644 zeppelin-frontend/.editorconfig create mode 100644 zeppelin-frontend/.gitignore create mode 100644 zeppelin-frontend/.prettierignore create mode 100644 zeppelin-frontend/.prettierrc create mode 100644 zeppelin-frontend/README.md create mode 100644 zeppelin-frontend/angular.json create mode 100644 zeppelin-frontend/browserslist create mode 100644 zeppelin-frontend/commitlint.config.js create mode 100644 zeppelin-frontend/e2e/protractor.conf.js create mode 100644 zeppelin-frontend/e2e/src/app.e2e-spec.ts create mode 100644 zeppelin-frontend/e2e/src/app.po.ts create mode 100644 zeppelin-frontend/e2e/tsconfig.json create mode 100644 zeppelin-frontend/karma.conf.js create mode 100644 zeppelin-frontend/package.json create mode 100644 zeppelin-frontend/proxy.conf.js create mode 100644 zeppelin-frontend/screenshot.png create mode 100644 zeppelin-frontend/src/app/app-routing.module.ts create mode 100644 zeppelin-frontend/src/app/app.component.html create mode 100644 zeppelin-frontend/src/app/app.component.less create mode 100644 zeppelin-frontend/src/app/app.component.spec.ts create mode 100644 zeppelin-frontend/src/app/app.component.ts create mode 100644 zeppelin-frontend/src/app/app.module.ts create mode 100644 zeppelin-frontend/src/assets/.gitkeep create mode 100644 zeppelin-frontend/src/environments/environment.prod.ts create mode 100644 zeppelin-frontend/src/environments/environment.ts create mode 100644 zeppelin-frontend/src/favicon.ico create mode 100644 zeppelin-frontend/src/index.html create mode 100644 zeppelin-frontend/src/main.ts create mode 100644 zeppelin-frontend/src/polyfills.ts create mode 100644 zeppelin-frontend/src/styles.less create mode 100644 zeppelin-frontend/src/test.ts create mode 100644 zeppelin-frontend/tsconfig.app.json create mode 100644 zeppelin-frontend/tsconfig.json create mode 100644 zeppelin-frontend/tsconfig.spec.json create mode 100644 zeppelin-frontend/tslint.json create mode 100644 zeppelin-frontend/webpack.partial.js diff --git a/zeppelin-frontend/.editorconfig b/zeppelin-frontend/.editorconfig new file mode 100644 index 00000000000..c220b3125b7 --- /dev/null +++ b/zeppelin-frontend/.editorconfig @@ -0,0 +1,15 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset=utf-8 +end_of_line=lf +trim_trailing_whitespace=true +insert_final_newline=false +indent_style=space +indent_size=2 + + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/zeppelin-frontend/.gitignore b/zeppelin-frontend/.gitignore new file mode 100644 index 00000000000..121610363f1 --- /dev/null +++ b/zeppelin-frontend/.gitignore @@ -0,0 +1,48 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events.json +speed-measure-plugin.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +# +.env +package-lock.json +yarn.lock \ No newline at end of file diff --git a/zeppelin-frontend/.prettierignore b/zeppelin-frontend/.prettierignore new file mode 100644 index 00000000000..b9f1979fbdf --- /dev/null +++ b/zeppelin-frontend/.prettierignore @@ -0,0 +1,3 @@ +**/*.md +**/*.less +**/*.svg diff --git a/zeppelin-frontend/.prettierrc b/zeppelin-frontend/.prettierrc new file mode 100644 index 00000000000..4b9bb8a441b --- /dev/null +++ b/zeppelin-frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "htmlWhitespaceSensitivity": "ignore", + "overrides": [ + { + "files": ".prettierrc", + "options": { + "parser": "json" + } + } + ] +} diff --git a/zeppelin-frontend/README.md b/zeppelin-frontend/README.md new file mode 100644 index 00000000000..af201a3e63d --- /dev/null +++ b/zeppelin-frontend/README.md @@ -0,0 +1,27 @@ +# Zeppelin + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/zeppelin-frontend/angular.json b/zeppelin-frontend/angular.json new file mode 100644 index 00000000000..d8364ae818e --- /dev/null +++ b/zeppelin-frontend/angular.json @@ -0,0 +1,332 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "zeppelin": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "zeppelin", + "schematics": { + "@schematics/angular:component": { + "style": "less", + "skipTests": true, + "changeDetection": "OnPush" + }, + "ng-zorro-antd:component": { + "style": "less", + "skipTests": true, + "changeDetection": "OnPush", + "classnameWithModule": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:module": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "architect": { + "build": { + "builder": "ngx-build-plus:browser", + "options": { + "outputPath": "dist/zeppelin", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets", + { + "glob": "**/*", + "input": "./node_modules/mathjax", + "output": "/" + }, + { + "glob": "**/*", + "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", + "output": "/assets/" + } + ], + "styles": [ + "src/styles/theme/dark/antd-dark.less", + "src/styles/theme/light/antd-light.less", + "src/styles.less", + "./node_modules/highlight.js/styles/github.css", + "./node_modules/monaco-editor/min/vs/editor/editor.main.css" + ], + "stylePreprocessorOptions": { + "includePaths": [ + "src/styles/theme", + "src/styles/theme/dark", + "src/styles/theme/light" + ] + }, + "scripts": [ + "node_modules/mathjax/MathJax.js", + "node_modules/systemjs/dist/s.js", + "node_modules/systemjs/dist/extras/amd.js", + "node_modules/systemjs/dist/extras/named-register.js", + "node_modules/systemjs/dist/extras/use-default.js" + ] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5.2mb" + } + ] + } + } + }, + "serve": { + "builder": "ngx-build-plus:dev-server", + "options": { + "browserTarget": "zeppelin:build" + }, + "configurations": { + "production": { + "browserTarget": "zeppelin:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "zeppelin:build" + } + }, + "test": { + "builder": "ngx-build-plus:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.less" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "zeppelin-e2e": { + "root": "e2e/", + "projectType": "application", + "prefix": "", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "zeppelin:serve" + }, + "configurations": { + "production": { + "devServerTarget": "zeppelin:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "zeppelin-helium": { + "projectType": "library", + "root": "projects/zeppelin-helium", + "sourceRoot": "projects/zeppelin-helium/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/zeppelin-helium/tsconfig.lib.json", + "project": "projects/zeppelin-helium/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/zeppelin-helium/src/test.ts", + "tsConfig": "projects/zeppelin-helium/tsconfig.spec.json", + "karmaConfig": "projects/zeppelin-helium/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/zeppelin-helium/tsconfig.lib.json", + "projects/zeppelin-helium/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "helium-vis-example": { + "projectType": "library", + "root": "projects/helium-vis-example", + "sourceRoot": "projects/helium-vis-example/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/helium-vis-example/tsconfig.lib.json", + "project": "projects/helium-vis-example/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/helium-vis-example/src/test.ts", + "tsConfig": "projects/helium-vis-example/tsconfig.spec.json", + "karmaConfig": "projects/helium-vis-example/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/helium-vis-example/tsconfig.lib.json", + "projects/helium-vis-example/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "zeppelin-visualization": { + "projectType": "library", + "root": "projects/zeppelin-visualization", + "sourceRoot": "projects/zeppelin-visualization/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/zeppelin-visualization/tsconfig.lib.json", + "project": "projects/zeppelin-visualization/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/zeppelin-visualization/src/test.ts", + "tsConfig": "projects/zeppelin-visualization/tsconfig.spec.json", + "karmaConfig": "projects/zeppelin-visualization/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/zeppelin-visualization/tsconfig.lib.json", + "projects/zeppelin-visualization/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "zeppelin-sdk": { + "projectType": "library", + "root": "projects/zeppelin-sdk", + "sourceRoot": "projects/zeppelin-sdk/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/zeppelin-sdk/tsconfig.lib.json", + "project": "projects/zeppelin-sdk/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/zeppelin-sdk/src/test.ts", + "tsConfig": "projects/zeppelin-sdk/tsconfig.spec.json", + "karmaConfig": "projects/zeppelin-sdk/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/zeppelin-sdk/tsconfig.lib.json", + "projects/zeppelin-sdk/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "zeppelin" +} \ No newline at end of file diff --git a/zeppelin-frontend/browserslist b/zeppelin-frontend/browserslist new file mode 100644 index 00000000000..80848532e47 --- /dev/null +++ b/zeppelin-frontend/browserslist @@ -0,0 +1,12 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/zeppelin-frontend/commitlint.config.js b/zeppelin-frontend/commitlint.config.js new file mode 100644 index 00000000000..984584dd970 --- /dev/null +++ b/zeppelin-frontend/commitlint.config.js @@ -0,0 +1,13 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'body-max-line-length': [1, 'always', 100], + 'header-case': [1, 'always', 'kebab-case'], + 'scope-case': [1, 'always', 'kebab-case'], + 'type-enum': [ + 2, + 'always', + ['build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'release', 'style', 'test', 'chore', 'revert'] + ] + } +}; diff --git a/zeppelin-frontend/e2e/protractor.conf.js b/zeppelin-frontend/e2e/protractor.conf.js new file mode 100644 index 00000000000..73e4e6806cd --- /dev/null +++ b/zeppelin-frontend/e2e/protractor.conf.js @@ -0,0 +1,32 @@ +// @ts-check +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +/** + * @type { import("protractor").Config } + */ +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git a/zeppelin-frontend/e2e/src/app.e2e-spec.ts b/zeppelin-frontend/e2e/src/app.e2e-spec.ts new file mode 100644 index 00000000000..d788f85250e --- /dev/null +++ b/zeppelin-frontend/e2e/src/app.e2e-spec.ts @@ -0,0 +1,23 @@ +import { AppPage } from './app.po'; +import { browser, logging } from 'protractor'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getTitleText()).toEqual('zeppelin app is running!'); + }); + + afterEach(async () => { + // Assert that there are no errors emitted from the browser + const logs = await browser.manage().logs().get(logging.Type.BROWSER); + expect(logs).not.toContain(jasmine.objectContaining({ + level: logging.Level.SEVERE, + } as logging.Entry)); + }); +}); diff --git a/zeppelin-frontend/e2e/src/app.po.ts b/zeppelin-frontend/e2e/src/app.po.ts new file mode 100644 index 00000000000..b8498c26f24 --- /dev/null +++ b/zeppelin-frontend/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get(browser.baseUrl) as Promise; + } + + getTitleText() { + return element(by.css('app-root .content span')).getText() as Promise; + } +} diff --git a/zeppelin-frontend/e2e/tsconfig.json b/zeppelin-frontend/e2e/tsconfig.json new file mode 100644 index 00000000000..39b800f7896 --- /dev/null +++ b/zeppelin-frontend/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/e2e", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} diff --git a/zeppelin-frontend/karma.conf.js b/zeppelin-frontend/karma.conf.js new file mode 100644 index 00000000000..5051381686d --- /dev/null +++ b/zeppelin-frontend/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, './coverage/zeppelin'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/zeppelin-frontend/package.json b/zeppelin-frontend/package.json new file mode 100644 index 00000000000..109409d4ae9 --- /dev/null +++ b/zeppelin-frontend/package.json @@ -0,0 +1,100 @@ +{ + "name": "zeppelin", + "version": "0.0.0", + "scripts": { + "postinstall": "npm run build:projects", + "ng": "./node_modules/.bin/ng", + "start": "ng serve --proxy-config proxy.conf.js --extra-webpack-config webpack.partial.js", + "build": "npm run build:projects && ng build --prod --extra-webpack-config webpack.partial.js", + "build:projects": "npm run build-project:sdk && npm run build-project:vis && npm run build-project:helium", + "build-helium-vis-example": " ng build --project helium-vis-example", + "build-project:sdk": " ng build --project zeppelin-sdk", + "build-project:vis": " ng build --project zeppelin-visualization", + "build-project:helium": "ng build --project zeppelin-helium", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "~8.2.10", + "@angular/cdk": "~8.2.3", + "@angular/common": "~8.2.10", + "@angular/compiler": "~8.2.10", + "@angular/core": "~8.2.10", + "@angular/forms": "~8.2.10", + "@angular/platform-browser": "~8.2.10", + "@angular/platform-browser-dynamic": "~8.2.10", + "@angular/router": "~8.2.10", + "@antv/data-set": "^0.10.2", + "@antv/g2": "^3.5.4", + "@commitlint/cli": "^7.5.2", + "@commitlint/config-conventional": "^7.5.0", + "@commitlint/prompt-cli": "^7.5.0", + "ansi-to-html": "^0.6.11", + "core-js": "^2.5.4", + "date-fns": "^1.30.1", + "diff-match-patch": "^1.0.4", + "highlight.js": "^9.15.8", + "lodash": "^4.17.11", + "mathjax": "2.7.5", + "monaco-editor": "^0.18.1", + "ng-zorro-antd": "^8.4.0", + "rxjs": "~6.5.3", + "systemjs": "^5.0.0", + "tslib": "^1.9.0", + "xlsx": "^0.14.3", + "zone.js": "~0.9.1" + }, + "devDependencies": { + "monaco-editor-webpack-plugin": "^1.7.0", + "ngx-build-plus": "^8.1.5", + "@angular-devkit/build-angular": "^0.803.9", + "@angular-devkit/build-ng-packagr": "~0.803.6", + "@angular/cli": "~8.3.9", + "@angular/compiler-cli": "~8.2.10", + "@angular/language-service": "~8.2.10", + "@types/date-fns": "^2.6.0", + "@types/highlight.js": "^9.12.3", + "@types/jasmine": "~3.3.8", + "@types/jasminewd2": "~2.0.3", + "@types/lodash": "^4.14.124", + "@types/mathjax": "^0.0.35", + "@types/node": "~8.9.4", + "codelyzer": "^5.0.0", + "dotenv": "^8.0.0", + "https-proxy-agent": "^2.2.1", + "husky": "^2.2.0", + "jasmine-core": "~3.4.0", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~4.1.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.1", + "karma-jasmine": "~2.0.1", + "karma-jasmine-html-reporter": "^1.4.0", + "lint-staged": "^8.1.6", + "ng-packagr": "^5.4.0", + "prettier": "^1.17.0", + "protractor": "~5.4.0", + "ts-node": "~7.0.0", + "tsickle": "^0.37.0", + "tslint": "~5.15.0", + "typescript": "~3.5.3" + }, + "lint-staged": { + "src/**/*.{ts,js,json}": [ + "./node_modules/.bin/prettier --write", + "git add" + ], + "src/**/*.ts": [ + "tslint --project src/tslint.json --fix", + "git add" + ] + }, + "husky": { + "hooks": { + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", + "pre-commit": "lint-staged" + } + } +} \ No newline at end of file diff --git a/zeppelin-frontend/proxy.conf.js b/zeppelin-frontend/proxy.conf.js new file mode 100644 index 00000000000..6f5f4431be6 --- /dev/null +++ b/zeppelin-frontend/proxy.conf.js @@ -0,0 +1,47 @@ +const dotenv = require('dotenv'); +const HttpsProxyAgent = require('https-proxy-agent'); +dotenv.config(); + +const proxyConfig = [ + { + context: ['/'], + target: 'http://localhost:8080', + secure: false, + changeOrigin: true + }, + { + context: '/ws', + target: 'ws://localhost:8080', + secure: false, + ws:true, + changeOrigin: true + } +]; + +function httpUrlToWSUrl(url) { + return url.replace(/(http)(s)?\:\/\//, "ws$2://"); +} + +function setupForCorporateProxy(proxyConfig) { + const proxyServer = process.env.SERVER_PROXY; + const httpProxy = process.env.HTTP_PROXY; + if (proxyServer) { + let agent = null; + if (httpProxy) { + agent = new HttpsProxyAgent(httpProxy); + } + proxyConfig.forEach(function(entry) { + if (entry.context === '/ws') { + entry.target = httpUrlToWSUrl(proxyServer) + } else { + entry.target = proxyServer; + } + if (agent) { + entry.agent = agent; + } + }); + } + return proxyConfig; +} + +module.exports = setupForCorporateProxy(proxyConfig); diff --git a/zeppelin-frontend/screenshot.png b/zeppelin-frontend/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..4c157c5fdef1d16f1757fd8b744bbf3b41c90a6f GIT binary patch literal 220058 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%0D5iW% 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 literal 0 HcmV?d00001 diff --git a/zeppelin-frontend/src/app/app-routing.module.ts b/zeppelin-frontend/src/app/app-routing.module.ts new file mode 100644 index 00000000000..06c734263e1 --- /dev/null +++ b/zeppelin-frontend/src/app/app-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + + +const routes: Routes = []; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/zeppelin-frontend/src/app/app.component.html b/zeppelin-frontend/src/app/app.component.html new file mode 100644 index 00000000000..7feacadc636 --- /dev/null +++ b/zeppelin-frontend/src/app/app.component.html @@ -0,0 +1,538 @@ + + + + + + + + + + + + +

+ +
+ + +
+ + + + + + + + + + + + {{ title }} app is running! + + + + + +
+ + +

Resources

+

Here are some links to help you get started:

+ + + + +

Next Steps

+

What do you want to do next with your app?

+ + + +
+
+ + + New Component +
+ +
+ + + Angular Material +
+ +
+ + + Add Dependency +
+ +
+ + + Run and Watch Tests +
+ +
+ + + Build for Production +
+
+ + +
+
ng generate component xyz
+
ng add @angular/material
+
ng add _____
+
ng test
+
ng build --prod
+
+ + + + + + + + + + + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/zeppelin-frontend/src/app/app.component.less b/zeppelin-frontend/src/app/app.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/app.component.spec.ts b/zeppelin-frontend/src/app/app.component.spec.ts new file mode 100644 index 00000000000..ebd58d89220 --- /dev/null +++ b/zeppelin-frontend/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'zeppelin'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('zeppelin'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain('zeppelin app is running!'); + }); +}); diff --git a/zeppelin-frontend/src/app/app.component.ts b/zeppelin-frontend/src/app/app.component.ts new file mode 100644 index 00000000000..d5056928fc1 --- /dev/null +++ b/zeppelin-frontend/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.less'] +}) +export class AppComponent { + title = 'zeppelin'; +} diff --git a/zeppelin-frontend/src/app/app.module.ts b/zeppelin-frontend/src/app/app.module.ts new file mode 100644 index 00000000000..2c3ba2995c8 --- /dev/null +++ b/zeppelin-frontend/src/app/app.module.ts @@ -0,0 +1,18 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + AppRoutingModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/zeppelin-frontend/src/assets/.gitkeep b/zeppelin-frontend/src/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/environments/environment.prod.ts b/zeppelin-frontend/src/environments/environment.prod.ts new file mode 100644 index 00000000000..3612073bc31 --- /dev/null +++ b/zeppelin-frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/zeppelin-frontend/src/environments/environment.ts b/zeppelin-frontend/src/environments/environment.ts new file mode 100644 index 00000000000..7b4f817adb7 --- /dev/null +++ b/zeppelin-frontend/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/zeppelin-frontend/src/favicon.ico b/zeppelin-frontend/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 + + + + Zeppelin + + + + + + + + diff --git a/zeppelin-frontend/src/main.ts b/zeppelin-frontend/src/main.ts new file mode 100644 index 00000000000..c7b673cf44b --- /dev/null +++ b/zeppelin-frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/zeppelin-frontend/src/polyfills.ts b/zeppelin-frontend/src/polyfills.ts new file mode 100644 index 00000000000..aa665d6b874 --- /dev/null +++ b/zeppelin-frontend/src/polyfills.ts @@ -0,0 +1,63 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/zeppelin-frontend/src/styles.less b/zeppelin-frontend/src/styles.less new file mode 100644 index 00000000000..90d4ee0072c --- /dev/null +++ b/zeppelin-frontend/src/styles.less @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/zeppelin-frontend/src/test.ts b/zeppelin-frontend/src/test.ts new file mode 100644 index 00000000000..16317897b1c --- /dev/null +++ b/zeppelin-frontend/src/test.ts @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/zeppelin-frontend/tsconfig.app.json b/zeppelin-frontend/tsconfig.app.json new file mode 100644 index 00000000000..565a11a2156 --- /dev/null +++ b/zeppelin-frontend/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/test.ts", + "src/**/*.spec.ts" + ] +} diff --git a/zeppelin-frontend/tsconfig.json b/zeppelin-frontend/tsconfig.json new file mode 100644 index 00000000000..45f06cbf7de --- /dev/null +++ b/zeppelin-frontend/tsconfig.json @@ -0,0 +1,47 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@zeppelin/*": [ + "./src/app/*", + "./src/environments/*" + ], + "@zeppelin/helium": [ + "./dist/zeppelin-helium" + ], + "@zeppelin/helium/*": [ + "./dist/zeppelin-helium/*" + ], + "@zeppelin/visualization": [ + "dist/zeppelin-visualization" + ], + "@zeppelin/visualization/*": [ + "dist/zeppelin-visualization/*" + ], + "@zeppelin/sdk": [ + "dist/zeppelin-sdk" + ], + "@zeppelin/sdk/*": [ + "dist/zeppelin-sdk/*" + ] + }, + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "esnext", + "moduleResolution": "node", + "importHelpers": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2018", + "dom" + ] + } +} \ No newline at end of file diff --git a/zeppelin-frontend/tsconfig.spec.json b/zeppelin-frontend/tsconfig.spec.json new file mode 100644 index 00000000000..6400fde7d54 --- /dev/null +++ b/zeppelin-frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/zeppelin-frontend/tslint.json b/zeppelin-frontend/tslint.json new file mode 100644 index 00000000000..74aed2552ed --- /dev/null +++ b/zeppelin-frontend/tslint.json @@ -0,0 +1,141 @@ +{ + "rulesDirectory": ["node_modules/codelyzer"], + "rules": { + "banana-in-box": true, + "templates-no-negated-async": true, + "no-life-cycle-call": false, + "prefer-output-readonly": true, + "no-conflicting-life-cycle-hooks": false, + "enforce-component-selector": false, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": false, + "use-view-encapsulation": false, + "no-attribute-parameter-decorator": true, + "no-output-named-after-standard-event": true, + "no-output-rename": true, + "no-output-on-prefix": true, + "no-forward-ref": false, + "use-life-cycle-interface": true, + "contextual-life-cycle": true, + "trackBy-function": false, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "pipe-impure": true, + "angular-whitespace": [false, "check-interpolation"], + "directive-selector": [true, "attribute", ["zeppelin"], ["camelCase", "kebab-case"]], + "component-selector": [true, ["element", "attribute"], ["zeppelin"], "kebab-case"], + "callable-types": true, + "class-name": true, + "comment-format": [true, "check-space"], + "align": [true, "parameters", "statements"], + "array-type": [true, "array-simple"], + "arrow-return-shorthand": true, + "ban-types": [ + true, + ["Object", "Use {} instead."], + ["String", "Use string instead."], + ["Number", "Use number instead."], + ["Boolean", "Use boolean instead."], + ["Function", "Use specific callable interface instead."] + ], + "binary-expression-operand-order": true, + "curly": true, + "encoding": true, + "eofline": true, + "deprecation": { + "severity": "warn" + }, + "import-spacing": true, + "indent": [true, "spaces"], + "interface-name": [true, "never-prefix"], + "interface-over-type-literal": true, + "label-position": true, + "new-parens": true, + "no-angle-bracket-type-assertion": true, + "member-access": false, + "member-ordering": [ + true, + { + "order": ["static-field", "instance-field", "static-method", "instance-method"] + } + ], + "no-any": true, + "no-arg": true, + "no-bitwise": false, + "no-consecutive-blank-lines": [true], + "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], + "no-duplicate-variable": true, + "no-conditional-assignment": true, + "no-construct": true, + "no-debugger": true, + "no-duplicate-imports": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-floating-promises": false, + "no-for-in-array": true, + "no-import-side-effect": true, + "no-inferrable-types": [true, "ignore-params", "ignore-properties"], + "no-invalid-template-strings": true, + "no-invalid-this": true, + "no-irregular-whitespace": true, + "no-magic-numbers": false, + "no-misused-new": true, + "no-namespace": [true, "allow-declarations"], + "no-non-null-assertion": false, + "no-shadowed-variable": true, + "no-sparse-arrays": true, + "no-string-literal": true, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-this-assignment": true, + "no-trailing-whitespace": true, + "no-parameter-reassignment": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "number-literal-format": true, + "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], + "one-variable-per-declaration": [true, "ignore-for-loop"], + "ordered-imports": [ + true, + { + "import-sources-order": "lowercase-last", + "named-imports-order": "lowercase-first" + } + ], + "prefer-conditional-expression": false, + "prefer-const": true, + "prefer-method-signature": true, + "prefer-object-spread": true, + "prefer-template": [true, "allow-single-concat"], + "radix": true, + "trailing-comma": [ + true, + { + "multiline": "never", + "singleline": "never" + } + ], + "triple-equals": [true, "allow-null-check"], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "use-isnan": true, + "variable-name": [true, "ban-keywords", "allow-leading-underscore"], + "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], + "no-input-rename": true + } +} diff --git a/zeppelin-frontend/webpack.partial.js b/zeppelin-frontend/webpack.partial.js new file mode 100644 index 00000000000..3d9a5f5cf09 --- /dev/null +++ b/zeppelin-frontend/webpack.partial.js @@ -0,0 +1,15 @@ +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); + +module.exports = { + plugins: [ + new MonacoWebpackPlugin({ + languages: [ + 'bat', 'cpp', 'csharp', 'csp', 'css', 'dockerfile', 'go', 'handlebars', 'html', 'java', 'javascript', 'json', + 'less', 'lua', 'markdown', 'mysql', 'objective', 'perl', 'pgsql', 'php', 'powershell', 'python', 'r', 'ruby', + 'rust', 'scheme', 'scss', 'shell', 'sql', 'swift', 'typescript', 'vb', 'xml', 'yaml' + ], + features: ['!accessibilityHelp'] + }) + ] +}; + From 27c5bdf25ecfe668c1ed17e9de69fb5d81ab3d62 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Mon, 21 Oct 2019 20:43:02 +0800 Subject: [PATCH 02/19] feat: add common projects - zeppelin-helium - zeppelin-sdk - zeppelin-visualization --- .../projects/helium-vis-example/README.md | 24 + .../projects/helium-vis-example/karma.conf.js | 32 ++ .../helium-vis-example/ng-package.json | 7 + .../projects/helium-vis-example/package.json | 8 + .../src/json-vis.component.ts | 31 ++ .../helium-vis-example/src/json-vis.module.ts | 11 + .../src/json-visualization.ts | 52 ++ .../helium-vis-example/src/public-api.ts | 18 + .../projects/helium-vis-example/src/test.ts | 21 + .../helium-vis-example/tsconfig.lib.json | 26 + .../helium-vis-example/tsconfig.spec.json | 17 + .../projects/helium-vis-example/tslint.json | 17 + .../projects/zeppelin-helium/README.md | 24 + .../projects/zeppelin-helium/karma.conf.js | 32 ++ .../projects/zeppelin-helium/ng-package.json | 7 + .../projects/zeppelin-helium/package.json | 12 + .../zeppelin-helium/src/common-deps.ts | 31 ++ .../projects/zeppelin-helium/src/index.ts | 1 + .../zeppelin-helium/src/public-api.ts | 6 + .../projects/zeppelin-helium/src/test.ts | 21 + .../src/zeppelin-helium.module.ts | 4 + .../src/zeppelin-helium.service.ts | 84 +++ .../zeppelin-helium/tsconfig.lib.json | 27 + .../zeppelin-helium/tsconfig.spec.json | 17 + .../projects/zeppelin-helium/tslint.json | 17 + .../projects/zeppelin-sdk/README.md | 24 + .../projects/zeppelin-sdk/karma.conf.js | 32 ++ .../projects/zeppelin-sdk/ng-package.json | 7 + .../projects/zeppelin-sdk/package.json | 8 + .../projects/zeppelin-sdk/src/index.ts | 1 + .../zeppelin-sdk/src/interfaces/index.ts | 1 + .../interfaces/message-common.interface.ts | 117 +++++ .../message-data-type-map.interface.ts | 152 ++++++ .../message-interpreter.interface.ts | 58 +++ .../src/interfaces/message-job.interface.ts | 36 ++ .../interfaces/message-notebook.interface.ts | 197 +++++++ .../interfaces/message-operator.interface.ts | 470 +++++++++++++++++ .../interfaces/message-paragraph.interface.ts | 456 ++++++++++++++++ .../zeppelin-sdk/src/interfaces/public-api.ts | 8 + .../interfaces/websocket-message.interface.ts | 9 + .../projects/zeppelin-sdk/src/message.ts | 490 ++++++++++++++++++ .../projects/zeppelin-sdk/src/public-api.ts | 3 + .../projects/zeppelin-sdk/tsconfig.lib.json | 27 + .../projects/zeppelin-sdk/tsconfig.spec.json | 17 + .../projects/zeppelin-sdk/tslint.json | 17 + .../projects/zeppelin-visualization/README.md | 24 + .../zeppelin-visualization/karma.conf.js | 32 ++ .../zeppelin-visualization/ng-package.json | 7 + .../zeppelin-visualization/package.json | 8 + .../zeppelin-visualization/src/data-set.ts | 5 + .../src/g2-visualization-base.ts | 45 ++ .../src/g2-visualization-component-base.ts | 81 +++ .../zeppelin-visualization/src/index.ts | 1 + .../src/pivot-transformation.ts | 187 +++++++ .../zeppelin-visualization/src/public-api.ts | 13 + .../zeppelin-visualization/src/table-data.ts | 23 + .../src/table-transformation.ts | 14 + .../src/transformation.ts | 34 ++ .../src/visualization-component-portal.ts | 34 ++ .../src/visualization.ts | 32 ++ .../zeppelin-visualization/tsconfig.lib.json | 27 + .../zeppelin-visualization/tsconfig.spec.json | 17 + .../zeppelin-visualization/tslint.json | 17 + 63 files changed, 3278 insertions(+) create mode 100644 zeppelin-frontend/projects/helium-vis-example/README.md create mode 100644 zeppelin-frontend/projects/helium-vis-example/karma.conf.js create mode 100644 zeppelin-frontend/projects/helium-vis-example/ng-package.json create mode 100644 zeppelin-frontend/projects/helium-vis-example/package.json create mode 100644 zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts create mode 100644 zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts create mode 100644 zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts create mode 100644 zeppelin-frontend/projects/helium-vis-example/src/public-api.ts create mode 100644 zeppelin-frontend/projects/helium-vis-example/src/test.ts create mode 100644 zeppelin-frontend/projects/helium-vis-example/tsconfig.lib.json create mode 100644 zeppelin-frontend/projects/helium-vis-example/tsconfig.spec.json create mode 100644 zeppelin-frontend/projects/helium-vis-example/tslint.json create mode 100644 zeppelin-frontend/projects/zeppelin-helium/README.md create mode 100644 zeppelin-frontend/projects/zeppelin-helium/karma.conf.js create mode 100644 zeppelin-frontend/projects/zeppelin-helium/ng-package.json create mode 100644 zeppelin-frontend/projects/zeppelin-helium/package.json create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/index.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/test.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts create mode 100644 zeppelin-frontend/projects/zeppelin-helium/tsconfig.lib.json create mode 100644 zeppelin-frontend/projects/zeppelin-helium/tsconfig.spec.json create mode 100644 zeppelin-frontend/projects/zeppelin-helium/tslint.json create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/README.md create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/karma.conf.js create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/ng-package.json create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/package.json create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/index.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/message.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/tsconfig.lib.json create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/tsconfig.spec.json create mode 100644 zeppelin-frontend/projects/zeppelin-sdk/tslint.json create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/README.md create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/karma.conf.js create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/ng-package.json create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/package.json create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/index.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/tsconfig.lib.json create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/tsconfig.spec.json create mode 100644 zeppelin-frontend/projects/zeppelin-visualization/tslint.json diff --git a/zeppelin-frontend/projects/helium-vis-example/README.md b/zeppelin-frontend/projects/helium-vis-example/README.md new file mode 100644 index 00000000000..4553492bc84 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/README.md @@ -0,0 +1,24 @@ +# HeliumVisExample + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.9. + +## Code scaffolding + +Run `ng generate component component-name --project helium-vis-example` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project helium-vis-example`. +> Note: Don't forget to add `--project helium-vis-example` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build helium-vis-example` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build helium-vis-example`, go to the dist folder `cd dist/helium-vis-example` and run `npm publish`. + +## Running unit tests + +Run `ng test helium-vis-example` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/zeppelin-frontend/projects/helium-vis-example/karma.conf.js b/zeppelin-frontend/projects/helium-vis-example/karma.conf.js new file mode 100644 index 00000000000..dd5da0f117f --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/helium-vis-example'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/zeppelin-frontend/projects/helium-vis-example/ng-package.json b/zeppelin-frontend/projects/helium-vis-example/ng-package.json new file mode 100644 index 00000000000..2193ef3df33 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/helium-vis-example", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/helium-vis-example/package.json b/zeppelin-frontend/projects/helium-vis-example/package.json new file mode 100644 index 00000000000..17f7cb02fbc --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/package.json @@ -0,0 +1,8 @@ +{ + "name": "helium-vis-example", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^8.2.9", + "@angular/core": "^8.2.9" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts new file mode 100644 index 00000000000..e0da1129877 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { TableData, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +@Component({ + selector: 'lib-helium-vis-example', + template: ` +
{{tableData | json}}
+ `, + styles: [` + pre { + background: #fff7e7; + padding: 10px; + border: 1px solid #ffd278; + color: #fa7e14; + border-radius: 3px; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class JsonVisComponent implements OnInit { + tableData: TableData; + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) {} + + ngOnInit() { + } + + render(): void { + this.tableData = this.visualization.transformed; + } + +} diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts new file mode 100644 index 00000000000..7f0d109b907 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { JsonVisComponent } from './json-vis.component'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + imports: [CommonModule], + declarations: [JsonVisComponent], + entryComponents: [JsonVisComponent], + exports: [JsonVisComponent] +}) +export class JsonVisModule { } diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts new file mode 100644 index 00000000000..bd269599b50 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts @@ -0,0 +1,52 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { + TableTransformation, + Transformation, + Visualization, + VisualizationComponentPortal +} from '@zeppelin/visualization'; + +import { JsonVisComponent } from './json-vis.component'; + +export class JsonVisualization extends Visualization { + tableTransformation = new TableTransformation(this.getConfig()); + componentPortal = new VisualizationComponentPortal( + this, + JsonVisComponent, + this.portalOutlet, + this.viewContainerRef, + this.componentFactoryResolver + ); + constructor(config: GraphConfig, + private portalOutlet: CdkPortalOutlet, + private viewContainerRef: ViewContainerRef, + private componentFactoryResolver?: ComponentFactoryResolver) { + super(config); + } + + destroy(): void { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this.configChange$.complete(); + this.configChange$ = null; + } + + getTransformation(): Transformation { + return this.tableTransformation; + } + + refresh(): void {} + + render(data): void { + this.transformed = data; + if (!this.componentRef) { + this.componentRef = this.componentPortal.attachComponentPortal(); + } + this.componentRef.instance.render(); + } +} diff --git a/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts b/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts new file mode 100644 index 00000000000..c1f61fe8b01 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts @@ -0,0 +1,18 @@ +/* + * Public API Surface of helium-vis-example + */ + +import { createHeliumPackage, HeliumPackageType } from '@zeppelin/helium'; +import { JsonVisComponent } from './json-vis.component'; +import { JsonVisModule } from './json-vis.module'; +import { JsonVisualization } from './json-visualization'; + +export default createHeliumPackage({ + name: 'helium-vis-example', + id: 'heliumVisExample', + icon: 'appstore', + type: HeliumPackageType.Visualization, + module: JsonVisModule, + component: JsonVisComponent, + visualization: JsonVisualization +}); diff --git a/zeppelin-frontend/projects/helium-vis-example/src/test.ts b/zeppelin-frontend/projects/helium-vis-example/src/test.ts new file mode 100644 index 00000000000..978c64fb83f --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/src/test.ts @@ -0,0 +1,21 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/zeppelin-frontend/projects/helium-vis-example/tsconfig.lib.json b/zeppelin-frontend/projects/helium-vis-example/tsconfig.lib.json new file mode 100644 index 00000000000..bd23948e591 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/zeppelin-frontend/projects/helium-vis-example/tsconfig.spec.json b/zeppelin-frontend/projects/helium-vis-example/tsconfig.spec.json new file mode 100644 index 00000000000..16da33db072 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/zeppelin-frontend/projects/helium-vis-example/tslint.json b/zeppelin-frontend/projects/helium-vis-example/tslint.json new file mode 100644 index 00000000000..124133f8499 --- /dev/null +++ b/zeppelin-frontend/projects/helium-vis-example/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} diff --git a/zeppelin-frontend/projects/zeppelin-helium/README.md b/zeppelin-frontend/projects/zeppelin-helium/README.md new file mode 100644 index 00000000000..cd6d01d8109 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/README.md @@ -0,0 +1,24 @@ +# ZeppelinHelium + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.8. + +## Code scaffolding + +Run `ng generate component component-name --project zeppelin-helium` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project zeppelin-helium`. +> Note: Don't forget to add `--project zeppelin-helium` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build zeppelin-helium` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build zeppelin-helium`, go to the dist folder `cd dist/zeppelin-helium` and run `npm publish`. + +## Running unit tests + +Run `ng test zeppelin-helium` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/zeppelin-frontend/projects/zeppelin-helium/karma.conf.js b/zeppelin-frontend/projects/zeppelin-helium/karma.conf.js new file mode 100644 index 00000000000..55bfae21fc4 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/zeppelin-helium'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/zeppelin-frontend/projects/zeppelin-helium/ng-package.json b/zeppelin-frontend/projects/zeppelin-helium/ng-package.json new file mode 100644 index 00000000000..da717624c15 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/zeppelin-helium", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-helium/package.json b/zeppelin-frontend/projects/zeppelin-helium/package.json new file mode 100644 index 00000000000..cd0b499afa9 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/package.json @@ -0,0 +1,12 @@ +{ + "name": "@zeppelin/helium", + "version": "0.0.0", + "peerDependencies": { + "@angular/common": "~8.2.8", + "@angular/core": "~8.2.8", + "@angular/forms": "~8.2.7", + "@angular/router": "~8.2.7", + "rxjs": "~6.5.3", + "ng-zorro-antd": "^8.3.0" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts b/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts new file mode 100644 index 00000000000..020f5947eab --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts @@ -0,0 +1,31 @@ +import * as common from '@angular/common'; +import * as core from '@angular/core'; +import * as forms from '@angular/forms'; +import * as router from '@angular/router'; +import * as rxjs from 'rxjs'; + +import * as dataSet from '@antv/data-set'; +import * as g2 from '@antv/g2'; +import * as sdk from '@zeppelin/sdk'; +import * as visualization from '@zeppelin/visualization'; +import * as lodash from 'lodash'; + +import * as ngZorro from 'ng-zorro-antd'; +import * as tslib from 'tslib'; +import * as zeppelinHelium from './public-api'; + +export const COMMON_DEPS = { + '@angular/core': core, + '@angular/common': common, + '@angular/forms': forms, + '@angular/router': router, + '@antv/data-set': dataSet, + '@antv/g2': g2, + '@zeppelin/sdk': sdk, + '@zeppelin/visualization': visualization, + '@zeppelin/helium': zeppelinHelium, + 'lodash': lodash, + 'ng-zorro-antd': ngZorro, + rxjs, + tslib +}; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/index.ts b/zeppelin-frontend/projects/zeppelin-helium/src/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts new file mode 100644 index 00000000000..480ac3ebb15 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts @@ -0,0 +1,6 @@ +/* + * Public API Surface of zeppelin-helium + */ + +export * from './zeppelin-helium.service'; +export * from './zeppelin-helium.module'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/test.ts b/zeppelin-frontend/projects/zeppelin-helium/src/test.ts new file mode 100644 index 00000000000..978c64fb83f --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/test.ts @@ -0,0 +1,21 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts new file mode 100644 index 00000000000..e86e5039bf3 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts @@ -0,0 +1,4 @@ +import { NgModule } from '@angular/core'; + +@NgModule({}) +export class ZeppelinHeliumModule { } diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts new file mode 100644 index 00000000000..50f726b6a18 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts @@ -0,0 +1,84 @@ +import { Injectable, Type } from '@angular/core'; +import { Visualization } from '@zeppelin/visualization'; +import { COMMON_DEPS } from './common-deps'; +import { ZeppelinHeliumModule } from './zeppelin-helium.module'; + +// tslint:disable-next-line:no-any +const SystemJs = (window as any).System; + +// tslint:disable-next-line:no-any +export class ZeppelinHeliumPackage { + constructor( + public name: string, + public id: string, + // tslint:disable-next-line:no-any + public module: Type, + // tslint:disable-next-line:no-any + public component: Type, + // tslint:disable-next-line:no-any + public visualization?: any, + public icon = 'build' + ) { + } +} + +export enum HeliumPackageType { + Visualization +} + +// tslint:disable-next-line:no-any +export function createHeliumPackage(config: { + name: string; + id: string; + icon?: string; + type: HeliumPackageType; + // tslint:disable-next-line:no-any + module: Type; + // tslint:disable-next-line:no-any + component: Type; + // tslint:disable-next-line:no-any + visualization?: any +}) { + return new ZeppelinHeliumPackage( + config.name, + config.id, + config.module, + config.component, + config.visualization, + config.icon + ); +} + +@Injectable({ + providedIn: ZeppelinHeliumModule +}) +export class ZeppelinHeliumService { + + depsDefined = false; + + constructor() { } + + defineDeps() { + if (this.depsDefined) { + return; + } + Object.keys(COMMON_DEPS).forEach(externalKey => + // tslint:disable-next-line:no-any + (window as any).define(externalKey, [], () => COMMON_DEPS[ externalKey ]) + ); + this.depsDefined = true; + } + + loadPackage(name: string): Promise { + this.defineDeps(); + return SystemJs.import(`./assets/helium-packages/${name}.umd.js`) + .then(() => SystemJs.import(name)) + .then(plugin => { + if (plugin instanceof ZeppelinHeliumPackage) { + return Promise.resolve(plugin); + } else { + throw new TypeError('This module is not a valid helium package'); + } + }); + } +} diff --git a/zeppelin-frontend/projects/zeppelin-helium/tsconfig.lib.json b/zeppelin-frontend/projects/zeppelin-helium/tsconfig.lib.json new file mode 100644 index 00000000000..45b781973db --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/tsconfig.lib.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true, + "flatModuleId": "@zeppelin/helium" + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-helium/tsconfig.spec.json b/zeppelin-frontend/projects/zeppelin-helium/tsconfig.spec.json new file mode 100644 index 00000000000..16da33db072 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-helium/tslint.json b/zeppelin-frontend/projects/zeppelin-helium/tslint.json new file mode 100644 index 00000000000..124133f8499 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-helium/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/README.md b/zeppelin-frontend/projects/zeppelin-sdk/README.md new file mode 100644 index 00000000000..e4fce480e25 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/README.md @@ -0,0 +1,24 @@ +# ZeppelinSdk + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.9. + +## Code scaffolding + +Run `ng generate component component-name --project zeppelin-sdk` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project zeppelin-sdk`. +> Note: Don't forget to add `--project zeppelin-sdk` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build zeppelin-sdk` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build zeppelin-sdk`, go to the dist folder `cd dist/zeppelin-sdk` and run `npm publish`. + +## Running unit tests + +Run `ng test zeppelin-sdk` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/zeppelin-frontend/projects/zeppelin-sdk/karma.conf.js b/zeppelin-frontend/projects/zeppelin-sdk/karma.conf.js new file mode 100644 index 00000000000..8eb857ee837 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/zeppelin-sdk'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/ng-package.json b/zeppelin-frontend/projects/zeppelin-sdk/ng-package.json new file mode 100644 index 00000000000..41dc5a032d3 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/zeppelin-sdk", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-sdk/package.json b/zeppelin-frontend/projects/zeppelin-sdk/package.json new file mode 100644 index 00000000000..9be3b66d006 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/package.json @@ -0,0 +1,8 @@ +{ + "name": "@zeppelin/sdk", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^8.2.9", + "@angular/core": "^8.2.9" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts new file mode 100644 index 00000000000..d080a79bf4e --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts @@ -0,0 +1,117 @@ +export type EditorMode = + | 'ace/mode/scala' + | 'ace/mode/python' + | 'ace/mode/r' + | 'ace/mode/sql' + | 'ace/mode/markdown' + | 'ace/mode/sh'; + +export type EditorCompletionKey = 'TAB' | string; +export type EditorLanguage = 'scala' | 'python' | 'r' | 'sql' | 'markdown' | 'sh' | string; + +export interface Ticket { + principal: string; + ticket: string; + redirectURL?: string; + roles: string; +} + +export interface ConfigurationsInfo { + configurations: { + 'zeppelin.war.tempdir': string; + 'zeppelin.notebook.azure.user': string; + 'zeppelin.helium.npm.installer.url': string; + 'zeppelin.notebook.git.remote.username': string; + 'zeppelin.interpreter.remoterunner': string; + 'zeppelin.notebook.s3.user': string; + 'zeppelin.server.port': string; + 'zeppelin.plugins.dir': string; + 'zeppelin.notebook.new_format.delete_old': string; + 'zeppelin.ssl.truststore.type': string; + 'zeppelin.ssl.keystore.path': string; + 'zeppelin.notebook.s3.bucket': string; + 'zeppelin.notebook.git.remote.access-token': string; + 'zeppelin.recovery.dir': string; + 'zeppelin.notebook.s3.timeout': string; + 'zeppelin.notebook.cron.enable': string; + 'zeppelin.server.addr': string; + 'zeppelin.username.force.lowercase': string; + 'zeppelin.ssl.keystore.type': string; + 'zeppelin.ssl.truststore.path': string; + 'zeppelin.notebook.dir': string; + 'zeppelin.interpreter.lifecyclemanager.class': string; + 'zeppelin.notebook.gcs.dir': string; + 'zeppelin.notebook.s3.sse': string; + 'zeppelin.websocket.max.text.message.size': string; + 'zeppelin.notebook.git.remote.origin': string; + 'zeppelin.server.authorization.header.clear': string; + isRevisionSupported: string; + 'zeppelin.interpreter.dep.mvnRepo': string; + 'zeppelin.ssl': string; + 'zeppelin.notebook.autoInterpreterBinding': string; + 'zeppelin.config.storage.class': string; + 'zeppelin.helium.node.installer.url': string; + 'zeppelin.cluster.heartbeat.interval': string; + 'zeppelin.notebook.storage': string; + 'zeppelin.notebook.new_format.convert': string; + 'zeppelin.interpreter.dir': string; + 'zeppelin.anonymous.allowed': string; + 'zeppelin.credentials.persist': string; + 'zeppelin.notebook.mongo.uri': string; + 'zeppelin.config.fs.dir': string; + 'zeppelin.server.allowed.origins': string; + 'zeppelin.notebook.mongo.database': string; + 'zeppelin.encoding': string; + 'zeppelin.server.jetty.request.header.size': string; + 'zeppelin.search.temp.path': string; + 'zeppelin.cluster.heartbeat.timeout': string; + 'zeppelin.notebook.s3.endpoint': string; + 'zeppelin.notebook.homescreen.hide': string; + 'zeppelin.scheduler.threadpool.size': string; + 'zeppelin.notebook.azure.share': string; + 'zeppelin.helium.yarnpkg.installer.url': string; + 'zeppelin.server.strict.transport': string; + 'zeppelin.interpreter.setting': string; + 'zeppelin.server.xxss.protection': string; + 'zeppelin.server.rpc.portRange': string; + 'zeppelin.war': string; + 'zeppelin.interpreter.output.limit': string; + 'zeppelin.dep.localrepo': string; + 'zeppelin.interpreter.max.poolsize': string; + 'zeppelin.server.ssl.port': string; + 'zeppelin.notebook.mongo.collection': string; + 'zeppelin.notebook.public': string; + 'zeppelin.helium.registry': string; + 'zeppelin.server.kerberos.principal': string; + 'zeppelin.server.default.dir.allowed': string; + 'zeppelin.ssl.client.auth': string; + 'zeppelin.server.context.path': string; + 'zeppelin.recovery.storage.class': string; + 'zeppelin.notebook.default.owner.username': string; + 'zeppelin.home': string; + 'zeppelin.interpreter.lifecyclemanager.timeout.threshold': string; + 'zeppelin.cluster.addr': string; + 'zeppelin.notebook.git.remote.url': string; + 'zeppelin.notebook.mongo.autoimport': string; + 'zeppelin.notebook.one.way.sync': string; + 'zeppelin.notebook.homescreen': string; + 'zeppelin.interpreter.connect.timeout': string; + 'zeppelin.server.xframe.options': string; + 'zeppelin.interpreter.lifecyclemanager.timeout.checkinterval': string; + 'zeppelin.server.kerberos.keytab': string; + 'zeppelin.interpreter.rpc.portRange': string; + 'zeppelin.interpreter.group.default': string; + 'zeppelin.conf.dir': string; + 'zeppelin.interpreter.localRepo': string; + 'zeppelin.notebook.collaborative.mode.enable': string; + 'zeppelin.search.use.disk': string; + }; +} + +export interface ErrorInfo { + info?: string; +} + +export interface AuthInfo { + info?: string; +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts new file mode 100644 index 00000000000..1ea8f0dd6e0 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts @@ -0,0 +1,152 @@ +import { AuthInfo, ConfigurationsInfo, ErrorInfo } from './message-common.interface'; +import { + CheckpointNote, + CloneNote, + CollaborativeModeStatus, + DeleteNote, + EditorSettingReceived, + EditorSettingSend, + FolderRename, + GetInterpreterBindings, + GetNode, + ListRevision, + ListRevisionHistory, + MoveFolderToTrash, + MoveNoteToTrash, + NewNote, + Note, + NotesInfo, + NoteRename, + NoteRevision, + NoteRevisionForCompare, + NoteRunningStatus, + NoteUpdate, + NoteUpdated, + ParagraphAdded, + ParagraphMoved, + RemoveFolder, + RemoveNoteForms, + RestoreFolder, + RestoreNote, + SaveNoteFormsReceived, + SaveNoteFormsSend, + SetNoteRevision, + SetNoteRevisionStatus, + UpdateParagraph, + UpdatePersonalizedMode +} from './message-notebook.interface'; +import { + AngularObjectClientBind, + AngularObjectClientUnbind, + AngularObjectRemove, + AngularObjectUpdate, + AngularObjectUpdated, + CancelParagraph, + CommitParagraph, + Completion, + CompletionReceived, + CopyParagraph, + InsertParagraph, + MoveParagraph, + ParagraphClearAllOutput, + ParagraphClearOutput, + ParagraphRemove, + ParagraphRemoved, + ParasInfo, + PatchParagraphReceived, + PatchParagraphSend, + Progress, + RunAllParagraphs, + RunParagraph +} from './message-paragraph.interface'; + +import { ListNoteJobs, ListUpdateNoteJobs } from './message-job.interface'; + +import { InterpreterBindings, InterpreterSetting } from './message-interpreter.interface'; +import { OP } from './message-operator.interface'; + +export type MixMessageDataTypeMap = MessageSendDataTypeMap & MessageReceiveDataTypeMap; + +export interface MessageReceiveDataTypeMap { + [OP.COMPLETION_LIST]: CompletionReceived; + [OP.NOTES_INFO]: NotesInfo; + [OP.CONFIGURATIONS_INFO]: ConfigurationsInfo; + [OP.NOTE]: Note; + [OP.NOTE_REVISION]: NoteRevision; + [OP.ERROR_INFO]: ErrorInfo; + [OP.LIST_NOTE_JOBS]: ListNoteJobs; + [OP.LIST_UPDATE_NOTE_JOBS]: ListUpdateNoteJobs; + [OP.INTERPRETER_SETTINGS]: InterpreterSetting; + [OP.LIST_REVISION_HISTORY]: ListRevision; + [OP.INTERPRETER_BINDINGS]: InterpreterBindings; + [OP.COLLABORATIVE_MODE_STATUS]: CollaborativeModeStatus; + [OP.SET_NOTE_REVISION]: SetNoteRevisionStatus; + [OP.PARAGRAPH_ADDED]: ParagraphAdded; + [OP.NOTE_RUNNING_STATUS]: NoteRunningStatus; + [OP.NEW_NOTE]: NoteRevision; + [OP.SAVE_NOTE_FORMS]: SaveNoteFormsSend; + [OP.PARAGRAPH]: UpdateParagraph; + [OP.PATCH_PARAGRAPH]: PatchParagraphSend; + [OP.PARAGRAPH_REMOVED]: ParagraphRemoved; + [OP.EDITOR_SETTING]: EditorSettingReceived; + [OP.PROGRESS]: Progress; + [OP.PARAGRAPH_MOVED]: ParagraphMoved; + [OP.AUTH_INFO]: AuthInfo; + [OP.NOTE_UPDATED]: NoteUpdated; + [OP.ANGULAR_OBJECT_UPDATE]: AngularObjectUpdate; + [OP.ANGULAR_OBJECT_REMOVE]: AngularObjectRemove; + [OP.PARAS_INFO]: ParasInfo; +} + +export interface MessageSendDataTypeMap { + [OP.PING]: undefined; + [OP.LIST_CONFIGURATIONS]: undefined; + [OP.LIST_NOTES]: undefined; + [OP.GET_HOME_NOTE]: undefined; + [OP.RESTORE_ALL]: undefined; + [OP.EMPTY_TRASH]: undefined; + [OP.RELOAD_NOTES_FROM_REPO]: undefined; + [OP.GET_NOTE]: GetNode; + [OP.NEW_NOTE]: NewNote; + [OP.MOVE_NOTE_TO_TRASH]: MoveNoteToTrash; + [OP.MOVE_FOLDER_TO_TRASH]: MoveFolderToTrash; + [OP.RESTORE_NOTE]: RestoreNote; + [OP.RESTORE_FOLDER]: RestoreFolder; + [OP.REMOVE_FOLDER]: RemoveFolder; + [OP.DEL_NOTE]: DeleteNote; + [OP.CLONE_NOTE]: CloneNote; + [OP.NOTE_UPDATE]: NoteUpdate; + [OP.UPDATE_PERSONALIZED_MODE]: UpdatePersonalizedMode; + [OP.NOTE_RENAME]: NoteRename; + [OP.FOLDER_RENAME]: FolderRename; + [OP.MOVE_PARAGRAPH]: MoveParagraph; + [OP.INSERT_PARAGRAPH]: InsertParagraph; + [OP.COPY_PARAGRAPH]: CopyParagraph; + [OP.ANGULAR_OBJECT_UPDATED]: AngularObjectUpdated; + [OP.ANGULAR_OBJECT_CLIENT_BIND]: AngularObjectClientBind; + [OP.ANGULAR_OBJECT_CLIENT_UNBIND]: AngularObjectClientUnbind; + [OP.CANCEL_PARAGRAPH]: CancelParagraph; + [OP.PARAGRAPH_EXECUTED_BY_SPELL]: {}; // TODO + [OP.RUN_PARAGRAPH]: RunParagraph; + [OP.RUN_ALL_PARAGRAPHS]: RunAllParagraphs; + [OP.PARAGRAPH_REMOVE]: ParagraphRemove; + [OP.PARAGRAPH_CLEAR_OUTPUT]: ParagraphClearOutput; + [OP.PARAGRAPH_CLEAR_ALL_OUTPUT]: ParagraphClearAllOutput; + [OP.COMPLETION]: Completion; + [OP.COMMIT_PARAGRAPH]: CommitParagraph; + [OP.PATCH_PARAGRAPH]: PatchParagraphReceived; + [OP.IMPORT_NOTE]: {}; // TODO + [OP.CHECKPOINT_NOTE]: CheckpointNote; + [OP.SET_NOTE_REVISION]: SetNoteRevision; + [OP.LIST_REVISION_HISTORY]: ListRevisionHistory; + [OP.NOTE_REVISION]: NoteRevision; + [OP.NOTE_REVISION_FOR_COMPARE]: NoteRevisionForCompare; + [OP.EDITOR_SETTING]: EditorSettingSend; + [OP.LIST_NOTE_JOBS]: undefined; + [OP.UNSUBSCRIBE_UPDATE_NOTE_JOBS]: undefined; + [OP.LIST_UPDATE_NOTE_JOBS]: undefined; + [OP.GET_INTERPRETER_BINDINGS]: GetInterpreterBindings; + [OP.GET_INTERPRETER_SETTINGS]: undefined; + [OP.SAVE_NOTE_FORMS]: SaveNoteFormsReceived; + [OP.REMOVE_NOTE_FORMS]: RemoveNoteForms; +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts new file mode 100644 index 00000000000..3eb9d81902f --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts @@ -0,0 +1,58 @@ +export interface InterpreterSetting { + interpreterSettings: InterpreterItem[]; +} + +export interface InterpreterItem { + id: string; + name: string; + group: string; + properties: Properties; + status: string; + interpreterGroup: InterpreterGroupItem[]; + dependencies: string[]; + option: Option; +} + +export interface InterpreterBindings { + interpreterBindings: InterpreterBindingItem[]; +} + +export interface InterpreterBindingItem { + id: string; + name: string; + selected: boolean; + interpreters: InterpreterGroupItem[]; +} + +interface Properties { + [name: string]: { + name: string; + value: boolean; + type: string; + }; +} + +interface InterpreterGroupItem { + name: string; + class: string; + defaultInterpreter: boolean; + editor?: Editor; +} + +interface Editor { + language?: string; + editOnDblClick?: boolean; + completionKey?: string; + completionSupport?: boolean; +} + +interface Option { + remote: boolean; + port: number; + isExistingProcess: boolean; + setPermission: boolean; + owners: string[]; + isUserImpersonate: boolean; + perNote?: string; + perUser?: string; +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts new file mode 100644 index 00000000000..5e48b4065d6 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts @@ -0,0 +1,36 @@ +export interface ListNoteJobs { + noteJobs: NoteJobs; +} + +export interface ListUpdateNoteJobs { + noteRunningJobs: NoteJobs; +} + +export interface NoteJobs { + lastResponseUnixTime: number; + jobs: JobsItem[]; +} +export interface JobsItem { + noteId: string; + noteName: string; + noteType: string; + interpreter: string; + isRunningJob: boolean; + isRemoved: boolean; + unixTimeLastRun: number; + paragraphs: JobItemParagraphItem[]; +} +export interface JobItemParagraphItem { + id: string; + name: string; + status: JobStatus; +} + +export enum JobStatus { + READY = 'READY', + FINISHED = 'FINISHED', + ABORT = 'ABORT', + ERROR = 'ERROR', + PENDING = 'PENDING', + RUNNING = 'RUNNING' +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts new file mode 100644 index 00000000000..425913f6590 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts @@ -0,0 +1,197 @@ +import { ParagraphItem } from './message-paragraph.interface'; + +interface ID { + id: string; +} + +interface Name { + name: string; +} + +export type GetNode = ID; +export type MoveNoteToTrash = ID; +export type MoveFolderToTrash = ID; +export type RestoreNote = ID; +export type RestoreFolder = ID; +export type DeleteNote = ID; +export type RemoveFolder = ID; +export type CloneNote = ID & Name; +export type FolderRename = ID & Name; +export type PersonalizedMode = 'true' | 'false'; + +export interface NoteRename extends Name, ID { + relative: boolean; +} + +export interface SendNote { + id: string; + noteParams: NoteParams; +} + +export interface NoteUpdated { + config: NoteConfig; + info: NoteInfo; + name: string; +} + +export interface Note { + note?: { + paragraphs: ParagraphItem[]; + name: string; + id: string; + defaultInterpreterGroup: string; + noteParams: NoteParams; + noteForms: NoteForms; + angularObjects: NoteAngularObjects; + config: NoteConfig; + info: NoteInfo; + }; +} + +export interface NoteAngularObjects { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface NoteInfo { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface NoteParams { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface NoteForms { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface RemoveNoteForms { + noteId: string; + formName: string; +} + +export interface SaveNoteFormsReceived { + noteId: string; + noteParams: NoteParams; +} + +export interface GetInterpreterBindings { + noteId: string; +} + +export interface EditorSettingSend { + paragraphId: string; + magic: string; +} + +export interface EditorSettingReceived { + paragraphId: string; + editor: { + completionSupport: boolean; + editOnDblClick: boolean; + language: string; + }; +} + +export interface NoteRevisionForCompare { + noteId: string; + revisionId: string; + position: string; +} + +export interface CollaborativeModeStatus { + status: boolean; + users: string[]; +} + +export interface ParagraphMoved { + index: number; + id: string; +} + +export interface UpdateParagraph { + paragraph: ParagraphItem; +} + +export interface SaveNoteFormsSend { + formsData: { + forms: NoteForms; + params: NoteParams; + }; +} + +export interface NoteRunningStatus { + status: boolean; +} + +export interface ParagraphAdded { + index: number; + paragraph: ParagraphItem; +} + +export interface SetNoteRevisionStatus { + status: boolean; +} + +export interface ListRevision { + revisionList: RevisionListItem[]; +} + +export interface RevisionListItem { + id: string; + message: string; + time?: number; +} + +export interface NoteRevision { + note?: Note['note']; + noteId: string; + revisionId: string; +} + +export interface ListRevisionHistory { + noteId: string; +} + +export interface SetNoteRevision { + noteId: string; + revisionId: string; +} + +export interface CheckpointNote { + noteId: string; + commitMessage: string; +} + +export interface NoteUpdate extends Name, ID { + config: NoteConfig; +} + +export interface NewNote extends Name { + defaultInterpreterGroup: string; +} + +export interface NotesInfo { + notes: NotesInfoItem[]; +} + +export interface NotesInfoItem extends ID { + path: string; +} + +export interface NoteConfig { + cron?: string; + releaseresource: boolean; + cronExecutingRoles?: string; + cronExecutingUser?: string; + isZeppelinNotebookCronEnable: boolean; + looknfeel: 'report' | 'default' | 'simple'; + personalizedMode: PersonalizedMode; +} + +export interface UpdatePersonalizedMode extends ID { + personalized: PersonalizedMode; +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts new file mode 100644 index 00000000000..141908bdab5 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts @@ -0,0 +1,470 @@ +// tslint:disable:no-redundant-jsdoc +/** + * Representation of event type. + */ +export enum OP { + /** + * [c-s] + * load note for home screen + */ + GET_HOME_NOTE = 'GET_HOME_NOTE', + + /** + * [c-s] + * client load note + * @param id note id + */ + GET_NOTE = 'GET_NOTE', + + /** + * [s-c] + * note info + * @param note serialized SendNote object + */ + NOTE = 'NOTE', + + /** + * [s-c] + * paragraph info + * @param paragraph serialized paragraph object + */ + PARAGRAPH = 'PARAGRAPH', + + /** + * [s-c] + * progress update + * @param id paragraph id + * @param progress percentage progress + */ + PROGRESS = 'PROGRESS', + + /** + * [c-s] + * create new notebook + */ + NEW_NOTE = 'NEW_NOTE', + + /** + * [c-s] + * delete notebook + * @param id note id + */ + DEL_NOTE = 'DEL_NOTE', + REMOVE_FOLDER = 'REMOVE_FOLDER', + MOVE_NOTE_TO_TRASH = 'MOVE_NOTE_TO_TRASH', + MOVE_FOLDER_TO_TRASH = 'MOVE_FOLDER_TO_TRASH', + RESTORE_FOLDER = 'RESTORE_FOLDER', + RESTORE_NOTE = 'RESTORE_NOTE', + RESTORE_ALL = 'RESTORE_ALL', + EMPTY_TRASH = 'EMPTY_TRASH', + + /** + * [c-s] + * clone new notebook + * @param id id of note to clone + * @param name name for the cloned note + */ + CLONE_NOTE = 'CLONE_NOTE', + + /** + * [c-s] + * import notebook + * @param object notebook + */ + IMPORT_NOTE = 'IMPORT_NOTE', + NOTE_UPDATE = 'NOTE_UPDATE', + NOTE_RENAME = 'NOTE_RENAME', + + /** + * [c-s] + * update personalized mode (boolean) + * @param note id and boolean personalized mode value + */ + UPDATE_PERSONALIZED_MODE = 'UPDATE_PERSONALIZED_MODE', + FOLDER_RENAME = 'FOLDER_RENAME', + + /** + * [c-s] + * run paragraph + * @param id paragraph id + * @param paragraph paragraph content.ie. script + * @param config paragraph config + * @param params paragraph params + */ + RUN_PARAGRAPH = 'RUN_PARAGRAPH', + + /** + * [c-s] + * commit paragraph + * @param id paragraph id + * @param title paragraph title + * @param paragraph paragraph content.ie. script + * @param config paragraph config + * @param params paragraph params + */ + COMMIT_PARAGRAPH = 'COMMIT_PARAGRAPH', + + /** + * [c-s] + * cancel paragraph run + * @param id paragraph id + */ + CANCEL_PARAGRAPH = 'CANCEL_PARAGRAPH', + + /** + * [c-s] + * move paragraph order + * @param id paragraph id + * @param index index the paragraph want to go + */ + MOVE_PARAGRAPH = 'MOVE_PARAGRAPH', + + /** + * [c-s] + * create new paragraph below current paragraph + * @param target index + */ + INSERT_PARAGRAPH = 'INSERT_PARAGRAPH', + + /** + * [c-s] + * create new para below current para as a copy of current para + * @param target index + * @param title paragraph title + * @param paragraph paragraph content.ie. script + * @param config paragraph config + * @param params paragraph params + */ + COPY_PARAGRAPH = 'COPY_PARAGRAPH', + + /** + * [c-s] + * ask paragraph editor setting + * @param magic magic keyword written in paragraph + * ex) spark.spark or spark + */ + EDITOR_SETTING = 'EDITOR_SETTING', + + /** + * [c-s] + * ask completion candidates + * @param id + * @param buf current code + * @param cursor cursor position in code + */ + COMPLETION = 'COMPLETION', + + /** + * [s-c] + * send back completion candidates list + * @param id + * @param completions list of string + */ + COMPLETION_LIST = 'COMPLETION_LIST', + + /** + * [c-s] + * ask list of note + */ + LIST_NOTES = 'LIST_NOTES', + + /** + * [c-s] + * reload notes from repo + */ + RELOAD_NOTES_FROM_REPO = 'RELOAD_NOTES_FROM_REPO', + + /** + * [s-c] + * list of note infos + * @param notes serialized List object + */ + NOTES_INFO = 'NOTES_INFO', + PARAGRAPH_REMOVE = 'PARAGRAPH_REMOVE', + + /** + * [c-s] + * clear output of paragraph + */ + PARAGRAPH_CLEAR_OUTPUT = 'PARAGRAPH_CLEAR_OUTPUT', + + /** [c-s] + * clear output of all paragraphs + */ + PARAGRAPH_CLEAR_ALL_OUTPUT = 'PARAGRAPH_CLEAR_ALL_OUTPUT', + + /** + * [s-c] + * ppend output + */ + PARAGRAPH_APPEND_OUTPUT = 'PARAGRAPH_APPEND_OUTPUT', + + /** + * [s-c] + * update (replace) output + */ + PARAGRAPH_UPDATE_OUTPUT = 'PARAGRAPH_UPDATE_OUTPUT', + PING = 'PING', + AUTH_INFO = 'AUTH_INFO', + + /** + * [s-c] + * add/update angular object + */ + ANGULAR_OBJECT_UPDATE = 'ANGULAR_OBJECT_UPDATE', + + /** [s-c] + * add angular object del + */ + ANGULAR_OBJECT_REMOVE = 'ANGULAR_OBJECT_REMOVE', + + /** + * [c-s] + * angular object value updated + */ + ANGULAR_OBJECT_UPDATED = 'ANGULAR_OBJECT_UPDATED', + + /** + * [c-s] + * angular object updated from AngularJS z object + */ + ANGULAR_OBJECT_CLIENT_BIND = 'ANGULAR_OBJECT_CLIENT_BIND', + + /** + * [c-s] + * angular object unbind from AngularJS z object + */ + ANGULAR_OBJECT_CLIENT_UNBIND = 'ANGULAR_OBJECT_CLIENT_UNBIND', + + /** + * [c-s] + * ask all key/value pairs of configurations + */ + LIST_CONFIGURATIONS = 'LIST_CONFIGURATIONS', + + /** + * [s-c] + * all key/value pairs of configurations + * @param settings serialized Map object + */ + CONFIGURATIONS_INFO = 'CONFIGURATIONS_INFO', + + /** + * [c-s] + * checkpoint note to storage repository + * @param noteId + * @param checkpointName + */ + CHECKPOINT_NOTE = 'CHECKPOINT_NOTE', + + /** + * [c-s] + * list revision history of the notebook + * @param noteId + */ + LIST_REVISION_HISTORY = 'LIST_REVISION_HISTORY', + + /** + * [c-s] + * get certain revision of note + * @param noteId + * @param revisionId + */ + NOTE_REVISION = 'NOTE_REVISION', + + /** + * [c-s] + * set current notebook head to this revision + * @param noteId + * @param revisionId + */ + SET_NOTE_REVISION = 'SET_NOTE_REVISION', + + /** + * [c-s] + * get certain revision of note for compare + * @param noteId + * @param revisionId + * @param position + */ + NOTE_REVISION_FOR_COMPARE = 'NOTE_REVISION_FOR_COMPARE', + + /** + * [s-c] + * append output + */ + APP_APPEND_OUTPUT = 'APP_APPEND_OUTPUT', + + /** + * [s-c] + * update (replace) output + */ + APP_UPDATE_OUTPUT = 'APP_UPDATE_OUTPUT', + + /** + * [s-c] + * on app load + */ + APP_LOAD = 'APP_LOAD', + + /** + * [s-c] + * on app status change + */ + APP_STATUS_CHANGE = 'APP_STATUS_CHANGE', + + /** + * [s-c] + * get note job management information + */ + LIST_NOTE_JOBS = 'LIST_NOTE_JOBS', + + /** + * [c-s] + * get job management information for until unixtime + */ + LIST_UPDATE_NOTE_JOBS = 'LIST_UPDATE_NOTE_JOBS', + + /** + * [c-s] + * unsubscribe job information for job management + * @param unixTime + */ + UNSUBSCRIBE_UPDATE_NOTE_JOBS = 'UNSUBSCRIBE_UPDATE_NOTE_JOBS', + + /** + * [c-s] + * get interpreter bindings + */ + GET_INTERPRETER_BINDINGS = 'GET_INTERPRETER_BINDINGS', + + /** + * [s-c] + * interpreter bindings + */ + INTERPRETER_BINDINGS = 'INTERPRETER_BINDINGS', + + /** + * [c-s] + * get interpreter settings + */ + GET_INTERPRETER_SETTINGS = 'GET_INTERPRETER_SETTINGS', + + /** + * [s-c] + * interpreter settings + */ + INTERPRETER_SETTINGS = 'INTERPRETER_SETTINGS', + + /** + * [s-c] + * error information to be sent + */ + ERROR_INFO = 'ERROR_INFO', + + /** + * [s-c] + * error information to be sent + */ + SESSION_LOGOUT = 'SESSION_LOGOUT', + + /** + * [s-c] + * Change websocket to watcher mode. + */ + WATCHER = 'WATCHER', + + /** + * [s-c] + * paragraph is added + */ + PARAGRAPH_ADDED = 'PARAGRAPH_ADDED', + + /** + * [s-c] + * paragraph deleted + */ + PARAGRAPH_REMOVED = 'PARAGRAPH_REMOVED', + + /** + * [s-c] + * paragraph moved + */ + PARAGRAPH_MOVED = 'PARAGRAPH_MOVED', + + /** + * [s-c] + * paragraph updated(name, config) + */ + NOTE_UPDATED = 'NOTE_UPDATED', + + /** + * [c-s] + * run all paragraphs + */ + RUN_ALL_PARAGRAPHS = 'RUN_ALL_PARAGRAPHS', + + /** + * [c-s] + * paragraph was executed by spell + */ + PARAGRAPH_EXECUTED_BY_SPELL = 'PARAGRAPH_EXECUTED_BY_SPELL', + + /** + * [s-c] + * run paragraph using spell + */ + RUN_PARAGRAPH_USING_SPELL = 'RUN_PARAGRAPH_USING_SPELL', + + /** + * [s-c] + * paragraph runtime infos + */ + PARAS_INFO = 'PARAS_INFO', + + /** + * save note forms + */ + SAVE_NOTE_FORMS = 'SAVE_NOTE_FORMS', + + /** + * remove note forms + */ + REMOVE_NOTE_FORMS = 'REMOVE_NOTE_FORMS', + + /** + * [s-c] + * start to download an interpreter + */ + INTERPRETER_INSTALL_STARTED = 'INTERPRETER_INSTALL_STARTED', + + /** + * [s-c] + * Status of an interpreter installation + */ + INTERPRETER_INSTALL_RESULT = 'INTERPRETER_INSTALL_RESULT', + + /** + * [s-c] + * collaborative mode status + */ + COLLABORATIVE_MODE_STATUS = 'COLLABORATIVE_MODE_STATUS', + + /** + * [c-s][s-c] + * patch editor text + */ + PATCH_PARAGRAPH = 'PATCH_PARAGRAPH', + + /** + * [s-c] + * sequential run status will be change + */ + NOTE_RUNNING_STATUS = 'NOTE_RUNNING_STATUS', + + /** + * [s-c] + * Notice + */ + NOTICE = 'NOTICE' +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts new file mode 100644 index 00000000000..712d0564067 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts @@ -0,0 +1,456 @@ +import { EditorCompletionKey, EditorLanguage, EditorMode } from './message-common.interface'; + +export enum DynamicFormsType { + TextBox = 'TextBox', + Password = 'Password', + Select = 'Select', + CheckBox = 'CheckBox' +} + +export interface DynamicFormsItem { + defaultValue: string | string[]; + hidden: boolean; + name: string; + type: DynamicFormsType; + argument?: string; + options?: Array<{ value: string; displayName?: string }>; +} + +export interface DynamicForms { + [key: string]: DynamicFormsItem; +} + +export interface DynamicFormParams { + [key: string]: string | string[]; +} + +export interface ParagraphEditorSetting { + language?: EditorLanguage; + editOnDblClick?: boolean; + isOutputHidden?: boolean; + completionKey?: EditorCompletionKey; + completionSupport?: boolean; + params?: DynamicFormParams; + forms?: DynamicForms; +} + +// TODO +export interface ParagraphParams { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface ParagraphConfigResults { + [index: string]: ParagraphConfigResult; +} + +export interface ParagraphConfigResult { + graph: GraphConfig; +} + +export interface ParagraphConfig { + editorSetting?: ParagraphEditorSetting; + colWidth?: number; + editorMode?: EditorMode; + fontSize?: number; + results?: ParagraphConfigResults; + enabled?: boolean; + tableHide?: boolean; + lineNumbers?: boolean; + editorHide?: boolean; + title?: boolean; + runOnSelectionChange?: boolean; + isZeppelinNotebookCronEnable?: boolean; +} + +export interface ParagraphResults { + code?: string; + msg?: ParagraphIResultsMsgItem[]; + + [index: number]: {}; +} + +export enum DatasetType { + NETWORK = 'NETWORK', + TABLE = 'TABLE', + HTML = 'HTML', + TEXT = 'TEXT', + ANGULAR = 'ANGULAR', + IMG = 'IMG' +} + +export class ParagraphIResultsMsgItem { + type: DatasetType = DatasetType.TEXT; + data = ''; +} + +export interface ParasInfo { + id: string; + infos: RuntimeInfos; +} + +export interface RuntimeInfos { + jobUrl: RuntimeInfosJobUrl; +} + +interface RuntimeInfosJobUrl { + propertyName: string; + label: string; + tooltip: string; + group: string; + values: RuntimeInfosValuesItem[]; + interpreterSettingId: string; +} + +interface RuntimeInfosValuesItem { + jobUrl: string; +} + +export interface ParagraphItem { + text: string; + user: string; + dateUpdated: string; + config: ParagraphConfig; + settings: ParagraphEditorSetting; + results?: ParagraphResults; + // tslint:disable-next-line no-any + apps: any[]; + progressUpdateIntervalMs: number; + jobName: string; + id: string; + dateCreated: string; + dateStarted?: string; + dateFinished?: string; + errorMessage?: string; + // tslint:disable-next-line no-any TODO + runtimeInfos?: RuntimeInfos; + status: string; + title?: string; + focus?: boolean; + // tslint:disable-next-line no-any TODO + aborted: any; + // tslint:disable-next-line no-any TODO + lineNumbers: any; + // tslint:disable-next-line no-any TODO + fontSize: any; +} + +export interface SendParagraph { + id: string; + title?: string; + paragraph: string; + config: ParagraphConfig; + params: ParagraphParams; +} + +export interface CopyParagraph { + index: number; + title?: string; + paragraph: string; + config: ParagraphConfig; + params: ParagraphParams; +} + +export interface RunParagraph extends SendParagraph { + // tslint:disable-next-line no-any + [key: string]: any; +} + +export interface CommitParagraph extends SendParagraph { + noteId: string; +} + +export interface RunAllParagraphs { + noteId: string; + paragraphs: string; +} + +export interface InsertParagraph { + index: number; +} + +export interface MoveParagraph { + id: string; + index: number; +} + +export interface AngularObjectUpdated { + noteId: string; + paragraphId: string; + name: string; + value: string; + interpreterGroupId: string; +} + +export interface AngularObjectRemove { + noteId: string; + paragraphId: string; + name: string; +} + +export interface AngularObjectUpdate { + noteId: string; + paragraphId: string; + interpreterGroupId: string; + angularObject: { + name: string; + // tslint:disable-next-line:no-any + object: any; + noteId: string; + paragraphId: string; + }; +} + +export interface AngularObjectClientBind { + noteId: string; + name: string; + value: string; + paragraphId: string; +} + +export interface AngularObjectClientUnbind { + noteId: string; + name: string; + paragraphId: string; +} + +export interface CancelParagraph { + id: string; +} + +export interface ParagraphRemove { + id: string; +} + +export interface ParagraphClearOutput { + id: string; +} + +export interface ParagraphClearAllOutput { + id: string; +} + +export interface Completion { + id: string; + buf: string; + cursor: number; +} + +export interface CompletionItem { + meta: string; + value: string; + name: string; +} + +export interface CompletionReceived { + completions: CompletionItem[]; + id: string; +} + +export interface PatchParagraphReceived { + id: string; + noteId: string; + patch: string; +} + +export interface PatchParagraphSend { + paragraphId: string; + patch: string; +} + +export interface ParagraphRemoved { + id: string; +} + +export type VisualizationMode = + | 'table' + | 'lineChart' + | 'stackedAreaChart' + | 'multiBarChart' + | 'scatterChart' + | 'pieChart' + | string; + +export class GraphConfig { + mode: VisualizationMode = 'table'; + height = 300; + optionOpen = false; + setting: GraphConfigSetting = {}; + keys: GraphConfigKeysItem[] = []; + groups: GraphConfigGroupsItem[] = []; + values: GraphConfigValuesItem[] = []; + commonSetting: GraphConfigCommonSetting; +} + +export interface Progress { + id: string; + progress: number; +} + +interface GraphConfigSetting { + table?: VisualizationTable; + lineChart?: VisualizationLineChart; + stackedAreaChart?: VisualizationStackedAreaChart; + multiBarChart?: VisualizationMultiBarChart; + scatterChart?: VisualizationScatterChart; +} + +interface VisualizationTable { + tableGridState: TableGridState; + tableColumnTypeState: TableColumnTypeState; + updated: boolean; + initialized: boolean; + tableOptionSpecHash: string; + tableOptionValue: TableOptionValue; +} + +interface TableGridState { + columns: ColumnsItem[]; + scrollFocus: ScrollFocus; + // tslint:disable-next-line + selection: any[]; + grouping: Grouping; + treeView: TreeView; + pagination: Pagination; +} + +interface ColumnsItem { + name: string; + visible: boolean; + width: string; + sort: Sort; + filters: FiltersItem[]; + pinned: string; +} + +interface Sort { + // tslint:disable-next-line + [key: string]: any; +} + +interface FiltersItem { + // tslint:disable-next-line + [key: string]: any; +} + +interface ScrollFocus { + // tslint:disable-next-line + [key: string]: any; +} + +interface Grouping { + // tslint:disable-next-line + grouping: any[]; + // tslint:disable-next-line + aggregations: any[]; + rowExpandedStates: RowExpandedStates; +} + +interface RowExpandedStates { + // tslint:disable-next-line + [key: string]: any; +} + +interface TreeView { + // tslint:disable-next-line + [key: string]: any; +} + +interface Pagination { + paginationCurrentPage: number; + paginationPageSize: number; +} + +interface TableColumnTypeState { + updated: boolean; + names: Names; +} + +interface Names { + index: string; + value: string; + random: string; + count: string; +} + +interface TableOptionValue { + useFilter: boolean; + showPagination: boolean; + showAggregationFooter: boolean; +} + +export type XLabelStatus = 'default' | 'rotate' | 'hide'; + +export class XAxisSetting { + rotate = { degree: '-45' }; + xLabelStatus: XLabelStatus = 'default'; +} + +export class VisualizationLineChart extends XAxisSetting { + forceY = false; + lineWithFocus = false; + isDateFormat = false; + dateFormat = ''; +} + +export class VisualizationStackedAreaChart extends XAxisSetting { + style: 'stream' | 'expand' | 'stack' = 'stack'; +} + +export class VisualizationMultiBarChart extends XAxisSetting { + stacked = false; +} + +export class VisualizationScatterChart { + xAxis?: XAxis; + yAxis?: YAxis; + group?: Group; + size?: Size; +} + +interface XAxis { + name: string; + index: number; + aggr: string; +} + +interface YAxis { + name: string; + index: number; + aggr: string; +} + +interface Group { + name: string; + index: number; + aggr: string; +} + +interface Size { + name: string; + index: number; + aggr: string; +} + +interface GraphConfigKeysItem { + name: string; + index: number; + aggr: string; +} + +interface GraphConfigGroupsItem { + name: string; + index: number; + aggr: string; +} + +interface GraphConfigValuesItem { + name: string; + index: number; + aggr: string; +} + +interface GraphConfigCommonSetting { + // tslint:disable-next-line + [key: string]: any; +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts new file mode 100644 index 00000000000..ac4dbbd25fb --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts @@ -0,0 +1,8 @@ +export * from './message-common.interface'; +export * from './message-data-type-map.interface'; +export * from './message-notebook.interface'; +export * from './message-operator.interface'; +export * from './message-paragraph.interface'; +export * from './websocket-message.interface'; +export * from './message-job.interface'; +export * from './message-interpreter.interface'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts new file mode 100644 index 00000000000..f6bf8fa273f --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts @@ -0,0 +1,9 @@ +import { MixMessageDataTypeMap } from './message-data-type-map.interface'; + +export interface WebSocketMessage { + op: K; + data?: MixMessageDataTypeMap[K]; + ticket?: string; // default 'anonymous' + principal?: string; // default 'anonymous' + roles?: string; // default '[]' +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts new file mode 100644 index 00000000000..54b7ca86f95 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts @@ -0,0 +1,490 @@ +import { interval, Observable, Subject, Subscription } from 'rxjs'; +import { delay, filter, map, mergeMap, retryWhen, take } from 'rxjs/operators'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; + +import { Ticket } from './interfaces/message-common.interface'; +import { + MessageReceiveDataTypeMap, + MessageSendDataTypeMap, + MixMessageDataTypeMap +} from './interfaces/message-data-type-map.interface'; +import { NoteConfig, PersonalizedMode, SendNote } from './interfaces/message-notebook.interface'; +import { OP } from './interfaces/message-operator.interface'; +import { ParagraphConfig, ParagraphParams, SendParagraph } from './interfaces/message-paragraph.interface'; +import { WebSocketMessage } from './interfaces/websocket-message.interface'; + +export type ArgumentsType = T extends (...args: infer U) => void ? U : never; + +export type SendArgumentsType = MessageSendDataTypeMap[K] extends undefined + ? ArgumentsType<(op: K) => void> + : ArgumentsType<(op: K, data: MessageSendDataTypeMap[K]) => void>; + +export type ReceiveArgumentsType< + K extends keyof MessageReceiveDataTypeMap +> = MessageReceiveDataTypeMap[K] extends undefined ? () => void : (data?: MessageReceiveDataTypeMap[K]) => void; + +export class Message { + public connectedStatus = false; + public connectedStatus$ = new Subject(); + private ws: WebSocketSubject>; + private open$ = new Subject(); + private close$ = new Subject(); + private sent$ = new Subject>(); + private received$ = new Subject>(); + private pingIntervalSubscription = new Subscription(); + private wsUrl: string; + private ticket: Ticket; + + constructor() { + this.open$.subscribe(() => { + this.connectedStatus = true; + this.connectedStatus$.next(this.connectedStatus); + this.pingIntervalSubscription.unsubscribe(); + this.pingIntervalSubscription = interval(1000 * 10).subscribe(() => this.ping()); + }); + this.close$.subscribe(() => { + this.connectedStatus = false; + this.connectedStatus$.next(this.connectedStatus); + this.pingIntervalSubscription.unsubscribe(); + }); + } + + bootstrap(ticket: Ticket, wsUrl: string) { + this.setTicket(ticket); + this.setWsUrl(wsUrl); + this.connect(); + } + + getWsInstance(): WebSocketSubject> { + return this.ws; + } + + setWsUrl(wsUrl: string): void { + this.wsUrl = wsUrl; + } + + setTicket(ticket: Ticket): void { + this.ticket = ticket; + } + + interceptReceived( + data: WebSocketMessage + ): WebSocketMessage { + return data; + } + + connect() { + this.ws = webSocket({ + url: this.wsUrl, + openObserver: this.open$, + closeObserver: this.close$ + }); + + this.ws + .pipe( + // reconnect + retryWhen(errors => + errors.pipe( + mergeMap(() => + this.close$.pipe( + take(1), + delay(4000) + ) + ) + ) + ) + ) + .subscribe((e: WebSocketMessage) => { + console.log('Receive:', e); + this.received$.next(this.interceptReceived(e)); + }); + } + + ping() { + this.send(OP.PING); + } + + opened(): Observable { + return this.open$.asObservable(); + } + + closed(): Observable { + return this.close$.asObservable(); + } + + sent(): Observable> { + return this.sent$.asObservable(); + } + + received(): Observable> { + return this.received$.asObservable(); + } + + send(...args: SendArgumentsType): void { + const [op, data] = args; + const message: WebSocketMessage = { + op, + data: data as MixMessageDataTypeMap[K], + ...this.ticket + }; + console.log('Send:', message); + + this.ws.next(message); + this.sent$.next(message); + } + + receive(op: K): Observable[K]> { + return this.received$.pipe( + filter(message => message.op === op), + map(message => message.data) + ) as Observable[K]>; + } + + destroy(): void { + this.ws.complete(); + this.ws = null; + } + + getHomeNote(): void { + this.send(OP.GET_HOME_NOTE); + } + + newNote(noteName: string, defaultInterpreterGroup: string): void { + this.send(OP.NEW_NOTE, { + name: noteName, + defaultInterpreterGroup + }); + } + + moveNoteToTrash(noteId: string): void { + this.send(OP.MOVE_NOTE_TO_TRASH, { + id: noteId + }); + } + + restoreNote(noteId: string): void { + this.send(OP.RESTORE_NOTE, { + id: noteId + }); + } + + deleteNote(noteId): void { + this.send(OP.DEL_NOTE, { + id: noteId + }); + } + + restoreFolder(folderPath: string): void { + this.send(OP.RESTORE_FOLDER, { + id: folderPath + }); + } + + removeFolder(folderPath: string): void { + this.send(OP.REMOVE_FOLDER, { + id: folderPath + }); + } + + moveFolderToTrash(folderPath: string): void { + this.send(OP.MOVE_FOLDER_TO_TRASH, { + id: folderPath + }); + } + + restoreAll(): void { + this.send(OP.RESTORE_ALL); + } + + emptyTrash(): void { + this.send(OP.EMPTY_TRASH); + } + + cloneNote(noteIdToClone, newNoteName): void { + this.send(OP.CLONE_NOTE, { id: noteIdToClone, name: newNoteName }); + } + + /** + * get nodes list + */ + listNodes(): void { + this.send(OP.LIST_NOTES); + } + + reloadAllNotesFromRepo(): void { + this.send(OP.RELOAD_NOTES_FROM_REPO); + } + + getNote(noteId: string): void { + this.send(OP.GET_NOTE, { id: noteId }); + } + + updateNote(noteId: string, noteName: string, noteConfig: NoteConfig): void { + this.send(OP.NOTE_UPDATE, { id: noteId, name: noteName, config: noteConfig }); + } + + updatePersonalizedMode(noteId: string, modeValue: PersonalizedMode): void { + this.send(OP.UPDATE_PERSONALIZED_MODE, { id: noteId, personalized: modeValue }); + } + + noteRename(noteId: string, noteName: string, relative: boolean): void { + this.send(OP.NOTE_RENAME, { id: noteId, name: noteName, relative: relative }); + } + + folderRename(folderId: string, folderPath: string): void { + this.send(OP.FOLDER_RENAME, { id: folderId, name: folderPath }); + } + + moveParagraph(paragraphId: string, newIndex: number): void { + this.send(OP.MOVE_PARAGRAPH, { id: paragraphId, index: newIndex }); + } + + insertParagraph(newIndex: number): void { + this.send(OP.INSERT_PARAGRAPH, { index: newIndex }); + } + + copyParagraph( + newIndex: number, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphParams + ): void { + this.send(OP.COPY_PARAGRAPH, { + index: newIndex, + title: paragraphTitle, + paragraph: paragraphData, + config: paragraphConfig, + params: paragraphParams + }); + } + + angularObjectUpdate( + noteId: string, + paragraphId: string, + name: string, + value: string, + interpreterGroupId: string + ): void { + this.send(OP.ANGULAR_OBJECT_UPDATED, { + noteId: noteId, + paragraphId: paragraphId, + name: name, + value: value, + interpreterGroupId: interpreterGroupId + }); + } + + // tslint:disable-next-line:no-any + angularObjectClientBind(noteId: string, name: string, value: any, paragraphId: string): void { + this.send(OP.ANGULAR_OBJECT_CLIENT_BIND, { + noteId: noteId, + name: name, + value: value, + paragraphId: paragraphId + }); + } + + angularObjectClientUnbind(noteId: string, name: string, paragraphId: string): void { + this.send(OP.ANGULAR_OBJECT_CLIENT_UNBIND, { + noteId: noteId, + name: name, + paragraphId: paragraphId + }); + } + + cancelParagraph(paragraphId): void { + this.send(OP.CANCEL_PARAGRAPH, { id: paragraphId }); + } + + paragraphExecutedBySpell( + paragraphId, + paragraphTitle, + paragraphText, + paragraphResultsMsg, + paragraphStatus, + paragraphErrorMessage, + paragraphConfig, + paragraphParams, + paragraphDateStarted, + paragraphDateFinished + ): void { + this.send(OP.PARAGRAPH_EXECUTED_BY_SPELL, { + id: paragraphId, + title: paragraphTitle, + paragraph: paragraphText, + results: { + code: paragraphStatus, + msg: paragraphResultsMsg.map(dataWithType => { + const serializedData = dataWithType.data; + return { type: dataWithType.type, serializedData }; + }) + }, + status: paragraphStatus, + errorMessage: paragraphErrorMessage, + config: paragraphConfig, + params: paragraphParams, + dateStarted: paragraphDateStarted, + dateFinished: paragraphDateFinished + }); + } + + runParagraph( + paragraphId: string, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphParams + ): void { + this.send(OP.RUN_PARAGRAPH, { + id: paragraphId, + title: paragraphTitle, + paragraph: paragraphData, + config: paragraphConfig, + params: paragraphParams + }); + } + + runAllParagraphs(noteId: string, paragraphs: SendParagraph[]): void { + this.send(OP.RUN_ALL_PARAGRAPHS, { + noteId: noteId, + paragraphs: JSON.stringify(paragraphs) + }); + } + + paragraphRemove(paragraphId: string): void { + this.send(OP.PARAGRAPH_REMOVE, { id: paragraphId }); + } + + paragraphClearOutput(paragraphId: string): void { + this.send(OP.PARAGRAPH_CLEAR_OUTPUT, { id: paragraphId }); + } + + paragraphClearAllOutput(noteId: string): void { + this.send(OP.PARAGRAPH_CLEAR_ALL_OUTPUT, { id: noteId }); + } + + completion(paragraphId: string, buf: string, cursor: number): void { + this.send(OP.COMPLETION, { + id: paragraphId, + buf: buf, + cursor: cursor + }); + } + + commitParagraph( + paragraphId: string, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphConfig, + noteId: string + ): void { + return this.send(OP.COMMIT_PARAGRAPH, { + id: paragraphId, + noteId: noteId, + title: paragraphTitle, + paragraph: paragraphData, + config: paragraphConfig, + params: paragraphParams + }); + } + + patchParagraph(paragraphId: string, noteId: string, patch: string): void { + // javascript add "," if change contains several patches + // but java library requires patch list without "," + const normalPatch = patch.replace(/,@@/g, '@@'); + return this.send(OP.PATCH_PARAGRAPH, { + id: paragraphId, + noteId: noteId, + patch: normalPatch + }); + } + + importNote(note: SendNote): void { + this.send(OP.IMPORT_NOTE, { + note: note + }); + } + + checkpointNote(noteId: string, commitMessage: string): void { + this.send(OP.CHECKPOINT_NOTE, { + noteId: noteId, + commitMessage: commitMessage + }); + } + + setNoteRevision(noteId: string, revisionId: string): void { + this.send(OP.SET_NOTE_REVISION, { + noteId: noteId, + revisionId: revisionId + }); + } + + listRevisionHistory(noteId: string): void { + this.send(OP.LIST_REVISION_HISTORY, { + noteId: noteId + }); + } + + noteRevision(noteId: string, revisionId: string): void { + this.send(OP.NOTE_REVISION, { + noteId: noteId, + revisionId: revisionId + }); + } + + noteRevisionForCompare(noteId: string, revisionId: string, position: string): void { + this.send(OP.NOTE_REVISION_FOR_COMPARE, { + noteId: noteId, + revisionId: revisionId, + position: position + }); + } + + editorSetting(paragraphId: string, replName: string): void { + this.send(OP.EDITOR_SETTING, { + paragraphId: paragraphId, + magic: replName + }); + } + + listNoteJobs(): void { + this.send(OP.LIST_NOTE_JOBS); + } + + unsubscribeUpdateNoteJobs(): void { + this.send(OP.UNSUBSCRIBE_UPDATE_NOTE_JOBS); + } + + getInterpreterBindings(noteId: string): void { + this.send(OP.GET_INTERPRETER_BINDINGS, { noteId: noteId }); + } + + saveInterpreterBindings(noteId, selectedSettingIds): void { + // this.send(OP.SAVE_INTERPRETER_BINDINGS, + // {noteId: noteId, selectedSettingIds: selectedSettingIds}); + } + + listConfigurations(): void { + this.send(OP.LIST_CONFIGURATIONS); + } + + getInterpreterSettings(): void { + this.send(OP.GET_INTERPRETER_SETTINGS); + } + + saveNoteForms(note: SendNote): void { + this.send(OP.SAVE_NOTE_FORMS, { + noteId: note.id, + noteParams: note.noteParams + }); + } + + removeNoteForms(note, formName): void { + this.send(OP.REMOVE_NOTE_FORMS, { + noteId: note.id, + formName: formName + }); + } +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts new file mode 100644 index 00000000000..96d41ac6252 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts @@ -0,0 +1,3 @@ +export * from './message'; +// https://github.com/ng-packagr/ng-packagr/issues/1093 +export * from './interfaces/public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.lib.json b/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.lib.json new file mode 100644 index 00000000000..784751866f8 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.lib.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true, + "flatModuleId": "@zeppelin/sdk" + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.spec.json b/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.spec.json new file mode 100644 index 00000000000..16da33db072 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-sdk/tslint.json b/zeppelin-frontend/projects/zeppelin-sdk/tslint.json new file mode 100644 index 00000000000..124133f8499 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-sdk/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/README.md b/zeppelin-frontend/projects/zeppelin-visualization/README.md new file mode 100644 index 00000000000..2c4f31ef30c --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/README.md @@ -0,0 +1,24 @@ +# ZeppelinVisualization + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.8. + +## Code scaffolding + +Run `ng generate component component-name --project zeppelin-visualization` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project zeppelin-visualization`. +> Note: Don't forget to add `--project zeppelin-visualization` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build zeppelin-visualization` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build zeppelin-visualization`, go to the dist folder `cd dist/zeppelin-visualization` and run `npm publish`. + +## Running unit tests + +Run `ng test zeppelin-visualization` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/zeppelin-frontend/projects/zeppelin-visualization/karma.conf.js b/zeppelin-frontend/projects/zeppelin-visualization/karma.conf.js new file mode 100644 index 00000000000..5e77777255f --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/zeppelin-visualization'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/ng-package.json b/zeppelin-frontend/projects/zeppelin-visualization/ng-package.json new file mode 100644 index 00000000000..78b3ecf185e --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/zeppelin-visualization", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-visualization/package.json b/zeppelin-frontend/projects/zeppelin-visualization/package.json new file mode 100644 index 00000000000..1d4232be543 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/package.json @@ -0,0 +1,8 @@ +{ + "name": "@zeppelin/visualization", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^8.2.8", + "@angular/core": "^8.2.8" + } +} \ No newline at end of file diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts new file mode 100644 index 00000000000..415ebdb21ad --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts @@ -0,0 +1,5 @@ +import { ParagraphIResultsMsgItem } from '@zeppelin/sdk'; + +export abstract class DataSet { + abstract loadParagraphResult(paragraphResult: ParagraphIResultsMsgItem): void; +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts new file mode 100644 index 00000000000..07b02457dae --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts @@ -0,0 +1,45 @@ +import { GraphConfig } from '@zeppelin/sdk'; + +import { G2VisualizationComponentBase } from './g2-visualization-component-base'; +import { PivotTransformation } from './pivot-transformation'; +import { Transformation } from './transformation'; +import { Visualization } from './visualization'; +import { VisualizationComponentPortal } from './visualization-component-portal'; + +export abstract class G2VisualizationBase extends Visualization { + pivot = new PivotTransformation(this.getConfig()); + abstract componentPortal: VisualizationComponentPortal; + + constructor(config: GraphConfig) { + super(config); + } + + destroy(): void { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this.configChange$.complete(); + this.configChange$ = null; + } + + getTransformation(): Transformation { + return this.pivot; + } + + refresh(): void { + if (this.componentRef) { + this.componentRef.instance.refresh(); + } + } + + render(data): void { + this.transformed = data; + if (this.componentRef) { + this.componentRef.instance.refreshSetting(); + this.componentRef.instance.render(); + } else { + this.componentRef = this.componentPortal.attachComponentPortal(); + } + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts new file mode 100644 index 00000000000..15868457bbf --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts @@ -0,0 +1,81 @@ +import { ElementRef, OnDestroy } from '@angular/core'; + +import * as G2 from '@antv/g2'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { Visualization } from './visualization'; + +export abstract class G2VisualizationComponentBase implements OnDestroy { + abstract container: ElementRef; + chart: G2.Chart; + config: GraphConfig; + + constructor(public visualization: Visualization) {} + + abstract renderBefore(chart: G2.Chart): void; + + abstract refreshSetting(): void; + abstract setScale(): void; + + render() { + this.config = this.visualization.getConfig(); + this.refreshSetting(); + this.initChart(); + this.chart.source(this.visualization.transformed); + this.renderBefore(this.chart); + this.chart.render(); + this.renderAfter(); + } + + renderAfter(): void {} + + getKey(): string { + let key = ''; + if (this.config.keys && this.config.keys[0]) { + key = this.config.keys[0].name; + } + return key; + } + + refresh(): void { + this.config = this.visualization.getConfig(); + this.chart.changeHeight(this.config.height || 400); + setTimeout(() => { + this.setScale(); + this.chart.forceFit(); + }); + } + + initChart() { + if (this.chart) { + this.chart.clear(); + } else { + if (this.container && this.container.nativeElement) { + this.chart = new G2.Chart({ + forceFit: true, + container: this.container.nativeElement, + height: this.config.height || 400, + padding: { + top: 80, + left: 50, + right: 50, + bottom: 50 + } + }); + this.chart.legend({ + position: 'top-right' + // tslint:disable-next-line + } as any); + } else { + throw new Error(`Can't find the container, Please make sure on correct assignment.`); + } + } + } + + ngOnDestroy(): void { + if (this.chart) { + this.chart.destroy(); + this.chart = null; + } + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts new file mode 100644 index 00000000000..6c9bb110161 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts @@ -0,0 +1,187 @@ +import { DataSet } from '@antv/data-set'; +import { get } from 'lodash'; + +import { TableData } from './table-data'; +import { Transformation } from './transformation'; + +// tslint:disable-next-line:no-any +export class PivotTransformation extends Transformation { + constructor(config) { + super(config); + } + + removeUnknown(array: Array<{ name: string }>, tableData: TableData): void { + for (let i = 0; i < array.length; i++) { + // remove non existing column + let found = false; + for (let j = 0; j < tableData.columns.length; j++) { + const a = array[i]; + const b = tableData.columns[j]; + if (a.name === b) { + found = true; + break; + } + } + if (!found) { + array.splice(i, 1); + i--; + } + } + } + + setDefaultConfig(tableData: TableData) { + const config = this.getConfig(); + config.keys = config.keys || []; + config.groups = config.groups || []; + config.values = config.values || []; + this.removeUnknown(config.keys, tableData); + this.removeUnknown(config.values, tableData); + this.removeUnknown(config.groups, tableData); + if (config.keys.length === 0 && config.groups.length === 0 && config.values.length === 0) { + if (config.keys.length === 0 && tableData.columns[0]) { + config.keys = [ + { + name: tableData.columns[0], + index: 0, + aggr: 'sum' + } + ]; + } + + if (config.values.length === 0 && tableData.columns[1]) { + config.values = [ + { + name: tableData.columns[1], + index: 1, + aggr: 'sum' + } + ]; + } + } + } + + // tslint:disable-next-line:no-any + transform(tableData: TableData): any { + const config = this.getConfig(); + this.setDefaultConfig(tableData); + const ds = new DataSet(); + let dv = ds.createView().source(tableData.rows); + + let firstKey = ''; + if (config.keys && config.keys[0]) { + firstKey = config.keys[0].name; + } + let keys = []; + let groups = []; + let values = []; + let aggregates = []; + if (config.mode !== 'scatterChart') { + keys = config.keys.map(e => e.name); + groups = config.groups.map(e => e.name); + values = config.values.map(v => `${v.name}(${v.aggr})`); + aggregates = config.values.map(v => (v.aggr === 'avg' ? 'mean' : v.aggr)); + } else { + const xAxis = get(config.setting, 'scatterChart.xAxis.name', tableData.columns[0]); + const yAxis = get(config.setting, 'scatterChart.yAxis.name', tableData.columns[1]); + const group = get(config.setting, 'scatterChart.group.name'); + keys = xAxis ? [xAxis] : []; + values = yAxis ? [yAxis] : []; + groups = group ? [group] : []; + } + + dv.transform({ + type: 'map', + callback: row => { + Object.keys(row).forEach(k => { + if (config.keys.map(e => e.name).indexOf(k) === -1) { + const numberValue = Number.parseFloat(row[k]); + row[k] = Number.isFinite(numberValue) ? numberValue : row[k]; + } + }); + return row; + } + }); + + if (config.mode !== 'scatterChart') { + dv.transform({ + type: 'aggregate', + fields: config.values.map(v => v.name), + operations: aggregates, + as: values, + groupBy: [...keys, ...groups] + }); + + // dv.transform({ + // type: 'fill-rows', + // groupBy: groups, + // orderBy: keys, + // fillBy: 'order' + // }); + + dv.transform({ + type: 'fill-rows', + groupBy: [...keys, ...groups], + fillBy: 'group' + }); + + config.values + .map(v => `${v.name}(${v.aggr})`) + .forEach(field => { + dv.transform({ + field, + type: 'impute', + groupBy: keys, + method: 'value', + value: config.mode === 'stackedAreaChart' ? 0 : null + }); + }); + } + + dv.transform({ + type: 'fold', + fields: values, + key: '__key__', + value: '__value__' + }); + + dv.transform({ + type: 'partition', + groupBy: groups + }); + + const groupsData = []; + Object.keys(dv.rows).forEach(groupKey => { + const groupName = groupKey.replace(/^_/, ''); + dv.rows[groupKey].forEach(row => { + groupsData.push({ + ...row, + __key__: groupName ? `${row.__key__}.${groupName}` : row.__key__ + }); + }); + }); + + groupsData.sort( + (a, b) => + dv.origin.findIndex(o => o[firstKey] === a[firstKey]) - dv.origin.findIndex(o => o[firstKey] === b[firstKey]) + ); + + dv = ds + .createView({ + state: { + filterData: null + } + }) + .source(groupsData); + + if (config.mode === 'stackedAreaChart' || config.mode === 'pieChart') { + dv.transform({ + type: 'percent', + field: '__value__', + dimension: '__key__', + groupBy: keys, + as: '__percent__' + }); + } + return dv; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts new file mode 100644 index 00000000000..a3ea0b01796 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts @@ -0,0 +1,13 @@ +/* + * Public API Surface of zeppelin-visualization + */ + +export * from './data-set'; +export * from './transformation'; +export * from './visualization-component-portal'; +export * from './visualization'; +export * from './table-data'; +export * from './table-transformation'; +export * from './pivot-transformation'; +export * from './g2-visualization-base'; +export * from './g2-visualization-component-base'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts new file mode 100644 index 00000000000..5b57d653536 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts @@ -0,0 +1,23 @@ +import { DataSet as AntvDataSet } from '@antv/data-set'; + +import { DatasetType, ParagraphIResultsMsgItem } from '@zeppelin/sdk'; +import { DataSet } from './data-set'; + +export class TableData extends DataSet { + columns: string[] = []; + // tslint:disable-next-line + rows: any[] = []; + + loadParagraphResult({ data, type }: ParagraphIResultsMsgItem): void { + if (type !== DatasetType.TABLE) { + console.error('Can not load paragraph result'); + return; + } + const ds = new AntvDataSet(); + const dv = ds.createView().source(data, { + type: 'tsv' + }); + this.columns = dv.origin && dv.origin.columns ? dv.origin.columns : []; + this.rows = dv.rows || []; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts new file mode 100644 index 00000000000..a1af386c193 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts @@ -0,0 +1,14 @@ +import { TableData } from './table-data'; +import { Transformation } from './transformation'; + +// tslint:disable-next-line:no-any +export class TableTransformation extends Transformation { + constructor(config) { + super(config); + } + + // tslint:disable-next-line:no-any + transform(tableData: TableData): any { + return tableData; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts new file mode 100644 index 00000000000..8e8b5915031 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts @@ -0,0 +1,34 @@ +import { GraphConfig } from '@zeppelin/sdk'; + +import { DataSet } from './data-set'; + +export interface Setting { + // tslint:disable-next-line:no-any + template: any; + // tslint:disable-next-line:no-any + scope: any; +} + +export abstract class Transformation { + dataset: DataSet; + constructor(private config: GraphConfig) {} + + // tslint:disable-next-line:no-any + abstract transform(tableData): any; + + setConfig(config) { + this.config = config; + } + + setTableData(dataset: DataSet) { + this.dataset = dataset; + } + + getTableData(): DataSet { + return this.dataset; + } + + getConfig() { + return this.config; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts new file mode 100644 index 00000000000..8080d85ad42 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts @@ -0,0 +1,34 @@ +import { CdkPortalOutlet, ComponentPortal, ComponentType, PortalInjector } from '@angular/cdk/portal'; +import { ComponentFactoryResolver, InjectionToken, ViewContainerRef } from '@angular/core'; + +import { Visualization } from './visualization'; + +export const VISUALIZATION = new InjectionToken('Visualization'); + +export class VisualizationComponentPortal { + constructor( + private visualization: T, + private component: ComponentType, + private portalOutlet: CdkPortalOutlet, + private viewContainerRef: ViewContainerRef, + private componentFactoryResolver?: ComponentFactoryResolver + ) {} + + createInjector() { + const userInjector = this.viewContainerRef && this.viewContainerRef.injector; + // tslint:disable-next-line + const injectionTokens = new WeakMap([[VISUALIZATION, this.visualization]]); + return new PortalInjector(userInjector, injectionTokens); + } + + getComponentPortal() { + const injector = this.createInjector(); + return new ComponentPortal(this.component, null, injector, this.componentFactoryResolver); + } + + attachComponentPortal() { + const componentRef = this.portalOutlet.attachComponentPortal(this.getComponentPortal()); + componentRef.changeDetectorRef.markForCheck(); + return componentRef; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts new file mode 100644 index 00000000000..6e8a8a62adf --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts @@ -0,0 +1,32 @@ +import { ComponentRef } from '@angular/core'; +import { Subject } from 'rxjs'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { Transformation } from './transformation'; + +// tslint:disable-next-line +export abstract class Visualization { + // tslint:disable-next-line + transformed: any; + componentRef: ComponentRef; + configChange$ = new Subject(); + constructor(private config: GraphConfig) {} + + abstract getTransformation(): Transformation; + abstract render(tableData): void; + abstract refresh(): void; + abstract destroy(): void; + + configChanged() { + return this.configChange$.asObservable(); + } + + setConfig(config: GraphConfig) { + this.config = config; + this.refresh(); + } + + getConfig() { + return this.config; + } +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.lib.json b/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.lib.json new file mode 100644 index 00000000000..15689086f6e --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.lib.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true, + "flatModuleId": "@zeppelin/visualization" + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.spec.json b/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.spec.json new file mode 100644 index 00000000000..16da33db072 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/zeppelin-frontend/projects/zeppelin-visualization/tslint.json b/zeppelin-frontend/projects/zeppelin-visualization/tslint.json new file mode 100644 index 00000000000..124133f8499 --- /dev/null +++ b/zeppelin-frontend/projects/zeppelin-visualization/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "lib", + "camelCase" + ], + "component-selector": [ + true, + "element", + "lib", + "kebab-case" + ] + } +} From eafc5194e1f60a977bcd54da9e99f509855fd816 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Tue, 22 Oct 2019 10:10:28 +0800 Subject: [PATCH 03/19] feat: add base services --- zeppelin-frontend/src/.editorconfig | 12 + zeppelin-frontend/src/.gitignore | 43 ++ zeppelin-frontend/src/app/core/index.ts | 1 + .../src/app/core/message-listener/index.ts | 1 + .../core/message-listener/message-listener.ts | 47 ++ .../app/core/message-listener/public-api.ts | 1 + zeppelin-frontend/src/app/core/public-api.ts | 1 + .../src/app/services/base-url.service.ts | 34 + zeppelin-frontend/src/app/services/index.ts | 1 + .../src/app/services/interpreter.service.ts | 72 ++ .../src/app/services/job-manager.service.ts | 22 + .../src/app/services/message.service.ts | 319 +++++++++ .../src/app/services/public-api.ts | 4 + .../src/assets/fonts/patua-one.woff2 | Bin 0 -> 12844 bytes .../helium-packages/helium-vis-example.umd.js | 583 ++++++++++++++++ zeppelin-frontend/src/assets/images/bg.jpg | Bin 0 -> 185723 bytes .../src/assets/images/zeppelin.png | Bin 0 -> 3810 bytes .../src/assets/images/zeppelin_svg_logo.svg | 77 +++ .../assets/images/zeppelin_svg_logo_bg.svg | 77 +++ zeppelin-frontend/src/browserslist | 11 + zeppelin-frontend/src/favicon.ico | Bin 948 -> 4286 bytes zeppelin-frontend/src/index.html | 35 +- zeppelin-frontend/src/karma.conf.js | 31 + zeppelin-frontend/src/main.ts | 3 +- zeppelin-frontend/src/polyfills.ts | 63 +- zeppelin-frontend/src/styles.less | 8 +- zeppelin-frontend/src/styles/base.less | 6 + zeppelin-frontend/src/styles/font.less | 8 + zeppelin-frontend/src/styles/global.less | 108 +++ zeppelin-frontend/src/styles/rewrite.less | 13 + zeppelin-frontend/src/styles/spin.less | 138 ++++ .../src/styles/theme/dark/antd-dark.less | 5 + .../src/styles/theme/dark/theme-dark.less | 637 ++++++++++++++++++ .../src/styles/theme/light/antd-light.less | 3 + .../src/styles/theme/light/theme-light.less | 603 +++++++++++++++++ .../src/styles/theme/markdown.less | 154 +++++ .../src/styles/theme/theme-mixin.less | 12 + zeppelin-frontend/src/test.ts | 14 +- zeppelin-frontend/src/tsconfig.app.json | 8 + zeppelin-frontend/src/tsconfig.spec.json | 9 + zeppelin-frontend/src/tslint.json | 7 + 41 files changed, 3133 insertions(+), 38 deletions(-) create mode 100644 zeppelin-frontend/src/.editorconfig create mode 100644 zeppelin-frontend/src/.gitignore create mode 100644 zeppelin-frontend/src/app/core/index.ts create mode 100644 zeppelin-frontend/src/app/core/message-listener/index.ts create mode 100644 zeppelin-frontend/src/app/core/message-listener/message-listener.ts create mode 100644 zeppelin-frontend/src/app/core/message-listener/public-api.ts create mode 100644 zeppelin-frontend/src/app/core/public-api.ts create mode 100644 zeppelin-frontend/src/app/services/base-url.service.ts create mode 100644 zeppelin-frontend/src/app/services/index.ts create mode 100644 zeppelin-frontend/src/app/services/interpreter.service.ts create mode 100644 zeppelin-frontend/src/app/services/job-manager.service.ts create mode 100644 zeppelin-frontend/src/app/services/message.service.ts create mode 100644 zeppelin-frontend/src/app/services/public-api.ts create mode 100644 zeppelin-frontend/src/assets/fonts/patua-one.woff2 create mode 100644 zeppelin-frontend/src/assets/helium-packages/helium-vis-example.umd.js create mode 100644 zeppelin-frontend/src/assets/images/bg.jpg create mode 100644 zeppelin-frontend/src/assets/images/zeppelin.png create mode 100644 zeppelin-frontend/src/assets/images/zeppelin_svg_logo.svg create mode 100644 zeppelin-frontend/src/assets/images/zeppelin_svg_logo_bg.svg create mode 100644 zeppelin-frontend/src/browserslist create mode 100644 zeppelin-frontend/src/karma.conf.js create mode 100644 zeppelin-frontend/src/styles/base.less create mode 100644 zeppelin-frontend/src/styles/font.less create mode 100644 zeppelin-frontend/src/styles/global.less create mode 100644 zeppelin-frontend/src/styles/rewrite.less create mode 100644 zeppelin-frontend/src/styles/spin.less create mode 100644 zeppelin-frontend/src/styles/theme/dark/antd-dark.less create mode 100644 zeppelin-frontend/src/styles/theme/dark/theme-dark.less create mode 100644 zeppelin-frontend/src/styles/theme/light/antd-light.less create mode 100644 zeppelin-frontend/src/styles/theme/light/theme-light.less create mode 100644 zeppelin-frontend/src/styles/theme/markdown.less create mode 100644 zeppelin-frontend/src/styles/theme/theme-mixin.less create mode 100644 zeppelin-frontend/src/tsconfig.app.json create mode 100644 zeppelin-frontend/src/tsconfig.spec.json create mode 100644 zeppelin-frontend/src/tslint.json diff --git a/zeppelin-frontend/src/.editorconfig b/zeppelin-frontend/src/.editorconfig new file mode 100644 index 00000000000..8b14efe23aa --- /dev/null +++ b/zeppelin-frontend/src/.editorconfig @@ -0,0 +1,12 @@ +# Editor configuration, see https://editorconfig.org + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/zeppelin-frontend/src/.gitignore b/zeppelin-frontend/src/.gitignore new file mode 100644 index 00000000000..85158b7a442 --- /dev/null +++ b/zeppelin-frontend/src/.gitignore @@ -0,0 +1,43 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events.json +speed-measure-plugin.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/zeppelin-frontend/src/app/core/index.ts b/zeppelin-frontend/src/app/core/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/core/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/message-listener/index.ts b/zeppelin-frontend/src/app/core/message-listener/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/core/message-listener/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/message-listener/message-listener.ts b/zeppelin-frontend/src/app/core/message-listener/message-listener.ts new file mode 100644 index 00000000000..627ab3ec0cf --- /dev/null +++ b/zeppelin-frontend/src/app/core/message-listener/message-listener.ts @@ -0,0 +1,47 @@ +import { OnDestroy } from '@angular/core'; +import { Subscriber } from 'rxjs'; + +import { MessageReceiveDataTypeMap, ReceiveArgumentsType } from '@zeppelin/sdk'; +import { MessageService } from '@zeppelin/services'; + +export class MessageListenersManager implements OnDestroy { + __zeppelinMessageListeners__: Array<() => void>; + __zeppelinMessageListeners$__ = new Subscriber(); + constructor(public messageService: MessageService) { + if (this.__zeppelinMessageListeners__) { + this.__zeppelinMessageListeners__.forEach(fn => fn.apply(this)); + } + } + + ngOnDestroy(): void { + this.__zeppelinMessageListeners$__.unsubscribe(); + this.__zeppelinMessageListeners$__ = null; + } +} + +export function MessageListener(op: K) { + return function( + target: MessageListenersManager, + propertyKey: string, + descriptor: TypedPropertyDescriptor> + ) { + const oldValue = descriptor.value as ReceiveArgumentsType; + + const fn = function() { + // tslint:disable:no-invalid-this + this.__zeppelinMessageListeners$__.add( + this.messageService.receive(op).subscribe(data => { + oldValue.apply(this, [data]); + }) + ); + }; + + if (!target.__zeppelinMessageListeners__) { + target.__zeppelinMessageListeners__ = [fn]; + } else { + target.__zeppelinMessageListeners__.push(fn); + } + + return descriptor; + }; +} diff --git a/zeppelin-frontend/src/app/core/message-listener/public-api.ts b/zeppelin-frontend/src/app/core/message-listener/public-api.ts new file mode 100644 index 00000000000..ef611e2851a --- /dev/null +++ b/zeppelin-frontend/src/app/core/message-listener/public-api.ts @@ -0,0 +1 @@ +export * from './message-listener'; diff --git a/zeppelin-frontend/src/app/core/public-api.ts b/zeppelin-frontend/src/app/core/public-api.ts new file mode 100644 index 00000000000..ef611e2851a --- /dev/null +++ b/zeppelin-frontend/src/app/core/public-api.ts @@ -0,0 +1 @@ +export * from './message-listener'; diff --git a/zeppelin-frontend/src/app/services/base-url.service.ts b/zeppelin-frontend/src/app/services/base-url.service.ts new file mode 100644 index 00000000000..37aacd28157 --- /dev/null +++ b/zeppelin-frontend/src/app/services/base-url.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class BaseUrlService { + getPort() { + let port = Number(location.port); + if (!port) { + port = 80; + if (location.protocol === 'https:') { + port = 443; + } + } + return port; + } + + getWebsocketUrl() { + const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${wsProtocol}//${location.hostname}:${this.getPort()}${this.skipTrailingSlash(location.pathname)}/ws`; + } + + getBase() { + return `${location.protocol}//${location.hostname}:${this.getPort()}${location.pathname}`; + } + + getRestApiBase() { + return this.skipTrailingSlash(this.getBase()) + '/api'; + } + + skipTrailingSlash(path) { + return path.replace(/\/$/, ''); + } +} diff --git a/zeppelin-frontend/src/app/services/index.ts b/zeppelin-frontend/src/app/services/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/services/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/services/interpreter.service.ts b/zeppelin-frontend/src/app/services/interpreter.service.ts new file mode 100644 index 00000000000..7e05b6ce44d --- /dev/null +++ b/zeppelin-frontend/src/app/services/interpreter.service.ts @@ -0,0 +1,72 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { + CreateInterpreterRepositoryForm, + Interpreter, + InterpreterMap, + InterpreterPropertyTypes, + InterpreterRepository +} from '@zeppelin/interfaces'; +import { InterpreterItem } from '@zeppelin/sdk'; + +import { BaseRest } from './base-rest'; +import { BaseUrlService } from './base-url.service'; + +@Injectable({ + providedIn: 'root' +}) +export class InterpreterService extends BaseRest { + constructor(baseUrlService: BaseUrlService, private http: HttpClient) { + super(baseUrlService); + } + + getRepositories() { + return this.http.get(this.restUrl`/interpreter/repository`); + } + + addRepository(repo: CreateInterpreterRepositoryForm) { + return this.http.post(this.restUrl`/interpreter/repository`, repo); + } + + removeRepository(repoId: string) { + return this.http.delete(this.restUrl`/interpreter/repository/${repoId}`); + } + + getInterpretersSetting() { + return this.http.get(this.restUrl`/interpreter/setting`); + } + + getAvailableInterpreters() { + return this.http.get(this.restUrl`/interpreter`); + } + + getAvailableInterpreterPropertyTypes() { + return this.http.get(this.restUrl`/interpreter/property/types`); + } + + addInterpreterSetting(interpreter: Interpreter) { + return this.http.post(this.restUrl`/interpreter/setting`, interpreter); + } + + updateInterpreter(interpreter: Interpreter) { + const { option, properties, dependencies } = interpreter; + return this.http.put(this.restUrl`/interpreter/setting/${interpreter.name}`, { + option, + properties, + dependencies + }); + } + + restartInterpreter(interpreterId: string, noteId: string) { + return this.http.put(this.restUrl`/interpreter/setting/restart/${interpreterId}`, { noteId }); + } + + removeInterpreterSetting(settingId: string) { + return this.http.delete(this.restUrl`/interpreter/setting/${settingId}`); + } + + restartInterpreterSetting(settingId: string) { + return this.http.put(this.restUrl`/interpreter/setting/restart/${settingId}`, null); + } +} diff --git a/zeppelin-frontend/src/app/services/job-manager.service.ts b/zeppelin-frontend/src/app/services/job-manager.service.ts new file mode 100644 index 00000000000..7ce2ced8d2d --- /dev/null +++ b/zeppelin-frontend/src/app/services/job-manager.service.ts @@ -0,0 +1,22 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { BaseRest } from './base-rest'; +import { BaseUrlService } from './base-url.service'; + +@Injectable({ + providedIn: 'root' +}) +export class JobManagerService extends BaseRest { + constructor(baseUrlService: BaseUrlService, private http: HttpClient) { + super(baseUrlService); + } + + startJob(noteId: string) { + return this.http.post(this.restUrl`/notebook/job/${noteId}`, {}); + } + + stopJob(noteId: string) { + return this.http.delete(this.restUrl`/notebook/job/${noteId}`, {}); + } +} diff --git a/zeppelin-frontend/src/app/services/message.service.ts b/zeppelin-frontend/src/app/services/message.service.ts new file mode 100644 index 00000000000..051702b1aa6 --- /dev/null +++ b/zeppelin-frontend/src/app/services/message.service.ts @@ -0,0 +1,319 @@ +import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { MessageInterceptor, MESSAGE_INTERCEPTOR } from '@zeppelin/interfaces'; +import { + Message, + MessageReceiveDataTypeMap, + MessageSendDataTypeMap, + NoteConfig, + ParagraphConfig, + ParagraphParams, + PersonalizedMode, + SendArgumentsType, + SendNote, + SendParagraph, + WebSocketMessage +} from '@zeppelin/sdk'; + +import { BaseUrlService } from './base-url.service'; +import { TicketService } from './ticket.service'; + +@Injectable({ + providedIn: 'root' +}) +export class MessageService extends Message implements OnDestroy { + constructor( + private baseUrlService: BaseUrlService, + private ticketService: TicketService, + @Optional() @Inject(MESSAGE_INTERCEPTOR) private messageInterceptor: MessageInterceptor + ) { + super(); + } + + interceptReceived( + data: WebSocketMessage + ): WebSocketMessage { + return this.messageInterceptor ? this.messageInterceptor.received(data) : super.interceptReceived(data); + } + + bootstrap(): void { + super.bootstrap(this.ticketService.originTicket, this.baseUrlService.getWebsocketUrl()); + } + + ping() { + super.ping(); + } + + closed(): Observable { + return super.closed(); + } + + sent(): Observable> { + return super.sent(); + } + + received(): Observable> { + return super.received(); + } + + send(...args: SendArgumentsType): void { + super.send(...args); + } + + receive(op: K): Observable[K]> { + return super.receive(op); + } + + opened(): Observable { + return super.opened(); + } + + ngOnDestroy(): void { + super.destroy(); + } + + getHomeNote(): void { + super.getHomeNote(); + } + + newNote(noteName: string, defaultInterpreterGroup: string): void { + super.newNote(noteName, defaultInterpreterGroup); + } + + moveNoteToTrash(noteId: string): void { + super.moveNoteToTrash(noteId); + } + + restoreNote(noteId: string): void { + super.restoreNote(noteId); + } + + deleteNote(noteId): void { + super.deleteNote(noteId); + } + + restoreFolder(folderPath: string): void { + super.restoreFolder(folderPath); + } + + removeFolder(folderPath: string): void { + super.removeFolder(folderPath); + } + + moveFolderToTrash(folderPath: string): void { + super.moveFolderToTrash(folderPath); + } + + restoreAll(): void { + super.restoreAll(); + } + + emptyTrash(): void { + super.emptyTrash(); + } + + cloneNote(noteIdToClone, newNoteName): void { + super.cloneNote(noteIdToClone, newNoteName); + } + + listNodes(): void { + super.listNodes(); + } + + reloadAllNotesFromRepo(): void { + super.reloadAllNotesFromRepo(); + } + + getNote(noteId: string): void { + super.getNote(noteId); + } + + updateNote(noteId: string, noteName: string, noteConfig: NoteConfig): void { + super.updateNote(noteId, noteName, noteConfig); + } + + updatePersonalizedMode(noteId: string, modeValue: PersonalizedMode): void { + super.updatePersonalizedMode(noteId, modeValue); + } + + noteRename(noteId: string, noteName: string, relative?: boolean): void { + super.noteRename(noteId, noteName, relative); + } + + folderRename(folderId: string, folderPath: string): void { + super.folderRename(folderId, folderPath); + } + + moveParagraph(paragraphId: string, newIndex: number): void { + super.moveParagraph(paragraphId, newIndex); + } + + insertParagraph(newIndex: number): void { + super.insertParagraph(newIndex); + } + + copyParagraph( + newIndex: number, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphParams + ): void { + super.copyParagraph(newIndex, paragraphTitle, paragraphData, paragraphConfig, paragraphParams); + } + + angularObjectUpdate( + noteId: string, + paragraphId: string, + name: string, + value: string, + interpreterGroupId: string + ): void { + super.angularObjectUpdate(noteId, paragraphId, name, value, interpreterGroupId); + } + + // tslint:disable-next-line:no-any + angularObjectClientBind(noteId: string, name: string, value: any, paragraphId: string): void { + super.angularObjectClientBind(noteId, name, value, paragraphId); + } + + angularObjectClientUnbind(noteId: string, name: string, paragraphId: string): void { + super.angularObjectClientUnbind(noteId, name, paragraphId); + } + + cancelParagraph(paragraphId): void { + super.cancelParagraph(paragraphId); + } + + paragraphExecutedBySpell( + paragraphId, + paragraphTitle, + paragraphText, + paragraphResultsMsg, + paragraphStatus, + paragraphErrorMessage, + paragraphConfig, + paragraphParams, + paragraphDateStarted, + paragraphDateFinished + ): void { + super.paragraphExecutedBySpell( + paragraphId, + paragraphTitle, + paragraphText, + paragraphResultsMsg, + paragraphStatus, + paragraphErrorMessage, + paragraphConfig, + paragraphParams, + paragraphDateStarted, + paragraphDateFinished + ); + } + + runParagraph( + paragraphId: string, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphParams + ): void { + super.runParagraph(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams); + } + + runAllParagraphs(noteId: string, paragraphs: SendParagraph[]): void { + super.runAllParagraphs(noteId, paragraphs); + } + + paragraphRemove(paragraphId: string): void { + super.paragraphRemove(paragraphId); + } + + paragraphClearOutput(paragraphId: string): void { + super.paragraphClearOutput(paragraphId); + } + + paragraphClearAllOutput(noteId: string): void { + super.paragraphClearAllOutput(noteId); + } + + completion(paragraphId: string, buf: string, cursor: number): void { + super.completion(paragraphId, buf, cursor); + } + + commitParagraph( + paragraphId: string, + paragraphTitle: string, + paragraphData: string, + paragraphConfig: ParagraphConfig, + paragraphParams: ParagraphConfig, + noteId: string + ): void { + super.commitParagraph(paragraphId, paragraphTitle, paragraphData, paragraphConfig, paragraphParams, noteId); + } + + patchParagraph(paragraphId: string, noteId: string, patch: string): void { + super.patchParagraph(paragraphId, noteId, patch); + } + + importNote(note: SendNote): void { + super.importNote(note); + } + + checkpointNote(noteId: string, commitMessage: string): void { + super.checkpointNote(noteId, commitMessage); + } + + setNoteRevision(noteId: string, revisionId: string): void { + super.setNoteRevision(noteId, revisionId); + } + + listRevisionHistory(noteId: string): void { + super.listRevisionHistory(noteId); + } + + noteRevision(noteId: string, revisionId: string): void { + super.noteRevision(noteId, revisionId); + } + + noteRevisionForCompare(noteId: string, revisionId: string, position: string): void { + super.noteRevisionForCompare(noteId, revisionId, position); + } + + editorSetting(paragraphId: string, replName: string): void { + super.editorSetting(paragraphId, replName); + } + + listNoteJobs(): void { + super.listNoteJobs(); + } + + unsubscribeUpdateNoteJobs(): void { + super.unsubscribeUpdateNoteJobs(); + } + + getInterpreterBindings(noteId: string): void { + super.getInterpreterBindings(noteId); + } + + saveInterpreterBindings(noteId, selectedSettingIds): void { + super.saveInterpreterBindings(noteId, selectedSettingIds); + } + + listConfigurations(): void { + super.listConfigurations(); + } + + getInterpreterSettings(): void { + super.getInterpreterSettings(); + } + + saveNoteForms(note: SendNote): void { + super.saveNoteForms(note); + } + + removeNoteForms(note, formName): void { + super.removeNoteForms(note, formName); + } +} diff --git a/zeppelin-frontend/src/app/services/public-api.ts b/zeppelin-frontend/src/app/services/public-api.ts new file mode 100644 index 00000000000..4bcef3502db --- /dev/null +++ b/zeppelin-frontend/src/app/services/public-api.ts @@ -0,0 +1,4 @@ +export * from './base-url.service'; +export * from './message.service'; +export * from './job-manager.service'; +export * from './interpreter.service'; diff --git a/zeppelin-frontend/src/assets/fonts/patua-one.woff2 b/zeppelin-frontend/src/assets/fonts/patua-one.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..df2b38dc3025adb8fac45fad090522b6d62ea16a GIT binary patch literal 12844 zcmV+{GSkg>Pew8T0RR9105U894gdfE0C|`I05Q`50RR9100000000000000000000 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 K79##XA^-q%HNs>7 literal 0 HcmV?d00001 diff --git a/zeppelin-frontend/src/assets/helium-packages/helium-vis-example.umd.js b/zeppelin-frontend/src/assets/helium-packages/helium-vis-example.umd.js new file mode 100644 index 00000000000..fe77e3418ad --- /dev/null +++ b/zeppelin-frontend/src/assets/helium-packages/helium-vis-example.umd.js @@ -0,0 +1,583 @@ +(function(global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? (module.exports = factory( + require('@zeppelin/helium'), + require('@angular/core'), + require('@zeppelin/visualization'), + require('@angular/common') + )) + : typeof define === 'function' && define.amd + ? define('helium-vis-example', [ + '@zeppelin/helium', + '@angular/core', + '@zeppelin/visualization', + '@angular/common' + ], factory) + : ((global = global || self), + (global['helium-vis-example'] = factory(global.helium, global.ng.core, global.visualization, global.ng.common))); +})(this, function(helium, core, visualization, common) { + 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + 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 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function(d, b) { + d.__proto__ = b; + }) || + function(d, b) { + for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; + }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { + this.constructor = d; + } + d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); + } + + var __assign = function() { + __assign = + Object.assign || + function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === 'function') + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + } + + function __decorate(decorators, target, key, desc) { + var c = arguments.length, + r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, + d; + if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') + r = Reflect.decorate(decorators, target, key, desc); + else + for (var i = decorators.length - 1; i >= 0; i--) + if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; + } + + function __param(paramIndex, decorator) { + return function(target, key) { + decorator(target, key, paramIndex); + }; + } + + function __metadata(metadataKey, metadataValue) { + if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') + return Reflect.metadata(metadataKey, metadataValue); + } + + function __awaiter(thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function(resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator['throw'](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : new P(function(resolve) { + resolve(result.value); + }).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + + function __generator(thisArg, body) { + var _ = { + label: 0, + sent: function() { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [] + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === 'function' && + (g[Symbol.iterator] = function() { + return this; + }), + g + ); + function verb(n) { + return function(v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError('Generator is already executing.'); + while (_) + try { + if ( + ((f = 1), + y && + (t = op[0] & 2 ? y['return'] : op[0] ? y['throw'] || ((t = y['return']) && t.call(y), 0) : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + } + + function __exportStar(m, exports) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + } + + function __values(o) { + var m = typeof Symbol === 'function' && o[Symbol.iterator], + i = 0; + if (m) return m.call(o); + return { + next: function() { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + } + + function __read(o, n) { + var m = typeof Symbol === 'function' && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), + r, + ar = [], + e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } catch (error) { + e = { error: error }; + } finally { + try { + if (r && !r.done && (m = i['return'])) m.call(i); + } finally { + if (e) throw e.error; + } + } + return ar; + } + + function __spread() { + for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); + return ar; + } + + function __spreadArrays() { + for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; + for (var r = Array(s), k = 0, i = 0; i < il; i++) + for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; + return r; + } + + function __await(v) { + return this instanceof __await ? ((this.v = v), this) : new __await(v); + } + + function __asyncGenerator(thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError('Symbol.asyncIterator is not defined.'); + var g = generator.apply(thisArg, _arguments || []), + i, + q = []; + return ( + (i = {}), + verb('next'), + verb('throw'), + verb('return'), + (i[Symbol.asyncIterator] = function() { + return this; + }), + i + ); + function verb(n) { + if (g[n]) + i[n] = function(v) { + return new Promise(function(a, b) { + q.push([n, v, a, b]) > 1 || resume(n, v); + }); + }; + } + function resume(n, v) { + try { + step(g[n](v)); + } catch (e) { + settle(q[0][3], e); + } + } + function step(r) { + r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); + } + function fulfill(value) { + resume('next', value); + } + function reject(value) { + resume('throw', value); + } + function settle(f, v) { + if ((f(v), q.shift(), q.length)) resume(q[0][0], q[0][1]); + } + } + + function __asyncDelegator(o) { + var i, p; + return ( + (i = {}), + verb('next'), + verb('throw', function(e) { + throw e; + }), + verb('return'), + (i[Symbol.iterator] = function() { + return this; + }), + i + ); + function verb(n, f) { + i[n] = o[n] + ? function(v) { + return (p = !p) ? { value: __await(o[n](v)), done: n === 'return' } : f ? f(v) : v; + } + : f; + } + } + + function __asyncValues(o) { + if (!Symbol.asyncIterator) throw new TypeError('Symbol.asyncIterator is not defined.'); + var m = o[Symbol.asyncIterator], + i; + return m + ? m.call(o) + : ((o = typeof __values === 'function' ? __values(o) : o[Symbol.iterator]()), + (i = {}), + verb('next'), + verb('throw'), + verb('return'), + (i[Symbol.asyncIterator] = function() { + return this; + }), + i); + function verb(n) { + i[n] = + o[n] && + function(v) { + return new Promise(function(resolve, reject) { + (v = o[n](v)), settle(resolve, reject, v.done, v.value); + }); + }; + } + function settle(resolve, reject, d, v) { + Promise.resolve(v).then(function(v) { + resolve({ value: v, done: d }); + }, reject); + } + } + + function __makeTemplateObject(cooked, raw) { + if (Object.defineProperty) { + Object.defineProperty(cooked, 'raw', { value: raw }); + } else { + cooked.raw = raw; + } + return cooked; + } + + function __importStar(mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result.default = mod; + return result; + } + + function __importDefault(mod) { + return mod && mod.__esModule ? mod : { default: mod }; + } + + /** + * @fileoverview added by tsickle + * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc + */ + var JsonVisComponent = /** @class */ (function() { + function JsonVisComponent(visualization, cdr) { + this.visualization = visualization; + this.cdr = cdr; + } + /** + * @return {?} + */ + JsonVisComponent.prototype.ngOnInit + /** + * @return {?} + */ = function() {}; + /** + * @return {?} + */ + JsonVisComponent.prototype.render + /** + * @return {?} + */ = function() { + this.tableData = this.visualization.transformed; + }; + JsonVisComponent.decorators = [ + { + type: core.Component, + args: [ + { + selector: 'lib-helium-vis-example', + template: '\n
{{tableData | json}}
\n ', + changeDetection: core.ChangeDetectionStrategy.OnPush, + styles: [ + '\n pre {\n background: #fff7e7;\n padding: 10px;\n border: 1px solid #ffd278;\n color: #fa7e14;\n border-radius: 3px;\n }\n ' + ] + } + ] + } + ]; + /** @nocollapse */ + JsonVisComponent.ctorParameters = function() { + return [ + { type: visualization.Visualization, decorators: [{ type: core.Inject, args: [visualization.VISUALIZATION] }] }, + { type: core.ChangeDetectorRef } + ]; + }; + return JsonVisComponent; + })(); + if (false) { + /** @type {?} */ + JsonVisComponent.prototype.tableData; + /** @type {?} */ + JsonVisComponent.prototype.visualization; + /** + * @type {?} + * @private + */ + JsonVisComponent.prototype.cdr; + } + + /** + * @fileoverview added by tsickle + * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc + */ + var JsonVisModule = /** @class */ (function() { + function JsonVisModule() {} + JsonVisModule.decorators = [ + { + type: core.NgModule, + args: [ + { + imports: [common.CommonModule], + declarations: [JsonVisComponent], + entryComponents: [JsonVisComponent], + exports: [JsonVisComponent] + } + ] + } + ]; + return JsonVisModule; + })(); + + /** + * @fileoverview added by tsickle + * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc + */ + var JsonVisualization = /** @class */ (function(_super) { + __extends(JsonVisualization, _super); + function JsonVisualization(config, portalOutlet, viewContainerRef, componentFactoryResolver) { + var _this = _super.call(this, config) || this; + _this.portalOutlet = portalOutlet; + _this.viewContainerRef = viewContainerRef; + _this.componentFactoryResolver = componentFactoryResolver; + _this.tableTransformation = new visualization.TableTransformation(_this.getConfig()); + _this.componentPortal = new visualization.VisualizationComponentPortal( + _this, + JsonVisComponent, + _this.portalOutlet, + _this.viewContainerRef, + _this.componentFactoryResolver + ); + return _this; + } + /** + * @return {?} + */ + JsonVisualization.prototype.destroy + /** + * @return {?} + */ = function() { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this.configChange$.complete(); + this.configChange$ = null; + }; + /** + * @return {?} + */ + JsonVisualization.prototype.getTransformation + /** + * @return {?} + */ = function() { + return this.tableTransformation; + }; + /** + * @return {?} + */ + JsonVisualization.prototype.refresh + /** + * @return {?} + */ = function() {}; + /** + * @param {?} data + * @return {?} + */ + JsonVisualization.prototype.render + /** + * @param {?} data + * @return {?} + */ = function(data) { + this.transformed = data; + if (!this.componentRef) { + this.componentRef = this.componentPortal.attachComponentPortal(); + } + this.componentRef.instance.render(); + }; + return JsonVisualization; + })(visualization.Visualization); + if (false) { + /** @type {?} */ + JsonVisualization.prototype.tableTransformation; + /** @type {?} */ + JsonVisualization.prototype.componentPortal; + /** + * @type {?} + * @private + */ + JsonVisualization.prototype.portalOutlet; + /** + * @type {?} + * @private + */ + JsonVisualization.prototype.viewContainerRef; + /** + * @type {?} + * @private + */ + JsonVisualization.prototype.componentFactoryResolver; + } + + /** + * @fileoverview added by tsickle + * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc + */ + var publicApi = helium.createHeliumPackage({ + name: 'helium-vis-example', + id: 'heliumVisExample', + icon: 'appstore', + type: helium.HeliumPackageType.Visualization, + module: JsonVisModule, + component: JsonVisComponent, + visualization: JsonVisualization + }); + + return publicApi; +}); +//# sourceMappingURL=helium-vis-example.umd.js.map diff --git a/zeppelin-frontend/src/assets/images/bg.jpg b/zeppelin-frontend/src/assets/images/bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cffbba3ff89e5b7e68a247d7a3dc2c1b91c33001 GIT binary patch literal 185723 zcmeFacRZDE{6Bu|t;~cZBODn=2&L>z*0EK_vG;CLA&Ih+Qg(L8OhaU~%Z?IhM=2T_ ze%E#1=Nw1%et*8-@9+0}{Qmg8^>EI8U-va$*Xw$|=5^g#9A2E@Sf?B1?Sa9VnqsgR z42BWIN`=ADfGaA71n@zHUenQoYii^gK08ZAkCtPoK{+S!8Z@NhAzp)8RQwn^*bn$E zq7ooqS5iq*w8g<5Fk;}ho=O3^<_6cpN4zn#;2XAm2foArUGN|Cj~EO_VkPNY(>>6Y zAZ+dx7#0}n6&NI}tu3q|k5dtrG<9+F4uFMH7*L?9Ca#LPnV z`$H-WH4W+EN(>b(6%92lH5~&zBOMLvM(~h>hISn%j!x6Ug-ay(paMO2YJRPjs2Gps zb=QrGA%`AlGl-{IeVCYa3svGRs5`t~C*9gzx4ke-dDTPwB;n)5*p9yNXP@*WGK%8r zGjH@y%@sFv4m_W?@ra1eDrxK*eDQgczO85Ek(1|}x`$qV;lNN+18Qjzni%Nmln@fu z;W%jl1;HX*bP5MkxgiDDEgyW?DCRm5awtvPipMQfaaNoj62P!tsr?}!qHvYFE?zl| zFi9ripDB=7B4P0nhLr{xlLNC2^FC)qOSj%(cEONss-sPt9!1Y=|LM)H#B4&0;Z(BWuZzFZj67()+jrCC zax;`pT(~$66;(oPzRy_PZ*OEa<;d!1(4D`AqBWs<94 zWRJ7NihV!4EA2hTpzq6x7e&QsW~aorE#99?-SKQj{E5~1r`eCK4Bhs>7aX!1)BI&K z?z`=6f^Jgq=>F=v%06L)Rdc)d9}vJ3{>#El>0x@+2!gJRtPm$7I-9 z#yfZsWAJ{W_1D4~c{Y>gudO>fUX@(!8fIPO<@)(t30=yVtcC}=f3#ElUBJ5U$8A_^Mq%%o~|E#6#Yr;YvY~2tG3ZvuRG*xnndgtll1+%r_T(f7z;hsF6{PQ z#0Ix|VV4!UBH(;kG5h@&?6ts(y^ib~vcr!U{JL2#d+AKB!%LOzrIR|*wpLe0 zui>_dCfT zwb{`xjtwgYRlhpUzWdT-OTR{F%dzOapSXjoD%M|&^G@e-Jbr1#j}yQ4;spY1l-+#F zPulyMs8>A;{H3{wdD>UVhK)J1-zM5%Lq@Ogh|0^?`Ip`Aoqxga2&?B{uEg7Y5L?9bXABS4 zb+xV;TWP#~;n({<^Xmr|?#kR))2`zs@#%BGxwp0W1Mhr~hW6}bq`Fc+aVPL&-;IEY z{(aYKT@-Z|)@DaPbpP&O@VSxwb8|97y?*4mu{{^Fx*i+!xXAd%JkdO~HF{3)NY4I! z9xp747;E(MqTfhF-`9A3Dsy4b^~&h*r-ruM45@Px-~7!A+`8y%za9B0=EZdI8{y-* z1(_t{H=(%+pPE&?BrMdgHGE4f(LFgiMc)(9vH85^Hu>6=$oD^{HVir*O?fjsJofbI z=MKZb7mvQ28yjtKx+Xa9r1tKS@8!Kt1~Rk((g$ z=ObC#r!x~bO09G(uA0Ft?EF0bYp>?(Q@r+9kJ-N&aG#lbemgL==G{W{w^{r4_VbKt zjdp&aG3-@2id54=<3H}$4?orB*Ou~%>0;cX|M17XcaqOlVqBHh7RuhPyIg=vwXu zi<==?MWbRae2EDqGG3mXhDRkVU1Y3EOq~`nix20D?(LNh_%6tI{o7r6u5$Y;>(7kE zKlU9Pj1f;3<8<6?_S!7r=+;8*n}fG|Rv1if4SPa&VVBI-A&kSs%NgG+Ge_M`xx)7E zef5(%lYiYgW?4Rh_peHCD4;!sZL%ut&uh76*0R?vm)2o?+V=e-rs}+F3fF}PYSDD3 z@(3&5>hJviyzJ**s)6XZidXj%et9io{I3;S*UB`W{^_~Wj+GvxIr{Ba;n6RuE6x-8 zj_&0bG7a1;G--S_bs?a3{Fdcg#j9tk`nVslbng<}mK*ph$%fX)gW>tP@@-!^6+`I+$z}AJtUwa)>lg8e}=hw_Wf4PX^T*NFeee(Kl<dE%#J)$xrX$3JW~{*{#DEWK`E z_{ zxrr}Y@>V%|^fl^yZ&2)8#5hM+EMjQSv09$(XLxV$q0{}Np1{cFpc}!4BOhnNX$8Ez57RG&gGfi*z?&%fdg5(YWsFvpI*c?z14lAuGCU;v9&MU{rlwp>IIV%ZL-eZf#=d$x>6=GL>GdR77iT-|j<#oxa>_v?Dx}EQ%!|%Mdez8CQV>)H zKecw&GzZYGN&2$+dvs2J#>rdnnTpIt1X_;2Ib9b#k}~ENDp@z=KQ}s4^XfqEIlW>> zY)-CaDJEAKb_uou;vsyKoz4NoR&ddeha}1vGBHDKY*dF&@ThpD?Y(g?5o@2`;b2zh z^jx{V(di}E?9+9puN2=|$?)>pqy9}T#qB+^teAxp5jP!w)P8?0wNZ``oA~_trBync zrasN3)HmxMpKt74m-eWBVQb+4i`o;G_a5T1-|@#KyfF)=KOZz+3vZ21OZ?$mM?ZFQ zWxJwp{JG&y&)RGAU#@I>ge%;p`}V?}&l^^YN>guNSWN{3y**-Toq#=8N0wMNIA|#SfPxws)?ZxO(Nz#y9r# zD?ZxK2`pl4E|dfee@+k*6gwq#(__lyMB=M<9M4M32m3qT-W>P3XJhX?F5MC#u{~=* z$|R@7kUO0-ElcXCK^k=42jsk_L1e$Fk6~mlO0WNrV@)lV0lFM?)go7&Z)n7^;}$q_ooZtbK)MtiZ`oi zeym%0{l|svihVGZZkiobXWc4x_2vchm%dzg<}_1-5x%{#?C+Sy5nP=ck0Ac zM*e_6*ollq%)<;F&Ke9O69{lDT`i?ri;6i#(TF)CVyrnE3U0g?!UAbkNd7)Y5oQnvEd ztkWmC>wNhIdp)Z<-3Klq&(x4-L*R2I_=Q0~2E!5*0@4)Q!^48Y!@w=fmSD_7!>n8b z1O1R1h5=y&LO{4bd;qVJZ+}l=@Vx?QYvmmUYiN0g!8Y(Q+9x5z%*CHzt!rluiqOH( z2Ie7wfgVYMVOkDj!$xqBY!y?Exy8eDT^6y72~`XK>-lxg|>Sx+m(-5MVdrYCCY2UnB)qvhu={I_z;P`@yw z+&s!p!;)N`IfCF878s)A66OMDfDmc!X&y?#0lbEl!8aruV?yviyZLQcYmgB8+ptz{ zerUOQh?|Bz>;d^%=@t?gWa~u$iwiL69pFh~A3Jk9QQp{)GAM~c zNX^yD?`b|E_e{tq_@F{i3et!0hnXlA0I&!J^GK-yM$W?nJn;jcXwr|0SdxD#hg_ln z>QZ4Gz$dAW5>f(@Pb@+D{0i>*h@vlELui)dqK3e?0^9zD40R~RV(<(J3=djXL?0O9?deU{jJj5^_w7g-=!4BAJSKoepC&xSZ<#?rn7_QlG}Lps(V%zn3$u3d zTvm(Sjo{~JO^6CJ3^lMeH6bY}MpE&z%1mB?A$v6aygf;rT7gd9fK&_^z~D~sa0y40 zCQ}3yJ`aS47?YQO*LE*z5P|h{Q?4fiSG#mwMX=&r z@4;^#`1J*sPcfJta1AyJ_(0@S}G)SA^ zLa1AaH?q4R40{Bb^YAcr5kM(2g&@)iQF7P7`Ul}$LsjY^t06;d0+fY30P05<|`1A3A5 z_I6kE*yu`dQ}7_j006nkD7v~U$f#`Gq$opBR91DB->8gpQFSG?NAs;Ne*b%8ySu4* z1cvy#0A_gmyLb}he1Zs`NSA8bAq0@H1$!@GV({d!jJ%QzPTm@)tfnNdrnpfW;8$KA ztqR!U?XmmMs>DOa%c=sgaR>W3|5IfseF!fNyhFhj&~Bo9SP_DWSAO2Y2+-7A+~7`{ zx=5%?1i@W|Sm^zK%uTe@NU-~}eA>Uw1+5ku=n)p_5<<}Q1glNb0ZTiie>sFrBGApt zCBTya6p);V9GHm#M1u7K-JFlpO?9%3*;a6X!R1NLDcON}f*3vuj`Wsfw$iZh%Vov#` zn~&Zspl@2y(9wanzi1h0X#A1QP582xCY=SKm_GPmszQG42kz?+CPcM(EL}rpH;O50 zAL3235-)J80|)URDoyGfUS*vQ-Yer;*5N98plaB#n74zW|bKnb)tp&>yC=R_*8@M zdxLUuMio7)nDd{^b3aIy$J+0$WZBJaJK@LMHRQ7=_4o=Kc}Jh=qm~Ez!u2i-mRf%m z^w$=@7-DV9BEAnl(CHwPSS5?o5;D#!iJfeWz0`l8S~K7XkM@pnoLTS;PEuNNCZue) z@&!Lko%Z%3CJ8P{;in~nQjc?6%%w-g?g&~IGMDbR!;OoSW|$WoX0GmZPHg3gz`ny@ z#t&E2+PYE}bbtrX63bMou`$iZuF~yXaYMembjNUoEB|EqQ92x6QwV!CqtmfudFw{p zQZEyrb3fOr$)#T5AhyVW!xeVmnkuo$1Yb*};Wr8>ZUd1fVEWbA$H6wo!H5?V8~(>u za9+et96Ftyz#I$7ZS;B2J|P)ppfsq{DcGjg<}P+{sgaW&xEtvNZHdib^FW^9P6xgU zNCO3d5!eA_-b;0dkt&t=ZsKff5wb7iD`7Jm$Z8dY1A<65g7|U@-jiyO2Oh4_M(BNj zlthE~^R}Gy?c;I#@Ft`qYF7xYXL0b+dOGPldd31*)h%jt=G^vkj`+)U&gK#-*|b)@ zJcO(E5jC97Ouu-=m1mssG4rNNl@W97-cCmq4|%i6o;TtdXEIuWKlEZstVT$2 z^N%`-94YyQyquL{Jc_*v(rKe#4&*9#awi_Tmq5c^U^6C*E$5aq!G{Mgq3YeAfN8|p z%mV&SBsL{h5vs8Tv$j0mBIix;c&lz6=NqmhWH*tQzbp|niO06`_>z~W3;Tp}O&by~ zHCI$5HlIP(>ow>Z6_jO*PQm~mSkl*tm$K27g@A`>LP>5UUOB-jsE|ncr_KOTPl#9| zp{^1UPJE{$5e|dcnPvc;CHi)r&Y9_t{Y9ap@`nb%hXj?T835o%>hkXFDQz zE9GkM-`M(nVI)Iw{*sIHw}%86T6atFYh zEu16Z329lD>CmRcOQ#WjkziARvt>orF$y4ngUofXQiyt_SRkMf;VQs}zB~Z;w(tY2 z1hSIN=wc9;WVv1?u1`QVqAYy(f#E8Fo+K@>9fV%h8BkUW7Qbl-tT%=%L*B@n^xhlmW9%j#!~DMp+-NTWqDxhSiE_{XQS4VT|luSuC(7SCmnYSJy#W z$%)YhZ{6vbrg5lQ$~>^N^nLsTpZkwD-1;!zDVcCb4-1VKlwb-2PBOtwiGV?T2z$UM z(2x)SlSqjbu*-mV6yO~OW{}hn?XebMkf;Sodh(KMbqS&-DMtHkB}umxP#uE$olc0X zfOc?sDr{umQ&TGT29`Ww=@pK&m&|lcb@zqP z?s%snv(~eKRyVnk#)tK^YtRNB(TP#cj5_soZXp9zAzL>R_wIo@}gG zopHS<RsSKXD(kpA7|b_-&T>ctC>e{@|DP{ck&-KHA;$_7)|t=W&61m9b!*X6};-;ER)G@ z;Zn59NqipcO0&Mtb*`@Mj)GZ3dwFw(Cl4?ONkmyjRsdp;q~Rj4B&OLxjKYNlrUzl* z(qe(z%3zpaOagKt=NCh}|q}PPz2)h@$|l6{^8eQoJUs`T&C706;nuts_M2 zpOjE`RcmX-<68pEDap$JHYk9$6X+zWLjUjBMQ1<;=6!@cfjmCVzmIDNz_c`-g91Uj^0I?y)TU8Zpi`8|raR*C*k! zMtkE+yrxBhq{B%TEa#>Y<vWfAK;-rrs-eOl+F2Tz-plmp}7V=p?X zxJ{;4hIceuc&TrRO%$f)S!Bq2LvSRUj!mm_-2!>=lj( zRcKfcHUd>!jU5OkRUp}Y2q46Q!>DHvJMM&sI+5fvJjMZFL+cv#`+6^fT?Ybfu#hlT zx|9Lqk3Sm3lYKf8@qGW<=Ta!NY$#M;E>ndh`J@COf>Wq`N!^gc9LNm^M5Q-5ku5l= zzT_OjSP8O;dUuA3A6-ee|(Jn3hA_4wd|S zeAI_axIg*GujG_p$FQ|R&^><_->&*#I@Wy`-P*D9I%cRK*Gy0UiVYy_8pHJ+NWH(*Xom>a^(7f6P?peM|c`$9~9sBEgs!vbSC?8 ztweu)gl}Qk?RQlpd)zSvgX=E&y0{y&V-lk#1-Q~nHY8Q*JrgUv(MX*+9fyx99jC6^ z#C#>#JpRfRJ5hxpmmw;}F12z;M>K3LG_B%IhhkN@uB;2q3L6A3NY>L`MQ3;1bxr|g|nR-I#&Fpd!KH8{q z=yneP0R!PHlu$AY-$Av7x^OT_0#DN7O6Zby zfvf&br$_(`ppD={lY2s|BQdrU3|md$LmMa%OC2HD0;aPJf!ANsPr(#XKe?ez0%!-4 z4a*Wnz^%@v4Bz1(mCNZFM98j1kPF)qACr{kpJb8eLJW5nhu#Uf4pz&&u$yowd-J%C z;WQJ!G0T%%l381ibg-IoAHEd0J71|=pcv1YB5mO$W?{9`Na2?0$rLv2?S2~W)FCoH z7{y*}eZ-q{TbM_CR#jt*hBVx~qz<%J%H{?H@E5IMjZMa7l+k&XCmW4z=M&}_7t>Wp zDVCO;x@st#ofcn6XXmAD&rl&1k=(Ee#g5_LS=c-uo1^w+6tcUsW zB`)!){(COhJS?jmy+2uD@U>!;TclGe)xDuC&bobw8>_?EW?6O68O`xPjRDpJYBH!e zU@{5SelWfSb>NaW27@A^0H6;^D~!Sc-oYFQG++Q;?|(-QS{tfqNNpMH6wVS(2!Y|B zF*d0f&KHJTcE6*Qyx)ih1<(lvQKcY$@&3#z2aW!9I%ig{>Y#c|E4rcHIssFVh^ufg*;<(>|42({ z=Hq60zISSFK1L(&`@1f#E8WV+e$(RqO>vF3sWPeU*)Ps+j`Y>E*BEvezepW_|Llvs zyM{72BYXQ4j%7U#uL-dh?^y1^F394Se_@Xobm9E|TO%76ge-qTo^h zxFOGNj&|C7>M&~R2g!t6vZLYc)J$61(yGE!WidM?I*y548s9%_YtY)i%P4P$R&)6W zgSP5PM;;sNyf&liVtH{QAz^4Dox&3&nRYm=6@n_t5(Js(dO?6t2+4AEQh73sg>UlvSd1!by^9q`PcXc&D0R}7)liqWs_ZIa{owTf;gi;um>C+ z6=M3E+!u;RAVa@J@o0}r)&+o2uM5_)uktZ;Van0gr zmhv{@>Xp_ktLFSszu3v$1?Hk*fmt&SFNQ~ZBsFds)ZEJ69=6j`31`^vpGw~^>UsE_ zpOR0!qw*`|PRX8pyUxv%6$$t;R_=1Sh{F7Fu|wv)6{5JL9A(jPf!c}-o4EUrpXEs2 z=G$mXbE9kh&D}0hcf>clKRnBx*_p00S;duD&nJ{!u`eEbGG46<+nLxGq#qsk~iLjbW7AI48e&bhNF02X zx?-JGD#0}2>s+Vzp5h<}yd}m~idN23D%rDvCL!2~DgQmG6yX z1l}FuGL~Lza>8#zI<}9wG6G}EqoH3WU7K_+VV8!n|CUaHv4&QDdBvVCCjq|`sUIo7?Zgbu@J>|%Ao zP&Z`4Yx}-roQX(`@TgdMb~!0;Orh?OEjtLjff8(eBX$Uvj2((*O<~mBE=|>FfWtN> zxjUR~?oA1JZphuX5-;PBNY!S!>WVy_q{Z6xy|s+-NrK52@tT_2hX(4Jd1|DfOF-Pr zi2@gJ@lk|;aPVMS7}41{EZEuOwN3LqHBfu|ks>pf>pP zdVO!zgyO`DM8;zbhIY}jh*=3`+^WA7tt)j!zE8|eIG{Nr?vSEYAHN3MNrv-I-sZLr zi8?DpP3TSh8P<#sncpAqKKfW(@?p4#w^;3-Ls%-+l~IROF5g{7ityrJEr`!kI85O`*fZlcp65R*dHgUD@8{J?S-TpdI|;9nA10z^hx79YDiL44Hp2$SI|Fk z3dUD@C*~Eker+aXv=7ls(1?4rsRv10>NXA^DmJ2j+W#&n_dbVgW=~O^5ZxXZe7^*b zb>h~|PwUIgcY$@T#@^>EpHV@=6^P+sG(rQbh8hM?MFj$4NuN?UJj-=m33SR)Py2ty zFA+ZIQUf3_VbTgv@rPQZsE%y7wi1({z%i*N0+vJ!DbYU={5ydEs!hq78QETf<3xe< z2;7c9pxH(t1OUDO>r3cF-B(B}bgh~aVYVNN4y6Vs7NF{(62UoA1IP;a<7Ym)Ya+%b zOw0SJG#lByEPYc1j&44)8z|doT&Jg8UiUL~g)JsbMtPrfs)9r;KAaPDlOA#Px4qps zeyPmWc#eh%qafzsmOpaL5pT55x>v#WCv%#atdxGwK@+#SU4uI#_znD-HM|?lcx>l& zrDJ4DjiW@znW?m#eG={~rt;Sv@=_4HubI`m{ba+*L2Y@r-$LR?UX2$KFS#Slu2WYreo?Y8J2Hc zi2}CRARgU=J?{2Wd`;EZ>MNVtsG%MGwdOS+q45+u^0r!g@7$=CJf+rBd~7e4!@GDKj~YTgJ?_FAaU30XP%F?P4YsZii?s&_0ZcMysdhNcV zwRn{u`ia-#w)V@F@CTp09eO~CrhCIFVdbgOv{ko0jyXLFy`q@$)xC#%*j1YFXw5OE z9j(P7!sff^d%JSav)Hdrv2fg{E$k)wWEfX=qdYHx>SKFFH>$SEAv-~`2r46JhU;(? z!8ex)-g}?7b=Gsk;t(gvhKI@nd>BK+lIF;BM@%l$SLkc}Dlng4O+8%Z_rvk%>r@Rr zjz-P2q%sSW6s;8Q={Q@lEy-rsMC#M-oM261`Uqec!7@p4)dBTk(?+pKh*u63@A4hk zDAYicno+D2SWz@7^%v)uq8iDZ!`fuW1r-U{up91_L54uTo3!}9!QT$}0d3HAhEfGJ z?NV2$gxNud7xnr{YIn&`VO@SO3usMuB}rq(GOGMoRhnn}N6HA|pDoqN;V zxkiK2tf@@rd4`!;eNvK|>g{wf#lc3+=4#6Smy4K~4by6&WU;fOnL76>&{q=APsPqWbdk zlBFXnQ0TQlpg?D)89nwxentr(xOTrS<-gu8VS@|?M3b2iGcU)14+T1`CLskv73xqp z5$Ie};sy;NEAjmx2L>Gx3iP1fErq}!I7eu?;0Oe%{t`A8EA5d2H7g6!N$n*z3EXJHMXsa*UhN zl9c6L`{Tq&Qy=cP2xZt zMi(Az)1*?d4>}8ouSvGzDDyx_1o!NhIEl~)_%^$|2vyx=?x7q8`7$(l+6AW7fo5C( z6-3MXq*y=b+#zVTISBfMPTs#%1!qq|CGsMZYI-HX!&(rJ&{8jZ3{L=(1Ov7tX51#h zK?YKG6if|3&9a&3qoaxYCD1QVg^`pkm;uy?RjJ5uXbxm~P4XOJcm*W_Zcg2Qk9#|N zhblAjZK{N?t0{}&kER#T``7zMZ1I@V=jyj&6~DnNRJ={;t?gh_jiaO7Owo6H1=LeLwzBS4{Te4zyTS@^${{;}R5mYr`XB2c! z>Kc|3!G?$)dTxVa0E(WL)_>V^h%S3k^InqIf>^G+w8)IDJx&i443@*Imj4Btihb=L>y z=}xM#rWiPbZ5ov8FcL$RJIWbUtrRBKJF-ddDDBF0zo%PzgEqw2Kip-b!r@D~T~52& zH`aR&;y-UDXl$(KzgDcIIs81MbG2gF*LdBu{$nXs!KPA1r{nl)Bp$%J9y0 z-7inYzCZNvS{9ve7WR!Gz6OLP#65YyiOHoD3kgjm$tIoafcOH(-U0W(Mk;OzV5H^y zZJB<( zg{T&nBMV{`+F#NED|B4g0qSI+yS50ltbk?&CpwUT1>zS5m6Tx=S>lkW9RcZu#fR@d zdA$`Y@Y%ORh1Huo`FO8J{@8@kr92BwfDvF{5ac4q4xkyQ;N6?aDqH480qU|_5*${s zELuFC3ORBnIfAdoH&9uNo;iZsW|Au`l(4Fn_sfCuxdSbpYvM7Mo-!9?x<3) zPYL*U*)jT>$#iVlCDdZ*@2AmGqFkOj8mFWlqFg>uLJJ~*@*y5SS?n+$$X9|85LyQ` zbph-mv|40AgGUtpR4oX-sLrI!pD&vRu@e|V0Dh6IImJ9FMj<@stgggU2O2(pw2#Nwg4|y?R|FG;Iu!K+v&E#^@U_49Nf~@NRSCW9So1eJ|aX+@c!!6GC6_aOgJ0+w^W|Nvx~d67nI!x9Y(rIqp>lb6x!&|Jg=eMpo#G97>M?kEumGI}MyJJOIxPVC5z zy2A!~9>=$mU0FlY^kaf_dedjLIInov82l1?xv!B`?07@6j&FCzI$6H~dyO{EBOaW1 z1vhu+{DB#q;QP5V>)Q^<-ZXsiRJ z56$Z%i$`2}QUH!>7rW&u54vF>m1b8CTP*Ec0H@dK@B79h^4uyIP0M5iVkkhlZ~a~O)gp&%yMU$Sp*FO&ujYzG1>$kT*Oq{ z`k9u9YGyu<_Y6?bi4^@@=gjLEZ_8M4Fjhe@Q@$8|#SG4L1keLL@4vDTngftihRX(y zFfKzDWE|?jJ_us}ioXqx6@dQ|r)qU}x6qXPo9xHsN*DX~@#a_^p8=gdfk_Ka=C?az}(!j1F{5NBTo_B$Zj`}Q@iDDm2 z*3BqJXQ5=0R1u01Hz@Kj%L5f5X;uFnKK~m}|AFhn70$CHQ$tY&3De@SlQ=*JLatzq@fk;Nb>-q1`kJ~e$F6Z4h#oKUJ}%N?zK$T6>POe zdcU0YS{!ql#HXE)>%`M@K2`cD(>68(Te?7{?-F@1VJEPSm((apt#Z@y_@w%@8TWc{c zSBDJ>PS?bL{2sGr=k!OV4xBId*qP!S$FjtOW(}ho4ue;5rQ2+)6zH;d=mv?KPG`(_ z_&jgID*@-HsE@kg7Ee$$cGwADg99oFZedcIoG3)^Lok2@UH1FLxaxkMPRpZ84Rp=# zU*`z&wl5TNel^HXkIv$>AII-?%$Kpv=cDFOu_S62fE_*(dm+C`3JTfFfhL9IlR{7O z56y#kSx7PEg8n~(I#i`mc1TE^=OXzMpgJ`3=Vv9G+*;R=;(4!?myX_^K7_3tOa5T4ZSt zDd@>hoVk&2e^l&D4gDd*kR0<17Dst6H?f{D=#MSuIdNm-&Xa34J8$)Uar&@Rm+YQ* z%%oh*@0krgWFlxb8>G*GQU&r=%@feVW7mhBSH;#ld}y2NP8VPOe6GLto%}&RrlM=x zZLZMUZewss&*~8<6pvSl^EQc*TF0=UwK+uk!SU_!4?5e6EAq{oaN(xElFn?`Q+RJu zb-~gu^KCyNL1zH8Hg{qAHNxaT_wN8-FWSz z)jaDot9f5PbNQUv#8>dHWL1_c;k=2s0;8!yyMgSRc{nv-d}ROfXE~OIrRAJ|Bl(ONUS%*o`0LzN1drL(7xQ4_zj)V z`-PG;!UvpP3L!)70!V)RTf7W3G>iaI?FktTxATAjeus!JC_qlR1Cc_ME=4&wV-O4` zgn*`6l*g+10*zU71^+qQDEruYjxS2%bPZevn!X$E*Y!Bxm({sb%o0HqP z4qlaH9A%-c-)@G}T~kxXcs+5+EI{o#445fM=g^Do%Crp$N~l$!q6sZ8+W&EQvPsAu zoFWa+3cPsA;>N^wRb!84&&SCjU)vaXt;7wknpO`MVsHl1rBYxE*`-lAetL#=Mem*@ z&J!z5B--tFoGz8QX&wGPr}x=}>|6E#;S)DmGM-5Ja;qSY&{DpI6hxAOByds({D-dC zhGLff2`cNH*ZQU(9?DqkOdFyTKjAaP*}Ipg-XpOs{gnKPlCj-8c|>4m&D60>iZpSI0OP46t2pQO8Zze^|87odo`bOkXkLsvext?Z>Cm|ctKDFUV^%* zo5zn8|HsOsZ>XbF@oK=~QV z{iXf)TN1W~9+NLDf5ejDup5B zTr7Nga7~aEzlqim%lfZ#2Ncr>2$RF@8b3o=*i$BC|5@6wUzODNA9n$-OMOu8<~* zeFv0zkh6cy5uw%#@iIeV*ps8y{M1E&0elcq&s+LxuCU5zq4tk*fam zXBBL+l*XB5$40~6CuSyHJHRc&95l)wxmE1=a8*-Jj#coXtic?iew7P~rpXT)f;#fw zC~%!v>EF4P^H!B?Rv7DfrH~^`ku|+HIC!dU9>K-4gSKRedr+W>IZj|o!CUE?EebRp z)dq>K2U{Cjas?(Q8XY-lm~=AbHn0cty$rOp-kg3!F74TK!?(~sZ9|l_BKeim}e^I4mm6gy6Y@rxcAdb^dGEKcH~(1LTyUr6ko%<)QbJJ zlMc3r9=MKIL`@kQFn{B|)C`lAL6$+%F2RVW2tw#(={V@$5q%PTD^f>zpe{RnCGN0w)g67R@9fJf5~HOO~V7VG&rO%yTq=gC=;*^ zJV28nC|6;ig=W=Z?D-dm1f0YGfhH?Sd;bv|{KdEG<7dTd%3}Lf@`t4E7<3xZKUuH$ z*5Y6SHAg=~spxAb-b2;_Gww&fJuCySj~^Q7mkQ=k_dTJkX{?tP z+Ur3}w~cM@ls2=Op6Tf*h((mW$iYi}L<>Q2Lus$CSre=;&UC>Nm!+rNZjUhz2v0u2 zb&7`Th`N4=(?Zgj7Meb}51)ex+2?b@KjczX|NO)pbl&ZV#7cddiHsKU2x;SBn^}BX zml(H-UMIaW-;**|H=oaU;#DtI4mTM+QR9#DFl}BhgAs5^9JQ-3+rDGi+@Z=e=pj|O zSCCkxOWZ+!J@IvgwQa>W`bxOhtCoe1 z9~FBcD1H+zFhkPn!1wKQL`vFRdCv3jmAUo*Tw5By_VJyem!~#$@|G9$oDR;OF561Idj3cLf|8o>KMk1dCEv$s|jI+$}IJOK+@$VGw@? z84d{mpyAZ5#jkI%eiMv;EgXzj>NMZelHqu*@aIde?w2vOQU{VHHtTxdA)N30)bZe? zj!nsZcmErl)`!^SS)*A#fk{&POU=av!LG)KX%gdSjme9$tWRXHWymIVi_`~# zv8-JoG7l(Nqy(^&9=2W8FHI6{`fZ2uyu=Y9rO;ExkrXs}0Z&L`h@kzIqTVFMTlX~qU+xZl|g34?zUQ)ix7xq5ojGuD*KJoNXv$|5k zt59djzUJ8GL(M%KAs;IJt!1ZUZ)vB;mc>51!FOf8Lgz)HvU%<$r{>#6Z1=bt9Rx$` zp3__i$gX2E$b6filaV2qzEVJV#Xcs7(f6hwVr92?bLJOp8mG=Ia?xbtu!^&K%!LJ!D9<^ZezbTUUm{-XY=?;D)4afgjuJePa;*5OyF7tMj$OFMlD3#%p0&TEXia&UL) zvPuRUS%iANHpH)`dTWB;-;kX~2^yIq~}$X`|G(+Mj1EmtN9z1ERSMp1+I2 z?4NI`02zR3NYscxX9w*7W{II}{bdtk*$BT4NWMMkdd%T2gOlPhi>%kSS535rjlyEH zeqB*`Kz}&3!uMtayTT#2R0W&T+j~NGSGS$~-8UfF37A2E;nFgZKt&i`G*m{2P8>XW z4bOLl)#?&pO9 zns+&Z?$$A1*jH^)^6FLsmi4gmEUH`^5=Sdc@Os9XCMicZu(lb`D8;nOIrSWu;I)5v zI@!pOjp}x45(D$@UdhmP(GBU;In7mt({WCtlT{&4vhy3c+cGPC8x0wMZZw~>6WyPl zJ%W2OVEU z>1D=Zy~o8I#vGdSJGdK+zH}Og^F12XJv0-+=WS>-v!%mdu$?ZzQh8)om+`@LTR#3) z6HblEtE%?wyE11ceft`D8v8SSk`vl=s~$9(7Q874e>(0L^`cc7J2eyb?Q6?t%ez`kF=NaJrJY1yjl zSuzup_}E`hogTT@V}Nd3g**aFW5a)0JeBCAvCoSgfyUP^bZItC36Fik*RCHyx z+W$q@S3pJCcG1!e0uD$j9YaW`AR!Gy4>ceRpfn7vqSC@hcd6u14jlssA~1xY(v37q ziJ~axci$KE|Ki@eTniT?^3FW5pL6!v`#c4M^mH5z=`VEi5*{=^UPZ{ETaHJOO*5J? z+zooW6ObmQnHn;2zK6K78U^bnqhNMNqw<@oC8pbLU#%pi)^|95syf6d(IR?-#ZG3f zNlWXDp(WqcrU65(FTJiI%Dj=RW2fnTgO%So!~gQe z!#CmsB6Gcy+kH^=?D@-l4uT%nB!kD4sjQN{qGZ>-Ummk6&94@tcpf!IYV>apK;>Ao z9^7d)jpe+RFy%7PWB-ezSe*lAw!!%SnJjRUz+y+Pqhy(9=1nInQbC{sp(b&P;6N4P zPi^L3=N?c!2UaOo+WfqSN&1ed`sWwIiP3h!Uq0Ah6tb4PG?V|Bq!{D8?4@p!fDz5o zZHDwk^G_??XvU?E{(5IL_@$eLQ1^bKoX9<^RCSRFN}4!Q2L(oz408yeNjNQ>z5czc z-SUvthT%bY#DP|g$k0XfrzMe?*#zsQidhf-i1=T#-4LHj{*jhF)?Y&dY3X_eX5=Ql zUCagmy8c&k`+Mtv+w&4&ZiOk9@sr&c`}-{W&z=NYBNosB-JEWvveY7= z!5**Q8eQeEj&e(|F+WKTz~`TcJRV!lR!#d!mNg5DXZk-QD^XG(zB}t0Nh|d{CN0?K ztKWUS%t|N6F$K$-4&*&^Cf^#8>iHMjb%TR)8Wsh8t1$ue=zxgkugtLj8!bRJ-bvZ* zzld?tE#d!h+5t5YtCK?-6x*KuJMi{^sr+B_5TIBFl=Wx+>~^eD4nY687tjv%>I$SZ zUge>h5ls-Q0uq`xQ`8pXS2+ULUa1ti;;!%buhMfk^H{{!7)8FsnXI{m;7RM2x|fN$ z%wI_TP*_{icG`%l@6GxwoTGeAI_uQkX2oD%Z&7aEFx#*NSH#_@zseV>wGuM&+NVY% z|4n4pyg(EL$Eba5iFwarS@INNIZ1G91)gjugsQ~TG9ARgOB13cWO!Tn90ti+UD+H8 z#ToDI)6BRI^t_XBU89T}2@wW98xVN5T%mgcqjx8wiMd~Ak%0&`uc zQI&j+T#X4W@m4|#)=?Wa#^Wl>aK*om>91p z=l@PCa4vvA4{#kbpB@p$*mZ-_IPeSs&m;g=|HWg2APrP3E%9(YrG8aub#zgXk*@WE z%2=fZN8%+iYK&ASf8Ez;54M2Uwm%H3bcwqboOzq;rEs%)m(a^)`?N1Ul0B%>-BYQI zi{ma(5J&FTE3o*^K!6AqHp z1mR)r#OefwivWd|rq^xp2~Q=l^mu($d99MU4COb2B#|+b64IH!I(k!@P2$54zfXq3 zFwd8X7|C8ALyq8hU8!LF}Sls z4FA2GeOrLkWAC4@H5o~k*&Y&zAS^P@ZM)^-qPpaI|SRtY}tls-n5hr(sa6S{!y;nK0&hJWEcQ5Vv%6yYPL4>m}o|L*4KQD12;^j!8Rr3AH`_8DJp6OcrEI02%vqiugbmG^%Tov}*vuSm_?zSON7!SnxyuEaz#m2t4HUDCe+r z9;u#OWl&F{P(0#y$n`=s83+OST=A<3Dd)uAEKl{Oge4>YdBVcM%yMeRceo0-W+h}P z78~4yFIQeQduN|SGJh;6`ILWuhF56TmD1ED%gR%UNSes6+&fv2UTahAUC%uv*2%Jg z3#Az+gRUEBR=#XFo%<*$cvpJ8v<@rHg|m3qDQUSxN+;|;fg!mEot$8|Y{m$=?TN%7 z*g7~&wr0&K;E@NK6O2_YtpZbDYnL)#UQ#arczYx4U@q~+uo-<0@vXCK6yg5u-oUX2 zlt@sP?A0hKlcS;jz#h~Zjj>6C+v3CA#sOuzbU8L5vSa@*kl0B=+}D-T2z{w}yrSq^ z+M6?H&a$fM!Cv<$JM}QADbX!!pA~Utp*Xz4#5eh=dysa2F-#I(scycGhNm07FDkqR zG%G-6*6JfOCN!sL8!0v}6Dr=%KgYZ}JaJ7@#s;a&LB{#fT1&axH)!@WuKDNeff)!O z#6Bsffx0B1jKcDpkNQ`f>B&@X#pEa3KcRQ01Zy`---t6?g8E(6?&1y{IC><%Xd~2~ z#O!GsjtIW+;#ZAkLM-E;`bNhHtw3uj`+WfsX!vg5kx1v|Ihrh^R_d(5x&slERv4nDX0*O4fLOHuB!2@Uh~cWPfm9-DQzIEz3A&W&q?Jv6hsVQZi*86e9Q zSVPCiusXx{3O454;}mh zGnzVK!i0)>i+VFu?xtts09VC{2JA%1^?&W3lkEY#gWt3s2&;bwy`jL%ITK7~1~I`< zx^;-{IqUXYp=?)3Qp5AEB=Wbg6)$p$*{UwAlFi23(#izuEprwukaDo|~r zes$aarBt`2OA^u1CecJN(QJ}ah8GM|z_`U4*xL*UOX$@=*9SQC5#B8SfxhmB7EiIyqOrP#y@?R7{>_M%-&87_b4~ZhTn@4y%?^c#7XwSju%|{LXmHi9##?cMdE_8&P(pQDXf&(-hio!}BFB<)0LS3aB^L)Szs=Fut@g>F;tZW^r+q-vr? zl?He^#c<>u$wT^6z0aDr2=UiwcdVIvAK-d2d@{qlAH{#?+hldR)yCrgKF^f_hcfN* zj4Su2J+w!lZ7acw@XlHVE$r2_engjjQs7wj1$yMUto^fQogY0ZtpizkkZ|G4E(!c` z7AXCdYxT)@J18`-9Iql5T4SM&4?JPB_3p;vZTx-}vgAZD_;9RnL?)JY*g0NJH2KQp zUj$2xoC6vG#B$N)gf|C<{A6c>+CaeI4IDAp za~Npj6nvJ$GI9Cq{4{mT1mv(Ca!hXfsPU7Fddov;UUk8#uxC|oXHY6DSE~&d)tQd% zzS_MB=lr6U%D80p@r;%c{JEI6s!DUxa+};!YwfB`E^B)sn%okU*oJ>^&O4m$bK3goyQT64AOk4sW7V)>;vfVo_N`)NrNu zv)0X3HF0Yumbx8OS=G{QGSU40QNZdBZlDt>@o%uI^eJ;;VJ4Q6d|BXy+v78Q;+b31 zP&rkIr&b3#UjhQBO@Jyw)(AqdP#80x*cxFE8l_TiB>^EIagX(E zoo7xEtbhLr0Je{TKNlzWu2c_R(^i~F%)dbIoYt=ny>%^AZimL*kn@R@uD3&niw)5n z`>MJdmK0@c{fRi0i?B_Qp@VHF`>H4Wy~fANjdQ&NV)X7=CfY+P$+?~G%p#HWT{|L( zre}SuUobixMR@;6j!|!Gs!!dslNkh~@BC;dG4sroR~Mg-A^Kh=R|EvkRU-%0N6vYG zZSFx}w8*xZAWk;bzUDLpqbs3(*H4wbu=4G z%8zJ~Yr;eaeHfCY3MU}sy}5GTK>zs12TezU;O%v^F5^up=n0rR zdI2(XQI@q+oQst`boc6aJs2)CUBi$7DsY{?80>teB;Bn4|okj zy-BgpXq%<3SeNF^YR$U8pVQgyQK*d-uF?_$X1V<&SU%05K#16xU<&d?H_cX^n|94g za?6Q~ZDwC}dsxK8w2w9!lfHVfs!!A$O-3Wq&QPa$O_ zHUa>Yljc!Caj9y&8%dNY*sgVuPTx`@!K@Wp<0hpI$=PT!d7e-GkFEK8JAl7@!f?Gy zCbEQozBa5*dzLjkD&M4bZkU9PKa*ynO}56ahb{EElPhY_4#Q`X)p=Z1%HaBh6--4W2ARbS97GYT|QRzi@Ohp`87Ay>UIlHlmtt^EB zMqcF`O)3G*POk>UQlp?g<-kdDV!1eZcfa*O{~vzkcP-08<;7^k4GxPu4V_Btd@ALl z7Ya#RSe~cLQ932rGWDf9NzbROq=Vptbb$Tjs!&RcQ8_uo#mdi>T?9FYTC3OoEWp!> z1o-taurPsA+lufBM9+8ue1~^mf>{awIM?Dvw4Rf2c=#hLDz~=KzBNt_=*vA=*H^@< z(eWXQ#NHqC2KWR3U%kUZ@3(YOB6UX&L93&go04BMoRl^$+nU_FcA+h z0VRQ&Pk8&Q9A;a)(cF%eJ^1FiI(|Psw!~yF5^EP)$X^6TU1u*cf7h&Wa*0vCWOLD_ z`X^hI0L`WQVi zmRLror^U5MXHq*QZkc1Z4@70nS*vR`VPEf_p$uAA91*%{=IOb9}@_b`U_=3iCb>$ue0#(^rsjN>y$oz87p%=0UV7G_b}Qa5bG% zIZQD$(nXqe*z07oiEt7}mUq#+)${bSOv`*^s6t2ObJ&PYzUfH|5Yl+qmJ55#Ci1e^ zi)K-K>iY+H#Nq@I+eQc;{w?shuIRS0DH}vvb!RX!-tq zB}RI!I`YJ?Y`Fj40<%efr7Tq0dD z5`%dAPXg2HNtDH(9HS>gvpDI_cCEHP{E^F9lgS~s>+=@@V=_nTQtEP^tclQCaX2#e z8;!iUEAfAUm-lj5KZ@(b?+)HFaEWOKx<|wraT-!rv3IKBU-X;B+4I3rg$)MB#L=(^i=>#PHnLhqTV<1biJ{>%J zbWtrf`a`BlKb4N0yl0u7Cr=I`jq$*WHd%Y=p|=9U{E;DlW1b|JB|)gO-bMv;&L8>; z3sSYwz&MDcNzlRF^Lv195*%%_wrKsPgY<;T zgp$7g1F;0}+|kVTg1X|O>3gCc?k(F3Ouk;0-7^^Icpeffw^c$_!%5jaWSCO?P*H%S zi6ywZux3L-tjcyVCr4?+mHj&9`z#|*zeXX)fU64cJG^cv^mM5=gJ_(@4>zXI3@yko z;?d|mH~Bb^xdITnEMQ)t(Uj-f4zja3#GMT(G(F@n?_TRd#&Y#%-lWAB_nWRky2;ay z*gc~=<9~5kx~SK=o}UY$7q45%QGN6=SMXSWu3N-X`%>QJxi8Q9Hw%~x)ZJ1G6V<55 z^<9HED5&P>A%R3uJR=v)g!lZU%yKn3I~&xavkPS^w+%^3{ViJC6U(XcZa0 z)c$T>F*IuDt)(rzHAUnV(#;rbD0ADi>8oZ?e0wrYiJYp$hSPAj%_g>IBw}=uA@7EE zi}<1R+MTrAeh8w-J)>` zxo7XI;B{2wPWvamK0Rp*5n#4vHBuSgQXAgH9k9NJ5l58btH}1+3r`3)m?Q4aBPd zVVmNLXmhnF?N8}{dNtIr@iB|qFR1i2RP2|o-XSUIf zEBZYhwm?EmhlgLPDhY_DM(R=sA8DDsKggyrVth$tFIRl%MJsi@Ue0Etr#{3hkHAs6 zrM6^*t~%lz4Rn}V5>XZt22aZ0E&ga^=Mx+0O>2RXN<%ux-gBS)_6kmoiAj!ohz^eJ zf#Iay3#lqnH@tJZ+Z)-eF&H2(UhuN9Z=PWC^C^J{4=ZVxm2z8z2;``>2q1X_i`Pdg zc6P6SA9BVltA6zxc)eb*l59MgKEIQP-a$RP0b}fcNkH!5T1o{ZUKs{cpMX&6x#d^F zfw2DI@LKkR@J8Cbjum6R;LN*kFOz*I$5>`_z5}*N$Q>W`I5eJT)maq?1Ln3Eg+a==u7eQ9gI!Wa8 zwJN#cy#Q*{Nc=sgvPsk87pYNHNnb0Kl?ywFEUvZd##krT#Q^gD51*Zwcp(B=jjSe$ z5WyCbYLGMsRcSkGp!no8Wv#dDDn4GTyq++=A6{%K52G^OC!a1_=h~6)YbY~NcJhfJS4UST0ALvUw~Xb4^97g(PzH-- zBu7|kuN^7}z2T$^W>XD)(qcv-I^dn1R*Wr>xjLpE6vr9vQ4!LeI3U9cD?p&E}Q}W=S#2lLt6*6t7 zUK?YvZXS3VX4yW$0VbiHaF0moh!NN}UmF!PC5cQ;8(lsTq zY2y){FGJ+&*{3?rm22)j6yUJWWTCnp7TFm zL%HUSrXi#^BgF(d;$ILwLI2sY|e~=_Rt4&ih7z}t=PLaG~I5>3= zW~i3cm_kC@Up*;c#{s+4_=W$Y{iO#mmpFp8VQwhtmj!+!m*!z=c0XCX7*LoM3i7KC z>P}hdqwj4Yl5^0W*q8e_V|x|{J|Ro=iSAz6(CM2^TFb$H(mgU@Jj0C}8M(oE$IIRf z{b50>fNXSsHC|VIbAf2VaFTqM5v99)O}Kp5#Iwo?cg!Z`K{xe&xO-=1m@9Wxg=4JO z^>KJqTpAPcVpOc0CTm=eWAfIxpj}L4*O&_Avc?ZW_+<@CZ1hr8 z-FLBoTLcms(v$f@g8;9eYvQ*g`_bV!-J2Ey_tZF}Ko!Fx`oZA+oZR}LfWWW2$iZH# zuxG)a_oz;tq58&H_ed?Na)+0D{%h;)Bq&W>y=&l|Gvpm^s> z`qU})BHCZ3KMS&EV8RKT{#_T|*5>Orw~PyRN*=m9i!Xlhcy#v=#kiEyO(Od7YSGU0 z=hTJS_}hBA=RehcwbpHs99|uDsk+j{7p7D6{C<9LC5)#tq&wfEiTB5szITN|G_4vl zw#ts;P~F)JUX$13Iv;H1nPJDwJQ)|mB%(f!2r5)*mKrE-`t?6#sW9|zIxhSUm24`G zOaf5{l=Xws`n#Yn`_h!B0*m{(DQMeA_#xI@cik75S^3m!+XRwr8xwR?eid&86ExIn34V5ypz{iiHdd6nmp1UC-K8bv zaz#?K#Zm3GZ|o%b*GTZO@*9sN5!)uJxm6{(#WD~xPgwL_Y??qX#fGew*caDa88@gffTf5t}1XZiHSU8N0!6uRc(82fDNVFOma{ zOnJyE#^r(YBO#x=@7cZP$A@yhFB5*!nKk|O#-qh9vU9pW;Y&W)*&s*-P}$6gBpwdI zyd#EmS`9#mJ=%>jvdk|z-g%psz|DODg&B~!FMNUV3tp&(6OgQf19u9~e}k_i7|jBy zEy@FFDfVOri)ATIfOfifUY_ojckZWmaG!cOV=O(NH~0fCJ&&CrY*CckNE7;tfXTgh z+?`DX_vntw4dK5C;#O1IwNNDRe8}!Cj?J%a$DC?~KtBYIaZ+X%C6@)x6KCI_oc)Pq zZy0cP{ErSkp$NdzC*Xa42_(#?tjw9v$Ah6qL*30A%yQ1-%J77MOXN1ZV};$HmZ|B1 z$SoYNzv9PV4NV9X(TH9zlkxo-s+#VK1jIXSXar#;*rUF9)P}$=PhYDS6XE4zdVF^=!M2P0op!Oh9Ty5R~v{oJ1Wj@D3Kd z1ob{i#S`#K#Faa5^i|Z_+El&j!|3Yqau# zy3L){yAn z@Y}VZHk-ISIS8?%nhG6)Z}&Q8Z7EByjK5ctujEVJJ9g4G%?l!i47Xgpi$x*{Li*>- z5xTw)MPN>9D0l^IsjkSsK0c~;o_&Sqv#xB1z(g&4mGEF$wvD^TzKf&4iAAa$jQs)G zoGp~yd6~Ve+PS?mKE6%-`ugC`5lgjSCMni47e*^{=Pa56am&06rDv_fb4jA!?kP`s zA)bzp#thdS{Jmb&?!dUw5I#@cI{Y1_C8t;O9hvnN<)c~OU+a?C>e8-VDt<2NLgLNu zfHYc_asU0bHOick>#|@CAJ7+&L40x!%{%Bnp1+zVUqsv6M`_I{e5gWvAU$Yc0*jScmQZ=!4Ll3{&?Oc217ae$Cutfh-5xre;g# zxAbQSyL1veo{Wm=!PmC`L}@m0{1r z(uc6#^Eq^0u@l1;k!j~`;)NNv;t2A3JyOCe2#C3cY;#VtXj`z&7O((-CI|cUznr&g z6@beM43}NnQ)t5|ccXnKr2?J*BFN32$7bTorV@TV$at|mv}Zi$c>BE$a!TPmm+yGu z#|vp+Jl#k5CX8E8y;&0_vS zU}6o|oTVw1%b9HcLz1E_cv5`-YI;Ajl$&szNzTsAyW2v97pRZSS5P9{9&mW8D~UNh z!2*$nv#%3V*9R;eWQvF2Jt%@b$%;<^^6xS-NPd9g&C>b52y)W(EU%H?KOb(j{nf>= zGP>MOzz@McVjy#U)$z=7n&|AgoIqa#+cP@Mt%LAja!0E@^IjhETROCm1*5U6X3cUT z(M=MADgtkHuQo=1%cV$@Hws)9wVQ>_&`;N>av6EtDnJU?smtjy(5f-@K&ULpaQ_po z0E6MR27m;Z-UEls38Dk9oiu+81tcNh2?v_H?a^2#geCXnzM=78pyGohIOjDtzkKzY zIZrj}-TVCO80aV~69OhIqK<*cMQ)LB^lVP4ninno1+3x4=F!QqRi zECC>N5uWruZWVn#AmSIGPK;<9XkJ(wdJGi4pI9!DL0hxEOuf@vm<`ms@qQoh;%78X z(My7IR_BYiIM!iz6m2hx_<&;wq#;SiRaTXDcSc7N{87>xY~y>J744v^A*J%H*e=ahQ#KX^m&R zn>QV|lTyW8sXRdwDUqr!Pu8&s(*q>E_sns4^IOypHRDHY`U@7#o7|ow?g1O&q}kG` z_2$$}2JeUlnr=C}c@7^%syg4?le9Bwo}xB*JEG8Px`IMdAtg^rS|uPM11yEn1>g%< zQ(s)yFA`hR>GXd_?U~W7W>l3mC-rt6wVB^WF4lV)QK!|P5pUv1XFCm-c;ADc6bz%n z3CzyBdkcTWi}aKFWcFnA8a^rdH2b6R0zeHZDhaI2g!Ki?AY*~^#R^$Lt3NTtkzE7m zQr0&I@a?)E=-qP*+k2TLZ6{io#eRHnd55dM+DpZ;fL^Ovq_M={>DUu2{c2&eWmQoT z6hFnkO!yil&1qZPW#79u+xKE*cF^%KO5dK@0;KBT~BoxPpm8QhOOmP-{~#HTlH%b4Jt+448n_J zl>&Lgs3y$)<%cBgK3A?HOymQtFym@(-8B=6`dIrBq}j7q`4^E_4)Q92)&L*R!?^B0 z&tF1w0$mt{&2cf;G?Jin`T{xnax4O!W$b#2LIl^|&^ZX|RrGe7xw90H=C!yI`p|r~ zkfK!E@E65BmS-DE;_A&Otw<5E_?5*YIf`q-+RKbHzQSF z>LV5!v?r3q+&|9Q@OO@p&R~eVv#(gjCVL`nu%p8MdgWaNO^KNQc`N_GD+m@qtdoZ< zZ;(G?OcLRPbV=A_Z_#6J9Z9|IGl@`Jc07cSm|XZB^!Gprq`){qP5jzDOc+ z$k{>s#)18pUIX12lMeA-2v9iuBLf-(^#1%Or}`-{0!D&A-+{GV;Is!{Ke<8X1S5fJ zKJ%|d{d;|`xJ@HP=RTH?!tkPz)efvO8-h-VHi6RK(2PxxNt_4>w*2^e9spOK0E)44 zFLRhe74-RCqt1rP`@REEm}MJPLZA3u+1ctk5%5ZI3m+t-n3SJP53)xSdeh5d%TaEZ zPr)F{3IkcHVWC3kBtdO4$Ck52SYQ-=m5?BeVHIbG-VpL(R3_nIait?4<7~`uGxaf@*6i zq{FDj(rC3ewzS%3)7gbT9(Fa^Kulk*&u)elRDa-h%f%XvoWDPV(u^E;MwKax@hBUO z`Y0Fh)m+jL23SmQ>_m%b)a^R{+f5iba$asH2^sU*(k-Yo1v*$VGiIF_2vikLk{r;W z3rHxCmD;tB^P!(0Ov{}TXASwGX89Hd3ukQTMwEkkXp5!RDES{2f03PVAxZyyVPSx@ zjwCVta;5zUlIDr6v|0BN3r4MhZ@Y_mv;#Q(L47m`(I?kTR-&nFDu(&)iVlXivdhHj z8@}LP+%id2Akv1Bwm@+bRNm74Y1kHvF#*LmYD6JM(o3k*55JMI>hq_4?hlM;GbvmuVexBlpF?wGXzN1muliV1=iC_|gAZbw$z_e2`2N0{B4 zcF9R~d05DPb^!wEnRe$-&bW`SE8}TwW7w(5zUf{R&ImbR!HEf@_=%;rjKIFA=Wn ziXqA)b9P@Uc>UP%3+p{et=S%gIz!4e5E=vV2r>|6evi#5L3rxFuL8CscvgU6!`OD> zc&L}C3F!t`UNYRJ@!CCygk@pNLa9HwT4>W;O{DU2u< z^8rsi`K3GGXwOHHiUVJO*fVc(^~`9^2A`+^I2{dSm3IT#@KdO zaB45)Y$zpka8wQqQ|ry5M(R5v#0szrOr(=F!W8U@Je2wASiK>XEQe$9dJNK(jQ~Pr z&g)$)BLsy`u-hKEVLc7Kbz2ExIhhXbhLqGd3DHfqsSQt*tj`6LXX_-cxrQJ={T?-QQi*s$rR@Zl-J zz;ef%C3WR~3#EQL!N6(uTC2F#fr8Te`(;WLcu8&-OG>g9(6(1z!Rw0M+?b!KnPrXX zBaRSZNqy?Y9dD-@!~C>{%m*`D>Md(k;B(wfBMyXQBBfNx|y(Xm^3tVxm3~tExN=m5(IYFL~6%;t@UDLel8fj zAkfrilcMxVv9`c{Vf_)k)u5a#ST8xTgq0bGag9@S=1f;QBAo|{Mto|p4Ki(e_GEodS5r@FsKb1>6NE8 zje~^KUG{AIrT8FtC-!B{>sibK0rJWGy6HXwo^?nSvOvq5HqcJIYQLLIi_8w^&WSU~6c1>J$i&|f zZ|3zXIAWgSX9aeD637A{b4TgnqBoa(Bd0hD1+yvOyYCuP}uO@}*1 z%*P@6lWE6=lp%t?JoUGC-0s7(+)U5QN2BjSWPBT27=`09%RT&Qv1|7@fDM1ry5}_8 zbq0D3{^_6xmO85ZU)Em~-Ne#IB=OUL0B_z&yrO@_ZXtaAb8;+G^Q{+9_6981adr_W%7Ll=Z9NgS zzelj37q#yDLNAH3_J3Z*`2mO!e}`xQ_Xc)pI#t~K!NA}DW{)sw!a0=FiiJnxrg-B* z3)&U1m>b>tK0)z_v0&Cz4j4@{NfDd5z{aC;F$PRJuR!?wUN_8CR2!IYEdMqZk?2c;oMju>&hKabmeYE#tgu?iI=i@Co za*>p}IZ9JwUcViRgB%z0da{`n18X3hvaT93QOZ;l%5&y+XBxT0mYaXpryYsTW8>hD zTw4IF(!xM57rY%{W@<)IE%{a2!j7$RXe&rl*Kv_8xg<`JlG;1I_`x%-RCQLMF4iM3 zp} zc8zc_MDzwzh#Py?=Fc8K%-x$KaeRNi@qe)ZVnJ}SC z`Pvs?B$2CKZ)$|RQf}ANpj}H}qkc|2%5rv%%8U$pAUU=hH`mRCFhf}r&W zGOjVv5#i>VxzDCj4fIGxosf3s4#55NI4a~&u?~X9oAyQp-kITH0MLj^r7prvx-irJ zYfM>F)~zePrle|4Gn-iyGjdTGIeYjwA6?p^&jR>jw}frelD2rIL#QZ2yqJV+4i6GOnGc;$R3`54aqQ4!on^s_$lLSlE`c)4~3i(ZvM~{Te0l&5NBCPdV3e$(jNufbYj+=sCCA7zdL3Ok#8not{?QJ z+qv(#^r|b|Dn5WEmDYpIQ}8_FyZVi@>^!iqbVlVNV1#cC_#9TM1A$gZB-YNh&GGS1 zhkh;2u03ckIIe^y>SAw(Ueognx7ZN=;GJ51jVc~jRQyR%5@H*j9^iP!{Ma|0kfg%U z;}XHI5iW_e^Hv0tz-0w>vS-`vu{#IOJ+EI&tBh=R%D$zQN7^mWGhc~l*>`U*dHF=V zlBxT0bLk;>T24hr^79gjM8lXL6O8l<^FSE>tVi?(;T`x1lmUeB7W% z2wxN~R1{EG;=xr>-DaB)ky4`5sq98uHBYQI&ofcMm?$2F*~p^VSx&0iv%p`cX!keo zc`9CG`Mt4$WqE4ZFDuq&bM`P>(z!6t#c|R=Wh)!uY48*yb-aptQ^Co&eXx;%PVaJ% z3_ocgA=qU-uIHAddAFCzi@-?9skv;y-0QyJ`Q|;p`Z9yym7WI;QcDH2%zCbV{AxOg znU^yAtKwX7I8H|uv3`U6dVWt8mk9{o`xMaOEmZXVd7k%ym1KQk3ZvC^JWP{cS zUZz(Fu=;|uvnP|92OOWBvLm(UU(PObF|g!Kt`F4;3f_OrD%)QU5uaVl4%$XzRc9+b zy9-3^yv$Z4O_mKQh4xi<O`1^#&)(IS&2qhR`YrBT|7qy*0C70 z{8U*XxS(Yw1f6IUVES1^*@hTerB{)Ocv~QAh?zVq6smaoS9(6F&}pDrM*LTe82~2` zcy26ogV+mP(}MbE6R|P8MJZ1ATLGZuk|rcqtsooD(qX^SNYZXM#MZ2s!o^|Z!mpba zGGr;yVG;jR0O*yfYwgPFS3dob@?6RYD^;**Hhr_#efbk$*Pk)zj+!`MvBmPM#KQ^c z-g3PMz8m0N9A%{(k%jAS)~B2Fdk^}2P9-J0wuYLLkwiPqozStQ z+ch<@$z|wSgP`$~ZzD(zh1MXfdn!*l<-9~5XM@bV!Gqlh65M4*)|&~sEo#%-za9kb z77AAE=13aob2dLqshl%ryZt1%LeILJ;9-ZWzuYFWpG%mHtb2pKKqt&PtNK-q z2>gNjxy70B+c|+FhIKo8ey*Kp&lyR?IEXe$&^?XlYRh^W70ZzV^S7&8fU;qZzGQzw zdVi8-%mHE&>UeETW=9SKT`?Bx_=^Bx=}#nokg7`ug&M2J@F@`1_r&BM7VX;3F!QUu z?EE$nmP4ceX!`3U;i&iVMzk3fOB9!^a@5rU!J&vH=`q=}j;n}Ydq=Fv`7Ake!xI}4 z%8d$(5*3TrO3||&DZYLkM8I;21$cT3mCXuVLez{PrC;3;Ay=>O)c#_b!uNs&=j^kk zLv)Pc?iAd5yQ7Y_Ru$i^`w>D}lBrx-&%f+seS;So+U9JD32vGU z_>a|JXzpD-ZfMJ2ly~OfL1)b0x|x+&W;^VCiz8~6bhoc||D$iCpul%UA)|p8)N;hi z%{tT{0=JE6^t_^tcxXttF>iQpvxf%z-6U{dN60*}XL{V}#(%b7A$6vEI#=6R#fwy_i)$H{mn2VAR{-I4;?6;L zKuXQO#=38Y$_(CiW?g$XfF@G566|4%7s6gNX~T(T`?1)oJTw1Q;#jk#@vT_=)7zRk zey8t)iI(?F*8t9gCNpnd`gxTqx2D5i1iOv~tP}eihx>2qOBS}fl>2UU_HN2@ zW4es(i>L6{1gn`ILh&|m&$krvCfR;X)%1_yIqo4 zoNgd;r71z{g8F@a)CqKg(IJR>V56zi==l#73jo%s8|l)pAvlHhs^no2u9JDhIk-@{!?)_3yb^{&>qV9EyzWQs~<{nl| z2x9q8(fx7`Krrad|B zPe>0?81aYY_+J!YQB%a&Z?UoFR+m%P@1n_$QfYLgZO^4bJUwS8?w>OmUymP&4mAQVG+ybJ3H;`WvOf2?BhgJh5}kFO8z&O{{oL4hK`cB1(5&u1^}H zKv~;*HvN@N@6%D@9h3uc-Agm3ONinxq+D%#c8E}pdF=BpTD3YNl4ov8u{1KRIel_NPfMYOg9KWjLi)6Z^AVct`BQI${c+PhW^b_UDx^r@@mJkXQzz z?`wS}O}n*vIQcn6n66!VY?$zP-T~kNy_*k|eSl|x_6_g8L7lJ$#^M=kb|?%Pan~i3MIW!XH;TE>9itDK(v&7~$UPe>Yvw1NaXfmi%Z; zYj=~(NIbjqEBT}8yq9=h0XdA3+9Y|Gyn^ZHqtDi&@cqK`+`V|qonMWzRJu&hIW||! zz2FaEvkj$MWzSkuF3cTdzLZ-?3gb`dUmf6o8$f^#4*%ePH6lIkv>yS`8j!KlJS`q0 z#mw?zkL(;zZry+g-6K&2{k-sCMLEKOBP7tc*7Zqi+EOA(Wb6Y!!J;pG#6x~sGV-Zb zGl|u_>1N-qYZCiq%@)g=09#UMsa_<1C5!I$7l6$;WlD~HftD<@)ejxo40 zYQ-#a*MZ4LUXp&@d4Wf(As#!Yk~xY?kl0<8{g_OR8&H*qd%kc|5PximG3mM2#Wnt* zmvc%~MWEtsfKxJm`xAgREKd)7>zNDL-M7(28T+oLh&@B`$+tTC>yO@?`0+!6XPS{T z?A99d>W?GLGb(r3YZUbk+~VlQ6aNK6tYEYcr;`lf}n z-NgYhRYlrVzDDdD);;-u$a?FzsMhxjloF(olJ0I0>F(|vLRz{TB&3J#l!l>8Qu5HL zbR&(D0s<0(cMo{Z_jm98%a5qzp1t4wtS8o5kJ-Ty_mVHZ%q!R>K_UadCm!m1sNgZd zVmPQTt!Je}(rBo3-5P?DMlKB=Mwf92Zc#E2ATxySy+F; zm{NTygM!cz96QtTQ*AbXKP#5Ymm?G5S#6NCEb2N$o&3{rNoF?$G3ERQFnL41rE;)w z22dG1DfXa2)1^nDimR3lkEBCzCsMjZ#B838dp2^p@31j`D4r=mgwG6C;hy?Uhz1?^ zF3JAYmW%A0E73;LF+;f;IV5+;SPOX(JKZ$fGtv*GVeJB(>+v`8NmK)&iqjc(AmM>) z)63HL6igiq46?_s$d`uu%U{mPj(}D_HKmt3i~0~ZDy`)dY#%^w*kj%Y4g2os^Zi)( zJDUN*HBc%1kG*Xeq3{D-!uw#V=p#~#l4JB*c>QQ~l+wxlTHV3Bep> zot7yZimcu7DgoVQCP4E9pf3=xp~VW?|3~2qAc@iXW2XiP>%e3R+MfW1^?%h9&}}0v zV68nBTiqdhiYXK6dg8KWMnd8;{>I?CeJ(hRL(PEB273tzYXi#%5D9KNHi-=J0(ZrL#^aBN)`Ry4q^6_uZu}Ufpvk_>OBHG1wD`}U!!N_2x z;0p#P9Bd8rMtmP&1{^2U?S{IfnWL@-5}ok~%AViz1keYMa!HR2aL;h&G(gq-Kod_B zPy7J?p-{_%(2-x=W(*ZS>BC13$HTaZ1$(rW{O>N3Z-?M)kX$PNLA_<04MfXBRUd{e zZqc1r!ts6n53GvChr6p0Dek>t@q)Gfa#P{vd=V-#=|gqZYs17Ptj%GDnkiA&8Hsfp z<8JZ&#Kfi^K6{_i2>*~I1EX--R5syLCZJ^Lzo0h_v0 z>&V)FU~1t&2$;DHsJ=uwlnxsFom?c|J4y~2sgAk-FhKiW3^VmHRZJH8f`89JT}Afi z@~b#9Y=Ea1lBWrOWLbox^pW=G0ml9L84QWOC9Ps2J^E`vT0&E4i!MHZL6@Ojkz)gR zu~}cPG&3-#yEW5N3e42pYZu%}ax}<%h-1l-NQ5vsJ&h?rd==^y%UC{T<5e2YZoSjt zE9RMw&)6jdkAoBJn*zLx(`dgbnh$*bg%h_TMAPCLB84@kQtp${2_$ZJ|`rj z#_p-U57xNX7*^$CF>qyWvDa!+(JEyIs1(EQ12)#UFmbJg<#?9f4u-r(9~wHIeAUwA;z+cd z40{qF_UN_4B~?H(*O_|&Fbf{&#S@iL0pOeO43X2v*3|sX&vhP-Je;zz=rFY6-`(Su zVTE+*t?=b{4>@SFmaVX@^y>GnVwC7C$ADViml4@fby2XNCg=F#82M9hzjU{cEwdlARA$}1Z-Ck*^Mg%rr<{RW@n_^oD3%~6w;9Nt9wA;wABGa|kM(9;0}K4u8#FwL64`VT<15-72<$`3ZRCbbSE0E&57w&ZQWq_1c&9{<%Iri?nABFK5 z+$Y*^S<~y9?a2rB`9djV%Gzm~!hX;Io8WKx?8XH2iL9%VH^pPmL3rk;R`TO$QNRo^`$}Oe6pT>m>|;j# zug%T;3BCGU&CG05usZU}EM4OWr>qT8*xEnLZZ9zeae|5|e~(qEmKHWLYn!)wo0W7D zjp`52(kG;uHh9A0p@71Ui2V1MwJ?3y9ecY0-w4^NWkY>c(xY?2x8@QRJTQT15`_NJ zubI_{-!-k0lPJ=D63MyZ%*d?hwAqZ(at?-f%irA&)_$txfI$6k*8eY!1VVF&RSEd$ zPpW)m=iUcbK)Dxvf4_q_6#1b2S*Q#L&;kGF{Z}EmhpLhcTerC9m}^ApSxI_=Z7k;g z-EYs54`tWpe1+mmvR_=c3POu2dN|X9Jp-%O?=y+hcFiGp+KM&1g+3H`4{AKtxGuc? zVAkZxUjxXw@y_b4y#9j|WHpYt?l!{Xye80__;>;Tv!elsl6=6B|GzZ{(2Fx5s1)0( zaOZuja8+A=t&QF_>V^^APFHp_N<%@I2BN=UqBaSbTfdh14f4^PRHfwuk z+r(k5SqnK!9BHAZ422+Irhwwf$x{s-M0Y?p)=(8@gcZlDHFLI6o4E0kR4?p>;ZoKZ zDC3u_D=#!<=OWlPIpTRY4V!;pUcauNf!uDkXE4F!NR(Fxc%wDHjVHuuTwA-{~1hCGe%rC<8Vgj3mNB9Evq>I|6hA8UG4hx7I zz9bMDhY=`$9*L9W8_82WTHytgLaTDakbf>mpZSl_M;;1+5gfm=2zn=yOmxo`R^d}- z(+-a6b~q4KtJ;Dg)0(PeFsqhX_;(~Ft)}VL0*!e$obbDIE_}jXEqydDpsn{zyYqLjgcz>tT|)C!g+n{O2Hc?=6PK}Cm47TdlByvQ_!Zw zfqDyYD3b)t(z1fgiW@Nv^_rzI4+dCW=pjldyKbZe9t4S|yh`D{M~1Y1zW{bkbmM>s zz`0w=euVl~aO~X{N=k@M-Ih*mq0J2(*??WY4zOY*k*!`lC8I?wA zcnnws1J(uY7%II_lRQNaJ92#=pr<9nIOX!u9AFySfa~FFC9^36KG`7fR=g=+cTsgX zcMsCd-;pPnVW?tb9!wG8r(t_>4sUzWqsWFgov0>OBEH@Sqm$V~LzXdJvK-fG3P~j0 zDaV;4WAUPhN6QFi)f}d7(bDehRVkEPWT2wDtbJmz@vorWPoA=@LjdsnLv=9 z@~I<+U56uRE~v6S3l@r`cwgiQ`7siv;vwiUXU_yFL&I$SN2^42G+jnf9p_R~B||}i zUTxB}BSpoQhFHr)+kljeSi(^IL$^(=$}?Ft^a!QL3IfV6-+tmFKG~A|cNJQ%kH{J= zypF;7+;(v+lbm=270~w9zcUCZ7SsTJ>3_GdL5)!Xwhk1$wb(ZFix^fyl0#b+k%uBR zG20@0hMe?dvz3rk31 zZczMjOUffmJiQE0;QRlK3};RnsR9oqob%m@k_DW_V;38!Y)uwOPojqXgU zIJ`3kPgIYh`NaHuIm{*&ieM09QlpHJP-^SD8{)TGUx#W~(i*(Y?d1#$Gf>1lqyfS@ zo_144@5atsVzdG_>^zyxFd2YIGfZq0%m!ovE#Rs|_&9pNUZ+r^SJDTmu-|ihadgd5 z?zC*7RByI}$i!m9-$hZopeNeH)D=f)>AouB zf71ylc}e_$r_h21{rm`^g4b=KdB0C3%;NQsa&9w-989#6#Dt)*EohbqZly)L;PF+8 z%{TRi{p0)!!gU;myIo&Cpp&c;0*5L+z~H;Bpg=YH^Wa1@<&w~F)3lxBT9HN`pdE2w ze4bT|WFx?RG%llI&}a=(Dp3I_*rP%QoY|FbIEkCt&lT2Wl>BMMw6gM|`z@IJaRmruszLi;V#ju~BoMlG!H*SOgGdUUY_m$tZROMYHwU<{jpCiU zVeuK$Ut(+~*|fahqnNAoy4mZd;e)D;#0gV*pWf?IJmL$!O8f=)Jo$UA?_UY)`_c>1un{2{89t&RB%ui^}SZ@$S<8+qpW*J?MTDnF9`PQmY@!G+4G$p_GDv1*pBWA zE0jxzX3_wCWqyhpS(@-S#qSmgo4=(G9M9lRyX_w97mCE}fmQo5GS6HbNhU;$Nu!z3 z)eYwpRi=;U{<>EvUV2kyQIY&eH_wdL+UzGNo9=h@mwJuSo0-k3vcmUb`o||5n?k{a z*^Ra^mg2VheyVKH_tSn!&18&kOtha8M>{+Tu>a+%nN_1{FP125?Fm|NjsJt?!6HT; zhs7FOoYv(%^MgHErJ3ai1+5`w=uGXvI?KjVEM#L=%Tj3#+R+-&QN$h%=Y>rTdoC=J z4cSK|HN4(K8k_CS;*{tLfPsW$Yt&|K@!8MMRx8REmo~3xn>vHL~);5T(&k)u^s;4iTZ+G?bz?u2TgrAuAcH33WsC zEf>XrVPcO$UuBAQV~8)=hq`)L=U+gdVhMs*b{vM}{;VtY`y1g?GpRD%YSDCBKkgxN zv}&?p@_@C5<}C#9Zf~|ty=2e|9zYar?yxaLpurnm$CFjqHlX{Z%2Fe&MMll>jmQ%# z4@)Yix~U4d%K)WGqWHF?1s?-(DK$<7!`ed(#_oJ<42&l-TsHjCW!(+Dl5uTTV-7}@ z^k+I)z-um7((X+yWg@2E9{@q(7?dvBD;|#9twiK5-8jA!T4sFKe?qf*E!z85_p$@7 zRaa=tLdJQIOqe|}pS!!eA57DxEU=X}QeM!L8de?_#X8F|psU-A5LRvcp|0Ge#Pd6+ z#I%reWt&FmwxZoJyAn3$F1>%~rs+qL?+r92JsjWN06!**OQYcA$GfTZEfUC;73iV~ zQPBeSsBr+$xz6@T#x2mmxaU$9_t_858vP-eWN!EAIG7Cu`8t+Unr{*_4Pg;g9Cy7` zK3Zv`TD*q4RP5Ztu3pe=!1Y5M+=#VVG2Q{EV7~pZJzJ;y;ThULIebPn03{tSl~)ir zq;oDnX-PZBa?MLi{77a1Qs$b}#S6lujbw*GX~xw+rDqvSpUz$jBQjXmvexUOf7t{# z%aV20%Uh#d4q(`lyM(bhg!%n}X$eMm09}Z#fU93jE(~F7p(;0i5X0x-`AQg_POi&~ zsJzctW7hV2WiS=`Qa}|)UuHF_E(rl|k@z%x);J+U`H3K&1G&O!%9U?X<_g7=4&8Bx z4qAzC>`~?qYlU3;5Jp6YoU+zzo4DOoZsZt1Ix$da=C@r!|_y{P2#Wz&AnW z0usL-RP4pH^yXgwAL96{hF3$606`yanJCFv#$TNPae#_o^#$Rer?bmQY=) zsGq|*O@Y;hM`@d8y+O-AKhz)$ofL)G|B|DK5#q- zOad_!C&e62uWmGER3>wm)D*dssBR(0>E@@M0dq2j^p*o@q(De5mBdZsRd6@R#J3R^ z;gTJ#>G9Z2psw`7`lL4Ho?YqZ<6immKuVIvwY;3c7ZK^I=#DShFCW9Csn~~DWhT-h zX7qr^M}&^pVd#c~Fa?cLM16m@HtzbT!_E?I+1P^9W}?M1c1S=3Cb!XRs)!|9=I)uU zXCg{1;*@|0xFk@6J1(N01MBqUzXUbuVFBgC< zi6QMH`)Owv7soVK?-gC5&0e&;QH>h={6Dtg<=gCicPz1?C`%FOFUt(Z>0&KO3E+Rl zRK>2`RG2`QCp=xGCsUEv^IV>oUxa*Pp>;XlVVr~}oSd3h!H{MLDtiHb*|65t7Z(fI zk*Pz03tb4!p4YNUXlWr$Fg3W`gQdW_3H;V1M8LwyT7|*cDcUI)VR@?&uKkKn15|e4 zYu2Jr*QPn*`MO3d8t~q4R<#kWCUCs}X+$Z#Dz>OYi0|Se1$QKP}@rFiOayTj%K{4BST`&0GLIEA0AnVDk?+SZ{dLbhQQg<7L1fCdmMo0 z*wbJS?bj1#p?_M!zze-!;$dz*Uxx$Hu&cCI9O%MHN}0Xi5F|a5ls)ZDrz~Y6k+oOb z^6W`H)vh|dg>=^|j|Y;OJQ!k86kCCh#gcp`4dC`Bs_>4tkk`Vt`tZ6?2+A|`W6)7UjJ zQ~xia)w4Sh$m%c+le=M+*&etMTNA~~?J%|#T%I*oMrOP11r#E>Y%+j9FN@MR{R}4e z-rsMtxLnbtXAftiLr--KHuw0H;FpR^kH#yy6E3h|Maz+RU$sHPP) z6-}xVh4*xwXDan%U&&kLa6D)_!f_FMCM)Z5KMXmbsaUcS^iE}?PR4noya-PCjN~{E z4@Gj{Shs#Uf|>9At9PY>^`b9z@ARpetqH=G?ejUyj_+MrLp*!6rhH!j!VrZNA!66S zy`!Y(2H#V0l`8@cOdADfNMOyoP^Il5Zr%wn#pkx}9gV2=V??77@rx-#JGIQdpK#2S&D0jzL5g42b><14SK;udtDV z1UIxszkcQu>4AsShE6v-0Ba&BSOUR#x#Tvj^DV@|g#yEuVJ-gdMR=6NOO>BSB}ac? zG)cBvsgsH;wCWx#YL`n@L)!2o=Px|wsKu(xv*$@ghyJ#SqKQPE|o=5Z*uXXqMNiyEYUk@lw1AU*vz4K6d1)eKP%KB>u$ zQ3ux#3g)L8_@#V`dK)=MqGjf(Ec9byIqDA#)==qsRnqboq+21EdSi|7_2j_HWRk37 z{9oN$tXw~3cw~rIEDHNLcf0S(=T)zx%`|nevJk@`nqsc-f9kL0lN-J*(*>3oyFQ%V zyo-?sttAGIcGm0fB+(`D(K@<{Rm&}Pc;Ds3mIpVhL{h6o*TKM^_biPB09uy6#RXqw zy`omlVmNKQ&-*V?m%0Tj_KPOzvicPfS``BH{k$t&iosYFSkocSuITyfRGq%)b+k5S zs?GA~V9kL0Q`+HG-M};i)@=#91)6~d7~4BtI8ecNFC<(ZHCP2*e8n4k*??{35DRM=U2NKvVhc-8CS3?LeEy7aeY$0ILx){za)%GV)JBNe#PiY3Y=#sEGS&sz4>Lf zr^3kRv*}l28n2RjlO!4XG6t>M%eWPrq#yoAFiq2FyQ!`_c_l7>o42WSD|)@!@!)kO z;E211vNiG&V&v?|I=PDjZ4?oyz}DP=zX?8mG>~EWz+^J6x{vfsnI`C&PczDFu0nm} ziy7ZBAtEXV*#$Ez+jZgc1r^~|UoJEgNn|Uj8ghq4boyP6Fim1AF}11b)*wuOrm=UC zD8?{1IaCv`k&)GbdlE>X;{9Y#%~QKP=}+SBK+2EY^{)HTqM0t?@v-Jh zDL>15@78yMx9TFs^JK2vo#IMfQ-dAB6z52rd))!Z;8h-zJS?pAdC?`7^8y#ViJ0$j z2A|KWrC}w{m;?LFS(8JPED15U(lIY_**c=|1}L(&-Zo$k8zk}lf#D_k14A45_Jx2c zD{HRn#)IXp==2ONSX9Ybj4z0*!LWd6k)2Vz8PWaV#V<23NH?+fJuBeVk8vuhKEuh? zSD6<`n=EY^djUDj7_cY?I38Ozkr?NsNIipxgM*zRTI z5b)=iuRL$IZD4I8>&j}`uj*;#WNp$#wG!lf9n@0*gmfHW!yesE;CtppSVPt9nSaJ! zBh3SpQ^1C|QlO}S$wVPZQRYnX4#DqivDO24AK45FeWhbK*rm@f1|c-_i#qva_n1?` z3D&x0hp~m}50iIsjOw1tsZc3M;XEq%eTY+FH*IePhG7=zuaQueS|;>9jG~j?@Tn-! zhY4PC8HBJT8Pz^uI4(lSY_5FDq&-qj9N!1KM(wcV8D0L)R)qa(q1tb+k`3xav%7_x zxr?C)xIF-5$hbSRDyMM)^@m%wo4g2C`@>6uT2Fo%$sf0$_WH7Gc60}QIEBRzo8hn# z53d$6`~B-cpz3FS;00Vh2DAL z?N+Wgu~^W0kq%YPvGWspbV67+;j>X`j-sMv(>BgieBFhgA7-4-K0kM*E`7C^z5j{Q zR9kGQL~ix>ADFkVZ(h^1^Vx*T`cZzIOcUOrcTo}kh&8&JNTiI7CoTo!-@uX%xN|;n z(H|$i8uPTs#zlLA2J{!OHZ_=XMR-m(#eoeYrv>azJe?g;WaW0WRhneT)L3JhQ--y_ zEH*bz463L76x#!t)+g{QujuIH*q*bDi`v3JfF8b(HziR=+V{s*5Ir>ChRv1CEUosv zb)c_2Jgp=x9}2p@<~=rz%wM$4EKRM}KS;by)2OOE@*;nIrR{{%Qtqsrn1xW+e;ccvx(6Kp`yEau-52$;75 zB<@FFD4-ql_s7tB2cWr@GNm0{^-5z1)Syb-Qw3^>Ak5atGyFz@&YD}d%0)ATnw)AX zr2R(nHRNN65Jf+Z=8eso2y#+Qi;j)8RVxn$dug&}|3(Y6ef25tir;-$utACK0=|}0 zph^P2IX$&8v1L$nZ1+y6JDtf4?rpI(hfU5J>IhAEElqe;K#o40q{H2Yq9h9G6WLX- z@|_D%rEIuU!Pz&?F zT{@u1ftHYfRRr{Yv(0VnsE70+H7#jId+6$Afy`Dbn+s&6B44njnmTHao%__0~l~w zpw|F73N*h+XwbVUHw1!2LbIp`QVFGI=2U8gF5hV?=k^LJ;@~&OjAFhxg9Y4lHo$z# z31cz;Q+5DB#pWc&H+->bbK~q8TT~{k4hu8Li?xy$ev|=uOY+ z*7G%3mjXt`l-AdsfZ^4A=V^hnFX`=8f=!bN6XG=?XnMU-e5DkT^~0|p;G#yqb@^+Y z)B4}8Idz>YXDZDW)EqX8(AE-gtf#SlH9s_HcH0Ar8^Er`{L4#xfNYAmrXKr}?Xr}K zsOFh^XUpw4D@?-RV2Z^e*p!$`VsLf3ntd$F(tA1dT<8}|hfgODX#iIDTRKbAfuWFa zop&0_qu>RqU7y;-V7wE%-_CT(I5Yib1FKQ%xUDF^&(zy#@ZzU#(5j7<^6Y3Rl zWR_^Fhd;ox+3gZ}PUc~Ei34W=`d)FAQ4bN9nBSIUhDBpGc>R7QNOC-HHPrXXFuos4 zR`VgSlHGZ5EJ4j_p?fnp+v)a9A=?l}*^6R^1`d}j>oMrL0ZINq!OO`~h&H zTpI0z1d>DGBr-GA9ButMrkA5v&&!Zj9e$2P=2y!cuXJyQC*Z#e(f-;g-doM&Fl;|p zKpw!ftAQ~XN?!aU41`CO=e5eg`kGeVu1>g{L;_=!Q>f-L(K~p1g+(X>&Vi%3!w=XU zCf@&!P-?TEBvPqog|n$ue^M?yf3H-1llz34jncO5v4;Q}(3v0_E+KYy?w`_kB(SQ7 zWcv`bh#Nh4>}MPDv(8!#^nHTGLP@{1!-^Sq zuY#gH4R`RsLpln!SDC9Jqr?{>%%d?RB8;7bkc3%}GTCL^ zycTnHJ+r?114C}+^LF=YcXZXIW_5ouka5Dl2~lNKz2CSF>XO_rf z28JP9WMzipAaDqvu@f2~E9n#ZV=3^qkRh>%+-a{1eYS|V%KdPbI#MrUQZ^~7MZM@1 z{=g(HnTC*67@M{@VC>wypF&H-+jb)YCl%c<=k3yQ^44pxC&i9@`uU-vKk=P`N_w>0 zyB%jt+Kr+k_rqA{GM-$!Wfa>htHq=wS|WbI%GxE$cHn1L0jl-(;rTyjhCXHBITdW-oO#YvC1Ns^O`0-TULlbalmG^(5 z{g9QcOFT5E?2)WF>^IcfPNeISkn2u(uV};OLdRUb>tMavY|rkkEFMsc^&q zj=0`HW(QnXm6LJ9Mb2nD<6$=1t9q3p?YP(yXEJItTQ`bUXQm@to*$dR%?nwYsl39@ zOi{Q_7AET59ad$1Kip#_BzQ9 zWIv*P9VRcvl_uR9e^!IN3Oqj_H8AY`As62Y!JI3END+a7t2oZAXkz=`PNJJgYKYB%kMVW6ulR_q}B z=0l}uotzoMW0phmSNoKEU1{=5&@9^;y%I1mDouA$-%matH%vU(+f0`lV!+S_M3cbN zX#(fJ7Bp6z@QvvvDIc6bU89x2FU}cb>)Lm=jjdO>wB1k)`VmaICi3pIVqSQVMNzr# z#;iIBrj!xA^=spR;-`zddPe5pEe=|p>Fuec?vIH%_B`h4$f|&$&>k}Gk$C6I;~9Wb z041cEd`%HEhV{%A4$kCcH?vu+`YK0=nA~YB!0;wP0WTf^War;*@z+rJDp{*sd9hh+ zV1d*6BUlG~zK%jv9bk^jZwKVsy4&^NIcOUsvTC+*+vGJ+Vr)n<@zZ_j<06jH<4i0g zkUjYz{V&=S-GFBeX~`lA*@KoH4K;HqwcV%IwU(u#O62~`sC+6HPhKYRL^azZ{L(1Qdb~vr>P$HaTLfSf%U15 z6`{~<*`7h^7o4F;Zli{5$CeS7_^xNNxEr11e9Vh77t^1>JSyDjX>J=A=Kyb7#1OgY zs(Im3<$DY#DisN8EeSZiIbMesQRtb9FzrIr`It=&hfbgYxVcA5&#=s}OV5h`gLi^x zdoCH;Zs@lCe6=#!D>)7zo&$BYw97#;=bGotWK|>+x|=B4kGBSZXG+rT5wr0Jo6RmO z=hl}BwmvZ8iywx5W01)cx5s)>+rGHiXq^5ClC$5#HVrzRcS})8X=dq9;VPoep*fY@R0K)Rtu}Qtz}6TIV$I!XPRR?8bcuu2G1f zCy&$h_{bO=Y^D4vApTCBcb@8xB$>M3hnU zO!2mt=6EYatAuPhRL;=L(Iq4spCLt+Q5R4Nqn%@iNRtjL-svsn)D)kp+59H%UWtb6xt3{ zG!&hmPr~%1?LX3FpV}3Ae}@uLOg~qsy`0%PJ~fxKI1jIPru;@B-!^8vYO6gu8!Ku4 zPG2ahGSd{#CRNw|L1X-=2dvQa@MbBE^LHB#B|8mFrjNOF>$I`8cAw4B>W1W1QSFM% z9~in;JpK1jb@Ef7s@JZjs@a)AXG%<6zpEGDa-Ocy zV{SircZ#P4*gF9+7+^L1Ykm(z)&q4O!=+4&qCi&5g2{7RtlP%AFez#v;+=(uHH}&u!ld4T$#WRiQ4jBrIxb z^Yxi3h01PPkDoRT^_!I`%gh{da{Ve@vf>B&tdm=4;whC4lOf01>g@$o;OqE^&7g*7 z+OrQ|6r2pBO%1k?T$=GwE3|Eut)FpPOxili5fs#|Xn8HSmN*|8YCY>m@w3q9Zg-e& zH`Pg%6MeZI9DXoa9~<(HpAgkCrd5%?>4Q}a$7!iuF+;nzVk9$KH1CW5yyKvh0zj^M zyH$<$e=CMZKcr84ghTknD5!HO2p^a0hwAW$sMKq$n2gc6D44|$+!;^d+1dGhOd=od)K3vnsIoqhel zX|X3{jltOBTCmcG!dfxu6pZ1RB8ZVu*{16lg4UV&gj9rf~VX# zR%5r{0<>CO#lH*)E|I73(>^v@bj`{gYu`@1Q{x55A%JJ{_FmY@kmMOR!1r2SP3AubVhQS5*yWmze_Lw#qc^@uM zX*br&@6Sxc5GSG;I1mMAt-Y3;6v&9!M=JVLZX5DoAK#bIhEV4XU`lQ%Ehy&j=x75h zF3?Ks02R{U(OM zf`?YT`qr|W2fkun3D55WB-%)ZvtfsA=9k8{Yba{pr= z`s#~DUCM|>)-%P^yavrHMe~v1wqI1NYXd$dakUa-7fZCRLF@j2k%$o_m*I+fHvaFXH1?# zH^+zcKLh4f5720xk<&^F_B~%FO$Zl%wD~0$?>OI+s;L?ufpeD`I6Cg#_6p{Fx9?#j z!H46`kXik~MwB?Eq*@E4rQ{$medyS7dObNd-n^ZH1*gS0FHXF3Jl|o#cf+Eob-J;d z(`?ioUG%D5qotRZI@5K4{MG*fl zGnPf|!DW4Z;lvmp+*~Mj_)5I9V#EKy*!eGwahn}%zSm@IDh*eQ?UW_ftiQ49rmMIS zWU!I)X&`n$krmpx7C+A58dl7pK%$vnQ30>|Gk14Mth~^=0%%rG0$30M5X^P))IN2F zPrXx&&|4UYBoJ!^HPo3r|1N8fWbythp64^bfUX8%S=*`5mndZ!+}$U@z^HO~=P`!X zEUm8$Gt3-0IM)Ex;BNy9iGZD{P;<9`fz1%gkLUY~a^NZ-ZGjJG+9IxUJlC>Ohe_IF zFvQK@)`_(hG^uR?TO+760N_ml-u;iT1a-W9`mCo!8d^sD-4)~w7_~jNg`lh*D66R* zPrbbbV6e*cn`l9lvJZY{WT>mmjWBs*JOlPq8$yQy8z7h7idRNM7$-gNfNde5&*Hch zzs7CIrcm;5X1FNduPSBL@zW+BOs}eOQgwz~#E9{^&SHmY4yTuX<$p^T-PBu}yU3c# zUr4`Mdu3M4-D$0|4UJtrn=F#~X0hrm;n%}@Z{ejg7C+O)a2)XPSL9f?cerk$e4|(d z=q{W2a@kefngQAj3573!4NtX(Csf^IQn}9r~Gc=_kO(@(i|}lkSKax_9kbQ0 z26QXEO-k%)9qJX#OIOuy9cG%%cVpYU0jFyBQu8*)$H(i~T7jz3d|5;TRS0Uz=;_IQ zXI2{bm{UB!?E7VwU{HH`MxWQ!#Zw*2ODUX+tR)^S0*t4=HM z6en7Mb22O&_*7=2Ry``Vl&f$!KSiz}!oMe>@aUhq@u%@!&A5$v2L^uX_yNFuVNqDX=*ySl-&vX3-%mI{ZKep$398 zRCA3zi6^)z=4G5wQYiZd{SS;WQ(Y_4mZ0~$MZNb0Lc3j*3wNR|G*tby@833iBfGUY5->Rl}spc3BM!GtE(3_+`+IHl)B?Xe))m*SxWbm{w~=U9s3y96GZJ zFejI`@~}MkT~2W+lH1)&*W*Alb_$4sq_>-9AdpM}W6Pc1O@nE!*?GsLGJOX3kmqcX zlN#b{bW%}2B=XCbNr9L^?C?)$rxq zHQ&7%@R}+tI;F3_U>>KL({Z;uZ9#yQ$YS+TRogOul ziufQa-^UjGvqxxNZ&5PMd()MjTMJ-&T z0b8$LUS*BlZhT8XeH)?LUMe*dZ6)C`lDXe<_OKuFYQ^ z%)+Ce;R&K?Be_%zz3lDA@$6Fw$~~~FT`NpOI_Yz-+?wiC{w6AR)<2vA180d|>;hAf zuSu0^4@6=p1M6QKyQ(B{qL)q$g^(Ri1-Nx}W#cakGqmIcl*U1m@pxw=8K)l>>pwUR zpLXTBy}n2?HuANXY+EGjZjbS2PTF3j%an=WNE<~m>XV8%9ahbP6QxRn&NiUN=TNdK zbW98Y6VSQ#2l_An1EnfxcLp#HbmB7#|JGw#%u*b`ut`Rstxl!A|9-jy4FwtlfK?yR zG;IpHhu|+F0UaHvKiz-T%Tv+wR0aX0jz`q`x631CR!;U2o+g?}=fj!S^5+dz?C!_` zZJ;AmjgQ*49}1@?_nf%e>bWOUvY%zq3pSyq7sy7RyJ?zU90`A9;$!SfD@$p9YlDnt zw{6$~Ln1Gi7Pl1&*0eQk#t_9*4Z3wMu7XWba{x%~$8Ri)UIfS6Tz5hV|T ziXTA)KHdzAg$U68D*c+Bg*dJ>)wy~vqOg>J*6Zo`1hBz_X_Ep?375L}6mmONEf%{BtHGoWEQrx`VIv{6$`ylxE~N2VjV{&K`emgpM&rQHn`+VMF>L*>dVS0>{^tYpMD2sJ z0$rdzgqCVj*=6FG7Lry)72w?#>W__&d;6)7(HBGF!zmqW$Q!4he_*ofxSSFMF%~Nh zTwV)vFxyDja$2Dpc5s?(VLFv&lr+UMxsf*aq+M^N z=AU3j+33>Z(Vz7c(>oX`Gx$Th$@j~2@;>}5>JXa9v%eQ}EJX+38=$3kgDATO7J{>f>829tBR1ih@ zdQvm9NvUG9CpttH?6=(9ZS15TGzlJYHkF*U#e^GI&IGh!vs<}vJEZ8Zp+@$_HBPv1 zOE2~Mi@*7o|L{URp0-lXLs64BNo3?lX6rY{b{_8MwoO7jzXSJHhg}@%&lp73`=~|& znNGT1|CS%Xfbsg;ioIc@;k%U@+#<@tBB`{gI`iAPM1HqSv=10YkM=2)4C`j@C&9)t zsxht}1=d+4tUX`+tu-JtKEW@ zE7S!%qev}$hy`0w2@>Vs4i<@N#HU+_f0~6Ev&9@{6u51gLAgaP_s-R9a4{$j#^1h0 z(~4;0Iex(WVj4@qV4&Y;`KGdBh@8s0$V`=vH8cg*pzyh;2@gcI$Q~`KVG`GL6*kzw zQ7G6UOqnATdn&;NmwW#ls0FYi>}EG$9i^LVgGWv&MkMw&7hUdP?vCf1Mqmat)Wc#l zm}@>a3X)$sXPlv!V~e*GSl?#7>7~)&_k!3@TMqTwd^H>E#{t&F*#T2spb=Ff!}xu$ z=@94T0Kqk=@n)|NLvio$qj`-y_OK*+*IU58?UkG}tfS-s2lutVKL@P5U=0F{$|dQ5sxx)v^z!~Pl;ALVq}?|VI4wn@B;SA$q;*tcBkYhfouKOtM&ibw z(xi%Y)iw+M>B!9Pp>4CMDb&Ex zqAV!iZ@UgRJ8(Wj&*>D=#Az3~On{lg72HdzH2cB!>pAfU8!R&7nJywNa^~#Xj~W=; z&3MxP6k~kMm8~OSRX^*g2gKxlCQwDj-vI`++=7jNt?|O8S_%vcA3N{HkoW==A_crq9I^A;G4$)Knx{FdAL0N zJ%D8WBteIgv;XhJXc1sZEUFS3-^)A`v;HN79Q`Qlv{4@*i>!Wc`6@51jY7(|?0MyGSiF1(9#Kd}yWw?47QzF#tn^R24mz{C=bVhpVfOi>lqa zG}4W9gS2$3gw)V6bVy34got!VcY}0ycL)rPO3A>0NGV+cBIS1meBXQT_wx^&VP-gU z&UyBF_TFo)JuU36HNyaH7p6Rg(5rOs$hUAl3^pL-wIqt6a0?ky>-Vu{v1z}2U5d>Zd`%l;~_7-wtJ*ll7UO8enizCkW z=ouiJRnBiiINIJqVeB>z&vaNzc1;VB+Sn*@p_)*Hx4ETAZTs~QBXpo$tiEYxaA96e z9Z(}M!YDBQt#&XswATcjNV;K`Lws0ccH$1daCgU`-W8x6;zhaf&9;`wJDBDbZ!76i z^he5$0@hBy_ad6zxxanRMk@M*G2dV(>K^^d@5|Iow5mqexb;7-!4J}YB7Md2N>m7k zen%Cmr4+BN+uMUCRfCdwLnZ}|d<{4j7tE32`w*xNFzS3{iF#Dm@sI%hQ01qw$dsq| zrWsL6i#Q2j|2t1&mtM`gZ#O*cXYs=8@~%H(`krzxrGJouHtiFif-6#aC5`y$HjIuE zOUCT-m{n2GnM=0J_{rq*%NPp`DwHXy!$+5Y5E@gF=IQiqBE<|Jr*=UIV#Q;n0bfBn z8e3#xQw=$Vs3m~rHkAg>{E~u_ki!9*G2tj%$>v8OAuiwC!2k+CFV9^tXewM|CHZia z#(C`yk|vqPmPn{A;w$O^t1A*zYq0b!%6Wtzufg<&er6oKB#dO+u&d>6R4_ zb=E97R$5fi;3f@A^~a7aEZu(Dp9ovESf_05fqbg=8WskPhtY;#U-K#@T+HT%Uw;&c z*C8)IM_cx%5{OdgOC{0LVJuNV>CDJSuRUJDta^q~+Y49#kD!~2MS2{Q5w>U}`@znR zY7}_6Wv-qz!;bp$+Oytmh&vmsf3ki)!!qST;6;rGGk*8m8a^Fu&t-F+IKv88rv5vC zmGE7fCbeJR%onMj6H7!}MqF~@%W-nIba56L8)74wrkeXQOa4Ytd<%|afpu3M(HMH@ z1jFt(g||-qP3=-Ltr|C~5?_{#xF?SiE+M=nRN6s_*9u()O9`M{swOEk#kaFHlWj~h zrGD)WGKyT~FPUf;u6@kdg-yvVT@)Fj+G>Uh8I1L#dh^hzF< z>o->wA$HsO>9+tb1v`8L0r|>Z0qSb!`>8W3HE{{+rp+MTVI=AAREo7yGsk_2R7HVu z;-1Jzz=c3y3ChxKflA7jO?uW}3oxsAU8?X&vob%pTtntGAwkA1ZmQc+C*!UxSM7ksyDNx*c%9uGd(X%Cm*o^x%_U#0KLAtvd)?T0<7!bq4h`#wXC=>pe+f#6i@Mkg5Zt%?el5e} z$*^-On_ePkP?-!KzWrrAr(SHMySq?brtw}iYW9e-+_Ugmv-5&PwS^0}i$L8u>wurJ zae3QcEC)5e-gt%1^}haeZZ}s8xdIOf+ipz?v|A=FvSbI-%Wi1Tx|KpeR&L*im`UYi z6H}I}yj&%S%DjuH^k}|)Lu=I*iYcy1b4Y!6$ROX4CP=4i^#{QsBum@b_=uz9E(8L3 zqugJYbsz)0BY51?B0AatLtj@qClxdbFyk|>c-@s23Y~7Gs}Va1-excutL}r{SJZ!k z*IV81SOMbUb*CE%xsK51?WR+{(c8ioSB{6M=^`1(CWgZlLTB ztSQK`>qr#@#`Tzk9+i*f%m?bg$&xi?vc82=#W#GvYrR%mY@Mlh`|Emz)FcSVw+{)` zN%BFoKH4wD`J&R@96PAMF2uwo!#!*cd%(hY;Hp)B*mI$!g4m6&tX@soAI~u0 zpoU@D(ijoXGwE9>yEq61lao3f{!RJe=k1;YUXA^ii2_gNfsNQbx&e*H-AMe_FSkvW zxWHHov?vpWW0ZhB|FD-oJuEd`NYD=;y2u)>ra&H-S*OL}z_Zwaba2u|cieZi_%=NbvrveAthGC`%lt(O0{rjygHF0w`k`uAYUxhw5I{Ek%5`N>bbgne;Yd|mky{jn=I%o?L!fSN~(x%Ls) zywB;4x1LMz*5|dE-`nps= z`wsj+rt5`H2lrBV#|-dK5@3{68u5%oDr1Psx9e#nZjoTZbXi}wVh{v;eELFGkA+;p4Xa)T7>W(I-C&!mX1kg_1dJgj2R_ABsLr4+ z#QmP(+wNru8$n}ONUj-S_q!hL-%Jdjl5jC6;qpWWXhSSOd zJzxt;vQwi&Pf_H8-D2VJGS`dvI7{p%d4FK6lsDUs436qPilt(BTa?4v;gT7Q1t?6Oajti)@59L zdrv-6ws1QgF6^RbGIje|M(%64g#es|;40wa$>zhZuid8S?4ma!YZw0@IDE5gkkh`B zcN4=a#4*5kUw)=A)E{1~rkriwmyF$b;^(Mi?j>*p_{^4QzLI4!F??+2h3?=bDFh@) z8@ZGoPj4@{ag~o>J6C z3%6*NckV>^sYpFhIOa1CxqRx}f~wmW_uk$^#3$UUWU>{*3dl~m@1qo(l1CJQEA6vI zjnhp)%9QDPvQCIC5?^Ir{9s!{c`;^LB9t^S`9k%kUM4sG%Fts#xw@oV28>h+bp(uI zX_$J}oHYXuq>9--@24Vsv0N~>*`M`BJ`80lC)e*mD?ebx1FhOU|6t2J0(&8^$%?Bb zQNXRtGp1Nv5`+JY;EbY~x)F<|?7gbr6IQC3C}XrsFGUI1cdL{A()ps8B1s$X^KstccNP3piE^}7N!$_(fo~Z1&#Iq& zJP#YQdSm$Em|X$0qwb3D`S5<+nVW%76<`X;HMvPYx8_m&~UC*2VV+JtgRqwedt^m|hx|^?sYZV^I?pkV8;J&px zcHAAR!fQnUeUAzr>vS^u4_v0pBb06eTO<0bAJ}(hW|IQt*Aij7^}u^*`Jb2wSET(x z_%BJ5Ik3}(hf!eseDr_HmxYZM4n5| zp;``L&UpvOAk*#i0skYEvbK(NSJ?>ELY&sg;toF6f@H@QZ9_*Jx=asb%o?=3m{4CH zR)_aUO~0u$cfzX0&wxNcsPNmo=I{vU5a|T<`k^%GAR>-2jb^D^#m&Q(-180t*-D~bNu4g{(PTtrJ~09|ojKn*-tVd3Ghpt~ z=&uC=FgpaEY5#lb0p!1fC0m9$uD7D1cn$8t#8q0gpD=}jG}c$Rx%qB}0F!ubMAhZ+ zSoV6)D`Q*|r!KjhI`)qqi@85$m`0U(t5wumX8@Iw$4x3R^&bQs_C+4!o5CkShH|TWJ1b0a0~sn++?w4<2@;LuXsyzn%V77U8y_KVYazoO9O@! z>^$H=;u%%Q`XG6~5p#;%Y9GgpV39_tHBOW%;HL##PMft#E5Mu#PnrkNnhg;3Gzul+ zFF#wHd@t&X4o;;)ML{oZXS$#*=sj*RoGb?itdIHtHV3&as2+$0^Y5P6h<~$0nzsXY zoB>1(O(i_R?~4`Kk^jU|VcFi!yFcGT5BeN1mwl}bed8EYk~E2Q8T|R#%X7cip_vam zv%()q=UP){etrZeRRL4W$SRDhpi${jjvDJHnf zVuPq_gENUVK%adZjIf64jLI29?R!l#`jK%TaYTk%gE#9Adk0!um{?}54j$~zuhb~h zg^op9K6}Xhlxz{fKJ6hfyLQZ$Pqw}>`-9K1AX!<3l^+Q$azf3P9`WqBN?!qLBdCg} zLttAonoO9w4B?4TA7)CQK;Bp{M$Ijah$vIka+?yp4hVyh?n|j(I@4pzV8d=Kxmnq3 zEc%7=V?^lpBu){mPDoNTW7uy6=kIsRQYV&`({Q50tLQCwLDiwoatkhMav8FmCGnJ! z6L;`8aX=C47JT%1 zw6H*qn;|~;u3*pZP4w`47g153nUS9=j;EGq$WoePQY*f|TJ2~_cL#FDs*+@aQ5wboq-sc9?zar?4u{#A}OWt6Cnp?(~# zoD4+q55n8Z+rgMC+R1Zwb}AOZYbC{@{PKb7?Z&DX#k0s+GIWLITAEco=)-JCv15cW z?BJ8h33r*6sIn=D-vklQ)w2x<+aH9mQCgjGg=$9WCW@Qu#C{5y?GAD6m|=+2IY&TF z{HUNY+6)-1)U=@<6pcf!a4Sz)BdJg_rM@Un3=~MB?p^Ke+U}~1&70agF4I+SoLFxx zu^?E8J6&%9Ojf@MRku`nAyh_8bo>f%UT7$zR7YGPP7!po9W*nu&uCpN9l%d)SA`Nj zM3RI#Xu@dLeZe(n+mS6~9yQR4Q@5B;Je?=2T2-mV$A<@ zwt$0y*JUg0lk0e{B!qWRVT+z)Iaz4 ze&kGmVBTVS#kWD7^pYhbM2-0pj<@re z80M^l96L;>=cx!`b6GFNq4BYj<@-~{7FN4X?b8xghfox+(zxGO<|+0{wUr>6?k!)w z_ThK@As0%lv6A2T4sd_hGUTKSckE%n@6b7jjzSPXdZ(@n25unp`SX43C0i-j;bbzi z*%zx&VulmOI0LH~syXx*CBeR=6AlZgJ~(l!Jt%YB8%U9f^5Ii}u=Rz`5-QZ;ZCyx8 zBwO9?noBY|;iS3>y2iyf8M_`z+!a4IyJCYT=&ws&&?v3jhVM}b;ny4V-?8Z@@&YR| zv!uP+)+7J`LMG!XGs3y{-dj_q|Gj+Wo{d(!kde2=hx@B(tGO7q5;;=DwGW8%jt4xly^3FVzgIdXok6@Bg?jkieW&?dPY(n86@ zWwLHMQ%$7T*3H4+rQJ*ri=g1gnsweSb-mz9w?|RbF3iLk+aZa|kSI@?;M0{s|7Qqxw zmVbPyt~n@ZX^kDF5m|37FZ1ilYN+)4C_P{q8m3b@=qnv<+v>DH@=Ddlwl!`!paE;J zKf6U_TI!YCd#U(?9&|*~^l?w!QWLDQVBA7;e#1lo;}TI<6jg2L0z8n^aden9otV)$ zO=XhH&OQ(QTG*`Wc-me&Z7f%dco|G1-HA?>@vhW|Hk?1ImJ&r8(@6>?Ch)f(x#0^e z6EXb4_LWxOcqsz}E>OH$cI5}>Y`e8dryXN7WNk9y73WLp%(TH(P>z=M5MHs zZ0>4i$5d%J$*xQtUfI=7#W4hr^bTSeQl{T+_-b!ni74w$VL;tA$tX^rKB?pFbP*uN z9v*5cW(#OE-4P^XqYK`jp#VL5J_}RJv3vnxT4=on1Wff9aIPiRXM(v|zRchifg+uc zKt(YWN~8+U(I~)_SypY)gExRdkxP~ZEA;hHd*KbpT#=3!;00wZiFyy@&zc`i=4;TH z+ZyClNbrqPm_5i~(OKkmGcnf;%MtheN$3+VD(W&r)UNVspLqwxj$D}P-IDGK&&%x5 zPhkYaJJbY1e3_qL?WYgI613BcKtYkLJk0k{RlxgC?d6_%{)NK^$6h{IxA~2DzP3|% zNA7~zt>!9POc8~Jk5jHtS)5~8Sh!}>4YW;4O+(?rFm1o`@vnC)FF6wqw1*{63KnfS z3hkJk)(s--b}+v89+8tPy?M}dDWb!SSrCBmbT4eD42|BP8mQa>`HJ~}Z@T{)I@jN& zP!Ju7icXKG$T;kMKmlTu4D2>oVZVY}arv#;lf5;enhhu@{j+_)e}@R5-~qUhB9$_5 zYyqlz|IV%f0B-$`5(kfw@Mr+|EK>r1rN21uLkqgE0zI%~8_)WXer16aL3}MMy2V<%fT`Lw+?-=YjH~4ISsBB9*&mb$eAKB7$Q>9K>>)L{* zGJBIBs6pD8UZhJ&aTp(82cPee`h0bkXNl@dg6uM~`8<65Fr3F?j~V+U&PYlMTUWc0 z2P9caG$Z7_IY=$!3%K1>R3M@prV50bXjeF(WSZd%{p8=Pr>xY z3~2haSZT#LF#ua$*8b2jS+?~A&Y=1H(vw_lbs1l_wxL;c&lSHW2%Uhv8kM>st6D!h z!qX?^kp@(*k<6$Ra4kl zQw&HOejh`OMyQCcOn=KQvW1)?!4zhL^Zr1A)1H5za$|#diKgk6j2ypGX7&_P?;+}d z-U3*v1H-bi5*T~qrf%AqgvNxay=vDZsYe2b(x*F0iZp|f4D4`L040+)Q-?xd$r&_h z##K2zicp)mtp=dhsa&3}_yP}gF2k0;SkZxSXAgLJ>k5om7J4##TjQ(7K?!X~lh4=t z@8<2wX4k-|f${~w9yXGLo@heOCZA0uWzI)={i6InjB+Wqba5F{Zn^f9eP88vZ~G73&yQn50U7d8PDezyM&O8w+^z3$J9~B3xTJbwHuProx@}n`rPhMkf=t$^z);Cyw^RuQvVU@T`I4^~wrT;;T)r0n{kUuVyjE-1Aa zg4_B1C#fpWR^!^LP*_BJWB`XGUeAIVj%39rcJ;74A8+3ygo*qGP`5VHQ|+?R84dBg zW>ek;TaBt#l?&9=c@)w6nyL&OHiwPwF&Kt|KXlz# zDHzS70&atvG?idjKEtD1vWnO}*Ns~kU<~I1VZuN1 zDIl`|$MBsRe*~piQQv+UfYE@u&B0qd1~qq?3)}?&{(1ikB6sR2GN3Ny4Zw*1zoM$U zwfY|$I{15Jz57XD)td*103+XcBj`^5r zU*pTlq1x#UMh%`05E<4ddx!GmfS?c#dOGK?tTxX3`&4 zSi5SP1Buk;;r=QSZiRZumyVqA=tJh*DW1WhT^P8efv@d1z9xWJdk&IPZtJSj;-_ z2XZJpk%YebgYc}7#Mt0;cReU^8P5+$s)<{O;)}gXcluH@D2r{C)wMr`18VTxYx_iO zT||?t4b6vT$a|-dBgLDdWQFC1d9n#ic+o>E;q+5a_S9XSFzxT1 z3G-*)<;V|Jg=|drT$KopFEahApklkml&?sZoUh%O<4w?6NB|$|HgTRx4>EN_;w$s+ z3LX+emwskGaa81T0R-QV1RaUVi?7p^d)pJIzpPrOGVwj53BWwL==rD?5@=N$ZOpqR z0rTK#o#Q@m*xP$)(JzX~Cl)+xjlS=A-A*U=Xfi{dC*%3MzDmp>;cEZgs+#h6p|To; z?cUx`S9}7a-gKwiH_3ycScz(?lDp>scfk|xK9tE>u;`J4T)sR1BXe6Wsxi7PsG5&6 z=kCi*kwEmmR-2o-jj>X?W?Y=;d9>G08=02$=?)C@*$y{9XS!yhxVk>!9B_LU0~q}( zfyNCk;9V+65lb#En?BPH4oB~&2c(0UGEa7Swq?|43pw#$ScdUEB#u9&dGZHgVq+(L zv0$9i0S~fj+~uNSi4|2&?9{a96H{NzCGL(`NP?bU!)TSdotEYtQ`D1`yFBu?maoSaVLG;@H4U&D?Wy4kQaq!B(2G|_<7(_;2ZXfIl$1sNt(pN8+j zAC6&b5F`DhSBJvvO$F(OP|3gmjA3+^8c%D6IFv=hK+EFt(lVW7Fbq8n7<`HGc_@_NNi28jJp>vEC%@Trp$jt6$_*vulNBi?$8Nn4IA}+27$)oZwq=#jep} zthR2c^Lf70-`-eJxexJQUz;b(l?;WuuV+=ZDvlb_MrJv1wAU>2F{g73eGgt^ny*H|y1ycVT! zIQljW9Sz9zC7(M6l&$!MQ_AgHKBHMy;gGr9jUt^KCPB)V+Sc^BA)DbS$<#?&)i_Fs zV^UaQK28fA4D1p4Rqe)v;jYk@mcO-_xzfi(ym=+*DOx-Z4AEJ7EfOS=X)PWfo_Qh? zZjdby((_hTp4oz!3_P`cGVz)>k8ykGOF|uVm=*%c^(8IKG;e`><^L1Sryz!d6*QDv zFB8^cf+gglB{;&vT2*)*?LXEQ@6`8A@!Nh+7-uCR=zJS1|J!YAosh1dohyq5!K;oa>o z|IJ;fz{{?{p@`g-1L6EVz<*bAC`EZf>@g&<*l@e^rm;0(Xd6s|w8ukO{q3%nQ+3xB z`in9D;?Y0$L2!}-d_LN}?y3tA3B$b#T!AV)@ONM&rMfisHuf>nWKUsM!Q@=>obm!C zKPQqG5l!7Q)MtLR=?fZ<8V}`j{#bKAnMhwp97xq-5`2xEDnPv?BtVw0`BjE8OzZP~ zx}mL~Q}wmUEX=NiwR4LS^EVQd`i_4Mbmjkz$Fnu;$-6p`fE1rvBYF)n@TckrwHhtV z8c-6tpIm8lEJ#heClmV;yDtqZhu_qHpgXLaxb#2*E&awLNN{MDrF2}O(??t2_R`1jAVHB|F&!TU8a<4t^@CQ6&{H}_&NvL@uMitbMLD=kFMgBv zOYTc^16nn$sxmMnYUA_%_^fSVDZ^=?HmE$c^W)p-=Tgzg@AsP$_(cOmCV&#}4v>IO z-@iHJKWA2jOEzs!W9ahg#L{$^`I=7y*p9 zc_96w1)anNt;>^f7)b`4U$%SeKaW{kh7OEjdiHZY&`~Y$1>4Vo(HK$7N*cM4^TE&# zBeKX$&9)NQx|XC1v%%uaAj$Q-5d}Smz=ARJco5Lq^)ZeLs}# zkjCJ&q&cy?_oVBb53xgsCFWK>!p9RL6ddK4a$fr>t>joGOZ$bh#L|0eRG#sC&TrkMdTe*b(tiU3{%PHOku}erUz4!?{j0CX;m2Jcm?Lc!FEi{%*+ut`tx9 z+uEW_ec=v9F=5OC*1~J!Aje-`lj$kpS|U^0a(sAEdDL~JQWNa*w5j;`>OYM_%49|_ z6K?%?Ekhhps^5gM6j~Sr;5_@Zx6!6yN6e+(e9h6Klt|%fAjqLI$y3knk1Fa6m_Y1@ z8mZCg#yr?<77{7?Vrzp5#`+zi6Tu0}Eb8l$OVpX$=gPC}TOIi018+E8hTU(B}R7$fGrrY$I8YMUO9 z7r5m$NIarh-8hO;DPbLp_gU5jBq5zfzRLLrL9JV9B&1wr2uN6`K1#6Ud>1`N(UD2A z049==Kr7#skWof_DpdAvm%X0|P~?Tlhvl-H67n;>>_q zZsq0Hh?XG8b$j;WVl6pN?W85Q6!i1MgR_hXZdpTj<95qYb0Jl2-h8Ac>YEcz<8d=k zV$tWg_iT17Oqw#SCOO9(jw2<2AbWQPKd7W-H5{TjFpQIjw*FlY{RyfNS#n?pgRC1u zbfWEahVi`6VvoWf1Wti-UCxi7!7)Fg&)I`>AU;abOdAF21+1@=ue@0{&Z!% z7qj5GS^YhOS@@z!K^Fg2U&ZYGuo<}N^nDc9{?Qh@jGb7gt)Z!y*N<8O`f6i4dPD8V zH5LaW1_KPYPy3dG99i6yvRghIVuNxD>{ZbkQ=#N~e&d!_6c(fYTkm9lZL{t)5}$s} zLZfZ`bYr^U;N4aBGc^J7fSF zy1finqP~3$18gWO`k#|DQ@^e!#M!{IL0A4Owz=;Ktw#bx^TxYm z4PG;YgB|>q>jK--Ik{GX`FS1VwKvKQPL-VLYg>dWBJ*X@kicC4g99E6|H2}?#P?4C zyu-N>tk zS46;mVPoW!vZA(VS?X0(^Ut|@ErOI4R*m6zV zq523bNae=T%+cjXqjK7MA0?#IpDfo$N|)A84lm2-Oy4iRSQJa>W!2{sPiR*#R84SjZ_-IkJxXNUU!&y>Ubc)gRO1fkE^a0VD{nYx zvZ~L$Ilb!L%|^F$C;q%G^CcaGV6;Laj(Gc(GiyVtSX8ToRqrW-0z}u1(dEI>2c5B9 zPnA;R8*@6fMQV3rpz)C^swcbY7)y31Z3OrgG~L#uNipGX8y{mY0o}-VvzMjpUk z(9;z-I8#hNPf&SyJ{;#;%On!~in(9EiwHwq?BGF-?MSITkzGpPXd&Dzs~%jFABf&V z(6X!BNIYBBQ%zrOYY@F)0Vs67^x=VZ*hhAyO$?7NWcFQGPg&Gd=CNIjOYcc8TkUf%|os*MqDrG@L^% zF@|4x_P00?XaF%V7+Rg*Ov&8!Zg6(VF;&`^1*%FV{oeVF4QJ`80>bCR8R53gA;9uC z9Hrd3tV$$a=zLU*WNh;?LG{M~B6jtMWZoC71HBHc`UdaW+NkTL&pGX9px9vZX*)NQ zr_f~0HNA2K>LHZregjY_3CA~Frho#b64DNxoy~B&nSlY`M`bmtmI8x6)f@$q%T6@| zXi9mQ(}89UBJng1IogSHGU1BC#hNzlgAz1QFtLCuaMbWpyB@q@4o*^yFI4!ks-bl< zoLPjwo0i%Cb2>qD>GZwIF6@_U+y#t_F+7WpVBUcJD^w`g_q(Zv!ebD{9|Ruc0R|jO zbFwdi)jK-F*AmC*T`dMaH8wG{dn&{G!oNZ7mg|sCc0*Uk^4D6s6f+ZPyr9(shSkB? zcP6BN5FY(D+&5>2G-OI-bP-mKG(28-@!&UsQBmYV5U_M@qlrcWO{sj-Jb?BJP_cnW z{(k}od`#~@|Gxv1iZ3pmNy-8h);bnPaiy4TvZhUTuFcv=S(g7+c%0bbTW0vY2lzXH zxtVvL`2P!bcQ5h(f4Zh&66UyFv57CW+hwIaFUwQqyO3J*_i{_2 zLSu#>nx)aT0uyxW+HI_GOwSv-66#jMUg<|WPs(_IFAMj?sTBbRaQu%09pC^m(8Hd% z^JPsGW#Dj-{iT)5qiB&8lIPUNn!^$Uwvdp5ro)WO*RMPb+B`6D@Id<+#a~cOlx^#Q zL08~B1SGFML#l@v09&q$OK342#_p)*fGPw?l(ThG`Ei4tQ#qLoailcnhcPg*Ud%eq zBa1uae7HKQe=sTJjuMcG;Ik>pXf`_VlyHn77(w259)JJCPTLEmyST4268HiAfj|%^ zFUPLGw46@)(PR;`*)-txrV$-)Ym{Nv4ASyz9%-=u?z5Kqfgvvw7n(n*ny0mbpE$MK zRzu8=EG3hm;A{cxfs1Wmn~^ofAK6U0+1;2O1*U3(a+TkA^HXvf-AK%2L)`B#Hz4zc zCsy$C8jxD8{ReuW_ecZ~(k|hpo-aUZ+R}?&A?7tv#lN)%cQ8t5i#1l5Sfh5)mM1Br z^}{rD>sC=yCY~Vq2+WcHBF>JqFH8OOrtYW3_vF`c6Bdg2(g{t!bTxfk28GJ6R7QI( zxk0>Z;th!Oa}u9kdz9Ld5=uKXi!kE!3BDduJEU0d9W`yfQcC0{peuM}p0q7ITH5Dv z!>{zbA;OPMDOZ2i|Mg><>$fK-kL_KntJ3V~L2KF$;As0iYbGO`*6RQB zR-{rkV5L~a6&$MKbrIe|qGK9D^L~N2I>tY()PdDY>>9{99mz{qdtP}G~8swR@Ga_(gs$DX%<~hNJ>UpZTV$7%gx4%E(s>AR|<3_ z^<0uAtjgQV4Bm>`E|+mkXd|tcQ&QZBB0xb@Bb8e1S~LE5=M5^3!m5%9i#fZYh`wH& zVzGnv_h($*&=z8*sIq(Q&u;^c=@7}a&^V%=bxK(ghaU;z zGfsW0GGX5X&R|=8qw&8@{vb?OXJ;XHSfdqEA5iU)bC{3vBYp&Y4eq<%sO1Y-=^y** zwo})0NP(|s)+%fgovp3q+$q$j=Rqi7E3;elURecC4LcWyw>eSAxPSK7HBT4^LD-Hw zu+wjr4yl&X=$4Qpeypo%+C^fzz}SLX9ElW(uf}Ma%3c;-=vhh>oM_}dCtMaFDpS-RB#@w%T`M)OR3TZJ~9ZN?C ziJ}MHj6>loz!jpmlJ&~DDz7~noZ3vDTQWJ&7+O9s$Q72x?s=i@Am?bTxL_cLTTnXG zPB_BNtLv^Vfw-H1O7(>!U{fj&_O>#qNIYF;DadF3?3)_1f<>bG5^;R5%?2|M`4E}{ zjEhMTOAc$ z8RMR8^v@_`Js>@R9G*pW#wc%1i8M=yRwn4UUYX=xw!6fB8Bg6#$w?_myK0TkU=hEJ z8BO1`{JPZwWQe9G-}5rT3D0K^siW2Co^@Ir3&`9!LpRef(_20|WX>qWPW7~VaVYYj zHHhZXGF`Fq6*#~6qLL>Vg*Su7r6!w#y5Ff`G#|xw<=Xp`tslohe{f#V<1_c;t;NA0 zb)4r4m_vANS8BK8sDYz_Cu&pD0q9ck%vg7E%m8AfdKXmyPzCn0A^^0;8392C{-6e! zn$>JDjry1QrtY21zBnkB^AV{6Fh{t<9H1ou{QNsJ?QVYzUk`wN+}&sY4@}+BzV#~q zBQt^*^wP}L<6pm@uD=Whd?bFzqDQ(OD64X5U9EjVfX8CK~+q({W@GAVDjZ@#YXR zX53!M9HRt6Bz5}Rg~$!GC7NNU3qqq5w8|bfN~f_?!+cKPH_>0d$6xs?SX{|4qd#aSTJtA%#Lwgq~zZuI_!b;7+a+RG+pMH=`r9T}S zwx9LxJ>AbnDrR5DX8ZQRDqT5rM%8x033gt(J-=w|Y(dd3CuqH_68K#d1IbrKO7U*`D*0SDzx1Uy_Zw{vljIZuVwuh=lPcvw7<(@IjPEKIYQq;i(OH&F`*!E761EQYrcT@4U{Z}I!r!=o$ z`-FUcjQDBo4}#w2MZYSfzczbvdp3VRBBi~z=O+`V|X{>OG}vb0+%=viK3RUIGm zOTXHl&v0Gi4f)wVSJ3mjAMKlk1!L+EGg^fQ%4N%j22KU_L8VX~mGCPUE?b_HaU zd`z-QFd6D9TNK7<*D`epq*zAwM{hv+ur&zx3(s$OJt!hbu=h}>6oIMQ=Z;%BflJoh zHYNuqG5W3)%E91KPhqi&(y z!$5X3Ny@YS{CJ!B0?X6Ui!>g-{_Jb5rcX8MR6#&BB(hZ*@PRA3HJgzg^sB&&@!X!Z z*1=A;9h!`b2qS;ahd=CEN3+n1v@9jT61lOVjf6+&<26!oTJmjAf^9!id$}!)SPKtc;77b6?=#^m`Y02Wc6j5VrI=$Q=SyT+cOwa7Z~}! zt@P=KC|x&JA4|68yp&DAk-_|)2sa!ra4;y5O|pRR`Z*lM&{2eCLMWC=9Iz%RG#axz zZoouLg4qj?pU5PxQ z0Sva7npiBqqn`pxS`Gl6`_w(Z1Y@!$Y+1gqw8U(jJ}HgLoUOVghsrQe>oA^kitYtG zJm1;Z4U^cl-L1VZz;Y{a)sC9H#M((++Bcl(4qgkBs>iwR-Dv^nB2QJ~>D;AXb7LuW zT6H3H&w<7)C31}3HUlY>|K_NgdBjkpbIn%0-Fb|?(15iLH8_01%mDyv`ail@IM|gN_cKeE0#lLUs!TL=S7a%E`s2w7t3etk7v3bPZFO0f zf;mQMxq%wlI$pI2|Hs_*#p6h2d}SNqAHsdJUstJNJpWC2xJzM4m2U$o1IF$?`|k2l zU~v51qJ5doc}L=Ln{0{%nf4395(#mru|xP_g0#%J5 z;pL>kV!!@5LPC71B&AtM@*f01g%6#J{YoC1^oXb)@(v;}@sbNpI`^&w-<_j>v_0v% zBnq`_mrZM6YL~@Qo#_a5;aF5{M&f!?9)tAFji=+*pHTL+issA6CLuq2j|pLe;qn%G|u-KdcooJw?JajPGtL9ti2vYb^GOlx@R%VMAD-eZw5bljdXs;{qbWHc-zWZ;C#=|H<$=>gd`XC_y- ziZDdu(s3$C$|R;mBTmDh1tm*Vs*7u#3kl=9h>QUfj&MXrq8x zP&;o{L1*IX9*04Rq-Re>@&%`A+LDNJQ!T9Gbf2MIK4Rph`h0J+b?;ts-#NV3^!<0* z<_)i7-0d6K=DDyFE~DnWp8ie>(>8KAs*8`z6gHdT&+%D9ATjucVSHY$jG@%*{8-;^ z`uVSf2$fDZNgv%?p-~KuswQzm$_VcL!qZGy%vDbnCFF4dRo-$R-S~^~1Hj91f^zhc97?l9+1e+nFj`*>Tkrra!AyU8 z#YaK6B4^NQ6H6vq376IB$0_0638M>mH4QL9nvkkh{(`^P-jPmt=dPxc>{Ok)#!nd& z3*rM9bTr(~60=p7L92ytw`Ux_uBFGndhL3Y!dmdc-gvv2&$2n7t!>RtlwjT=l+W=Z zPqUy~Xuu&=$*nG!Gn(`n>0+*g;8IZ9v-G{vNPS0U#f=bBD;Z5zK6CU+tQJx93@3lD zw`C@VAM9Bd2}@J(R8>;(#dzbNknY~_$qEF{s zS+S$WE_*R>vP+__$OFqD&EaOHqrDhb8P%S`Q}_M&dTr85%U1PdI~{<^UD|JOBB{XR zTBv*|hv0f>w3z)3UF)|yLx#aNX7#9)^`0+3rcaR3#rB}q_*=HPU!_|UT6{)%Y_vGp zE;sS0d>Fd0|AgZjvgsyQa3ZKa=%aMt(keo++^}YAuNHGv6hk>XC^ItJicqwiII=Gc z-kpI~>Q&&*gIb$14byd_lj2QsMs7tEn-+zUC;;vz02*Ka4nu_l$$i9!yST#X(eP)q zYy-C22S82Xex2Ct?dDiBs10$9}s=J&$Z|85EZX#xD9 z0r1cCUm?&x+t(U)i0}Bz4}*pIyIEWU9b%5wV$002A=+mX{k!4JmV$>pQgvL3+%m;d zyOC@fjB6rmN_1?4p-{&_r!=|PKM0uRSM_{|+2(4`hdw>eG}EDSr_m4C*c?z*6Zj63 z2@@l|cy~OZZ_PXzmPyZ}Fo#O4#mdyqK*vq?UsJ~ckoz}DtH9H2Ju?^B5hkl3dfr)k zV7iY-B~U(uDg~H*cQGNhk&M@khcsCOY99+yTB*s(yb5rP>Smk~24>!`5g{>EZuV|D zi79qY3x(-}Jr7vpDR_q>25SKa{XTZX9QCAJ!tLGk~DyiF88QC} zta|eW(TY0-3@OA~{!|#a4(x^zN)#;v1BjN$@A83i6}zVc;ZyqCFUU(>t(KQopDM{V zXFVqU6sA0(rDoL0qTKgRqY>)K_MTm7v&f}JV*~69{j<`LBJt`66F+py9v~3SGfW6Q zsFq)eggrk8xjFz<)YK&ON75`o#9n~69SAFlqdVl-CBmOXnO*SC20nB{inETI_ z2JS7m{bnbL!44V8lcQjTO7qN=(z%t0?RN48z>l`wqW}Ncdh56-x9@wLZV+h+rDF&g zq(P*+QwC64VrW4@^wKeOr$`PZ4FiZM(y5d*(kLaMV0+)cGc$0n_w)VzbMH$W=6QUc zbJkgV@3q&CHl$=k!C=buI0wm&%v&!#8mwHcvf##T9*R_u$!WG@3mS66-`}eB8v4j% z#0w7-s>TaFebl5s@u3GJ@O>A3oay9?sG1N@JaXNWq3PJ0Es#eewl-|n5QCDNfw z)(g6Cr=pts;Iz+tcuh8!HJKsIm0}@ePF;3i-yk%aRxQ7Vb*9sMoUGKO4iZKvIb=mc zdnmX|aG`3nH>HiQN7SF`NAwLMo%z&&QDPY^8Talpgw-T>M5c90j`$cvv^YisRo8@; z7klg%x9v}2>kK0(tu-pJ zRQQKui0MJtH1l$1;9n*`rXS@UaPIp7wCH4v4=ho% zf$_B55oAiS36Yby;jjt3{ijU3wP#RYp8ZajJmMs=Pam|fHQlfHAI|3h?bulN_=>VG zL0m3VOV76n@yzgFFdRR0l2uC0B6oF_rhyvKF<^f$`TCz5z^CR1fv&f~Cy} zxeV^jCB2O_JGdLEG_3cUP#4UoQxph$`{)l01WVkzx0KzZCGJ6GS##{GOftV)>+RRb zmiZiC{3S#_lJxYp?q_z1n|rEP_AL$ozZ*z$JcdVo@=uo(yS1dQqd5(6ZSR$V|DWs zlv6OiIB2v-7w<8+jOkKE4;1vIMpW&RSVQ*xH9bgvPP3H#jKKgD>Aq>AZJIPs!)jZ}l0U-u(z z>Nr)J?{ExjBw0x&xHni0e;z6NNoliBBF}`N<%;8%u>yg)yU(({^GNL+e%?sDwe~TF z(&kNYoR<{Y)e$__zuELTEO_+a(~QE~HCYm~8J66BhJ%rVw+c7h^$6Dk`XEnSep#*R z)l?FN97HlCO?o{PxG$vwtheHP>m5tfQp0UN-yT}{d=^$zxF$SAar9Gzy_&}u36-&;%ITuAknBEC$wz`EqO zH!HNJha%;TcMTO;LI`x%L|w1o_l5)(u6(rjDiQs>vAZwo_~D5&ZT30EMkgxKuj@WW zc#IvY{?DlfDE2W3_v}G)p)+q%@=ukkeF`)i+tJhx8tYXv(UO-vQO`tTW9SYLc*p_H zxp_Crj(|Svn`~WRuumBXcv^N~xDkS=1JGEK@N>P!A{ohl&!7MVn5Gw7qqt$=YAHKe z_V<5$lm!Ml_jLf8t8eL@AFAo@nbreUof-f+u9Y*2U%i{ckf1V!a<+=ptL&=9&oFB+ z2ddElvdGQQ6r{F^G!EjUyqHobf50>6Z;CrhThE20lN-&|3Hw}p6;1G;&DeG-up2B# z=gL5c?tOWs)sp(6|M9amT&>%bZ%X=|B>c;W-+-Elqm%BT!E>vMOtp_vV+xwyxdBI6 z)T&N*&t|8xDmQqE?;vhGB3_c)9x+HO^7dys43I2OGfmc0$e&}pNpEMQtcm=&NzPuW zNX^tIFbl`cke0<&yAj1tc$ei#%;`=@Bd)92mXY>4sroRb>GEZId(C&_oF41(-3$h5H%Hwr-(bf?cb%kr6?14%q6OhYjAc_ zAT_Q*UwmU<%?H?RB*4_CQ!m3$ z*7v@81&V*7_c`Vq7r0C;a!J62_s63=!gP1Gwp9crog+m}Bt(M^R&v-|NEj4)jBm1n z?^LO&RK5|Ftm8Jqr|kY|AhmtnQTs{EH(Qo6x7uIs`rkPb%_ADqV-ZD3a}MfA2j_u) z*7!^J^{#o<@Zj>*elO>I+_ymGVccTM@@>6}oZ>6rdc|ewDltk4+Jj%W<;f5vj7SL9 z%@-)8&?5=}8I{k_Uf-3oWp*YbMgA@Aa&ntT86pPBbX7i8t$$~jJTT?+Q~S}IpFATx zYU$Zus<(QIU+yf+GdxqV{CeYg(wCR>eEVA|kALX6NbNWUJv^}(=NW6?Vnr}raY)ae z855&(3jt0?RsC&G;cj<*R4$JSMPJZL6mXvBfnI_3w`ajLI2}K!izAv z6W;{|>e>f^cNCsk7wb2fSTv|N*xfy2RU1pG^n}Ez3 zztA#^2QwL{rH5aLdylAwHAE?{exy?qZvs3lCzQw|_!iZV9p~IOPu$=l$a82X0fu+3ylceCbPF zuy9+C76B7KYiiZ#S3uWC=U-8f346)w-qm(4l3mf1V8X+ohUDy@%hXV+RAu_qt^`(e)55TK%ALgUd54T=qYY&wl`DL_Z5itR z#t?NfM@Qyp@jv;E5@~=y5Nfl&dNqAh+p{$j&aPj`>C5A^f z8gPEoYJqndoY?}_Pdj|&;^{I3?x(5l2GnZO3#M{1M8ma19)I^Uy2+~Cj~F-}mH&2l z?;4TzeCABYcEddI;KWQ?tx8yawxsUh8I9>@=CIFZC@IfVhlUzZECV^v-Fc7%NEYQ~VM!iy@iNih*gFIkCHr?IG{a)`)pB3{}uNp1oKHZ)N>P z4$`6sTgJ>W3MB7BUhwBYQXlfWX$j0Xi^jin8&kUN^sM6LHyzDyKHE!Oo4#%Pi0 zliZ+s3wmRW#TUCfJCEq1vJFG0e;t#l7%Lm}mtIV$)_U0;^dwlTG}I+yatTrJSgF=b zs7rEyR3P$2cB*0R-#6rY3W{Dw6|@SRFb5|RbLn>TF4YUL(8v26YUd}EQKJKOzyY(- z(joNfwbp~0a=B%$8xhM6J^)iCSj9&olfFNBea%J6Tm8#{@bmXEj=L;9!chvM!_ak- ztheHaVv1ixe%TDn`SiWI(xURKoRBGxj8m!c<%n!)s^HG-lS7-fT!C-|8`zvfICd)o@6^|4+4O%V+n}4cuofr*kov+J%@xYxETDZc-^1`_G~+|J zn_zV)V6X%LRLq~wFWL`jG~ML)F%9Eku((@P@^H?0(DV}yFp1C)1qwN8SGpd5cNK!o zgwQdRfDf_ClPd%@q!-Jy_A(h_s%J{zVdhnc!&$P)x|h}zLuJLvYEZ?rcDAD$l&R!g zlwOFOt@)66U9j;&=G` z4Ts5+W3fv^c73m>z2tt^7n$bRb*faKHGby~iVpt~Y1qfmAw{orJTeywJ)t@!zKf3=47CHd z_Fgwg-70R6EMT*VDtJiFeko%6ynu=hJfgr$L5Cpdo*3+*ig|M9Fu!jY^7iTP`w0r; zBMmwc&1jxVsCh$ZZt-c&TN-^UQ7|3GN=TN1>|KxF6j^%^NSAO=mY^WJZ{3JMi;YB? zPYe7yu`QU9n`cvTgX$uStz(2v>b_pUh_nEnzB6y4BwvkpWaqqo=)>K<9jOKD8Rtlr zfctmsq(<+6#y!llGCpt~HDH8($@0lsf6NeHzfPgAQ>G447$3`K?5N&q49!caJGk$- z6Yg{5S^lB7k@AH5s!6CIS#DylV>n);q2{`;5@CgXMHxh$l${tFRG?8FtnB(q2j}KY zIQ?zr3KN5t6dsOys^#8t>v=a*aWxD|-bcjIHHdrUepm?Uh@b@n_@!m%2w$|`dj50%a!Rsu1eWyv~g z6VtTsh5A2ix~=d&Wfb0KI~vHfb&c7P{P7}>r$deL?E#-Rcfuo1k}EFXEUwmj{>sqBn-WP5m$D9ha620>?DL~ttZVG);{(gJg%lc?>Js7N-v#YgccJgr5bAv+rjw7a-QhErnx`(E z-D)3X@X(qmHSdJNtoIBB>-($;^@v8+5R9@X8}&zni((8U^C(4F0aGd-4iCM1xs=AY zV6oV3+@Zw$0@pHBS_^|{f2Y3P8yOKtQQ+HDim$@^_k6sgPKqMeT1(g^f4V%-1apby*4dtZN7#L=`+|FKPc!c}#l}A9?vHWtQ85*Ukt7LJg|Z#1m9~p>Zy} zdzgjprB34FsN~-TMa*%7ED`DR8PNvVk>c|~=pmPt`%&&gwnx=$AN$6|PGuTfjq!M8&D$+KZVyyCgbEud9b0(86^$ngOzVPqlh$q9r4LZRs#=@l~=Xz9wL?{_9ntYU+@#s67&nGhCEKR;IPlLlNIWo z1h)_!{8n4s=D7j2`Ct>a2?0|pLpKg$|A>AI=cUU(9{&Umg4Ha3Px4mWrz=j0^Ywjl zKmvI5G0c4J%h!>SL`g9EJ)YE>1o_)X#|+0~7LU1xP_!y1Ey)DM+zd;*)c9xlN_R(Zh zq559Q6%CW0~m z$&bvSwmAK^Zl3G7rT3+qOrUs}wD)8BfcncH&0XdQV3%ketn`xN!<2YJI8IN|n3@Ty z1O2p`nGX=PeSyL$DtH%k0|jg!IRGn%-I7(0eot@5bvj_x^G&Z%ktjj$)8DnC%SKii zOozIJ!mq1Vr!J0h3qgIaT!2gV*9;edT7&xlVeNEJXXS6paX{q>^p%6CxR*6whJWgs`6lF= znORda>PS!ngY1sSLW4$qN<;YS<7)Mke)^Iom$@8Pq@;a)qV=9!A+3+3mpHoK2y)<^ z#CC{Ut|H?1h*q}t?dvxd28V60F%4-TbKNZ@$_?eVxtpdk*sg!pgNui;*)p|`&eRRfsY|wKv#?AG!0{qyetfx8`38)1vVVnm+U*$ zQ-;8EZ3eu&h#8YosSt#|^*8a1GiEra`5s>WK@=lvCUfHdyXrx$L39OtZS9n&!0LgQ zaMO+ydn|pN`ZV`5+F$RuD1JYPGGVn!DoYu-=YO|nQi;`UsnuA)lFY5}qR-CJfS`3% zs9*@wSMk9GZVx(9&V}x16S}u(V(o$vT^262~xJ$$WkUEDZ>~Ri2@SOZ%<2O&tKR)s75uD*8mIh)!@Sm9cKcaGs5%?eb z4^1zzU*z!T_B{7vJ?Av#Vt&Tuc*Z&c%#*9V18aQTmEYJuib95pX#`BhHvuI}uJ36f z_akpSS&^LE@{AUj)(&eSykMQDDR-4z?~_p2!rRJm`|d3HM@&tE>0XGaAFLFM_Cf14 zCoHz+Q=T2`H4PHI0J}G*w-G(gl^=wc1;EpCNnB!7@nee~gR6TUnfvfzPn&^5|Y zYyt#L_|UI{;_Sa@5{P+*nCX^hbQ#D6_0$(<IiZ+rKg!tON&U!xp3&zlyu zxO4cEs?4IS64BOqa8av@1?HlkW-C(AjAxbukrjO;72Q>@V!0hHVMDkq6#T;dZn9I0 zxQaeT!<|RmsIagWB;CTS8|qt4^9QY3>gu-3RxLg#5l&>n+7ux&S6rFom-lD#!N!+Y zcR5{tOWvT_u2WE8TSl_xE6I);Zhms7hXqLVp;x5wZwCXbHtqTqlno-72W@Ob@K~4T z*du0a+JL%MkE7d+%fCo`@Ga-;KrbTkZ|O^vGh~%jr`^xvkg!CXqlVNEi?b`70gm;m z#ZOnfC>vlY;a?K2*H=}^;`SmOb$BFgYwF8KNJw~GlP{XFtf{rt)43ULXo1O&%Z4^; zuaf#ZoU0XNL9?mjI%&Usy9oL~M<38N5A>`CwaP4moYh&M^OT57hm)kRv4qTh{W8`U zZN#7KN>3S|T4*n?IeaIb=$jqS6^7(I2~vk?XQ37<)4My(vNAz5(xFpOr}Qj{C@wTy zKT)`=ayZsG&I}(9r4H5$W(%UXE#L!4w9WqxxNxOLj<`ydaxufnM@~C#gVmmI_czxd zLTkc1`imBixkv-?V>NEER4PDxid><~WOpn5t-kq_N1jYBrX&cq3s1M6bW;kCm;OGO z$d<#oc@Jp;7pf!IH3Wj)vqcClh}DH-qZF`AF~|WpPt{Da2(wvf)4{l%1-GKDnKpL% z-2Xuq%(})D!Ld0*te@t8q*~{E?7w&eqFoFU?~_e**ZnIHMt=y~FN1>q`3q>Z^}pqj zeZaub2|MgA1HS^ki>~xXUnfcSqc_&dNCw(i2cotrK05AOc8!|_g}c|T5iC*a%Q5t) zL&aqpMAA7q#P^bg8*caFn-4Z}crOJX@sygtSov+V58`;!FCyAwXvx)po4&=uhcr+~&-LrHf>{*E>lvUwtpvrzb{`zT5-Xjmbze#)SvZhB*P zpoCW>s&M-+DHy%`T8fj0>GUJ%s*lKK;Nb&xkCL5b`YXV)yc&laa-VsKXKoL9y7iu z6#H1wh=W~Cpyq|;Q7iJsOz+y=gy&w%UT%L9Wo;$9e^fIma5WIO%YXD8F>$QR;}wS~ zN^bgtPMp2a)OX?%YzB$+zNh6d`)CPV){bxXHBE#Sp4nA7;+)~NT2|1DFC9YYq$_vU zMPXK^5CxI!bixNLY4Ek_O)&swo&raiUhdM53BudP=4RF94Xqv_jZFtD_mauO-w1^ra5== zF1xTs@WpS%6*5NO8kc~huBu>Ii}|+)cFHG}5{DacJdM{CYyXxmo(S@oZ%NkdpxS@{ zL&=pWQ_H^Qbw{d>S@BPFu8Zo{+U;jXJ3jPG)pVZ~GUoPeqsObuGq{;ci$^N6RM ztUJ0K#cz^WIDPlB43D4Az&;`kff`u-_H5PEIje#tP(@_%@HRy?@Hz`%%_q#a(rse* z|KMl9p|;q&0MyU-K{sLis()97d)$W!*A`zzh_=x0J3>{E2UBA5#>~Oc(UODaWT~{H zVcj7(4neoz-FQQaa2^=;oWbk^G+4p<3@3NZkEMuHRPI|)!tQ&}ka!HJByco^o->6B z*m{<6b`kK1Gn~QXbk6Vq%g!)M2U|j`VQVm};lKRN)qLW~oSoSS6PT`Lf`^T~u$TIC z30ly*V{s)ByEQT9Am~Y6XT<~Tfrvqv^X-3rGJ*6HL{cm@zGbsZBcWy*_LvgRGEaNks|O4E(AUj<2f64>S`Q{}yDD=007 zd5INfO%Z#XT0)-TftMSupOF^9o3M* zp8u?77arggh()7yq+kR*-m{P%XhH|;GANb?a&=o&-YSqT%utlO8ca&ARrX+(-n%N{ z*?Lq}0AH|iMP18-RiO+{J7s;mxy!Y+u5Z3nZ0@G93A zT&tqh+5%JbRNN^oxZO34sl>r#%@q~L{clGGuf*-=^Ak48Fw-i9@VI(%X7H_$(^O95 zbZiES5h{H8Qq=o{GTZVP419i5!hI>^JtKNKTZ3a!eRugze2JGen1SZNzlirTenHzP z>-Wu?*)PS^$4nVx<#cOZD6-jvpghiIAKgE_yV$J zt4O(sS4GnGEm|dx{nCZZQw6@=kB=^dLOR@~Q*KWNR3d|sSPp_;~f#cGVZ$@!F z@J&dc5%o?LrIjC|gn_-25Ng%~$5gnmNk_1iEljD}s%TB8*QadVbKWqGo2D&NXk0Aq zrEzT&nfbV6fV=thd3ur$irc);JxItxy)$fE3gt+1i%6lkU~ykxU#ZcE>qq55RF%1s zt8YPG%F&^r>snZL_E+AnS@u`xa2*W+f0#03c0GE@oBq4WCu{nw_~=cBK)FdzsbsrA~f zXbZZhsb)&XE@Fx>k<^DeqxG==^A6jq41iZtsDIRn@g;uF=yuhoxTzx6bEGOu$414;! z86!^oP_ls@dWQnLM*35AxTxOS-G)=mAhu`D4pqA`Td3c@l&Ch!-}PdikBfeg?cdX+a1DOfaLX&pq6Ycgc)y|9xMrVy=@ z@nk{V_gLC2z8YW@P2K7m^x3*;VfkvA9~(rD`Y%_5Pa7Q8nMPH}i>2RFSCn67Y9a3= zehgsM6gOvqzq^#GK|Md1u8Rr+=}|iA!)#9;V1z57sne}6TX`(qx2Ne;p!&&5e7EYA&xe;hb&1Lw2*cW;7pcYPKO zpCJ6qj9$Q@;La3zWxP#z5HxiL#h(PnoPm3T)pc)NbxDbOlriO2_%l8*}r-odGwd> z@|C}iQaGvG8~KJaFV!L3@q4>s`iVYw43zJXpwdgWSV8C8Rkg zzZi}HN~tOjtZ^Xs_&4N2M|)t941iCcv#19Rq%OI3W>MO|3)-r0<#@G3T14U_$<2=h z(TVMUU1_kR#KGtSpilaPUX>tFP=LSaD$@*D+ZdnXKM)cVWd0jhp%E7#CAh81A+df) zf3~WYl6b1yBiE=o>hhG)(I7wG*zJpU3+4G`xkdUtweVwC9RGF+xb|1G0K%L%S9h;D zQ7FnT<;1F3?zCF;mfFQJPr>s)^;1e88}Qwj1d|ZXt*Fl^1sWaaz~!)6C-mWThSvcQ z^pCK`v4KCJ6cgc@O8joi6{7-eAM%^#lDl$t0{ViXBvwCt#GUjlNlnZ(=j@vLc5V={ zU2~_kd;f`wW-ARj53wiv%m{)#T2d3m$e*(l27B# z;`51xBT!H2rkHj0_<|%p*zKg5WCs?#onL*7(D$Ps6X~PGJfWhxnuy~J8AqOO*n1X= z%kF=vD@XfEj`cd(FN>F`RUI8fhJ6?dS``ZW)KfTEDR4FWbgg)yWjZf%Cu<}6qN|vc z7DIcDPPNPbaI9JhJk0THh;HJ9+A9?ddFBQ3G(T#2E_fP%zkb8z+vX4|?MtHr0>#&(J$pEY}HME;faJ5#*^iia|bV(HW{LaeQca%-!k2BrOicIJ@b z-grnfsS0h@>CH`6+}lVYh-whaRu83v>=UWQ+8|hc{(Zxk4)hX#oE)-(@BG7Ym&h+D zyl8$9Xh}6K=lq%VTR3uriU!4XbTDlvWL6?Ky{YNgCgEJO)eOZyj0Rs7h{(m@Sd`SHPsGsszKhf7HC){?g z+*#YRWPw>)ZzG5KyLJu;e{%gRi8xwUDQfRaK| z{dt1J8H)^>l)k4=g}HO?*-8#|SsJm-C%g%k?6hGsGQn<+li0jnc!Vi;N8Xc4F zcA<#Nn)34$;je!$OtrNPmN5G|(A$ zFtK2Y{Q9-E^=jj5G=Xt>tn9o2KW^OnB^EAb6``nW-*U6RN02AAg&4~^(eA-#9w;?X zv!^HYug0_^TXwB0`R|!1%WcMJ(Pg^g*gS94>kv#)S;*mZ&Hu8QtL8*UaZ7>>bVv(_ zN>(M-LXai2jWSnvLJfnW^4NAGlRZnRHH=aTlf`3I3z0m8&uqa2t$)_@zjJ}HP=>{p zr)KV=Op`St`XiCnTy@rF_hF#B5i@cH4Y>eOxU zoD^-)d9DTVY{N7N0v?NKWBq^H7n-|^v*|fIRNTFJK$T6CM=nf>GwQf2a?{j=-a+=X zj_8y)cy2AN@~y*e+JOGcM^0H*G<5@<%lb?98GVubzL3VJL3U<_n`E{EK41Hjw~E+A z>8&-Hrg(_j*yP0&p)CTeFTO)e_4B04*lh!bKdJYmfE>-^=f!HoixR3v_wdYt@)Prp zYmD2f89xiBB=qho1U5`<>c;BT0|Q-3h4+RL zYVe|VX6hglG3X%9l)ngP#`nK;r-eOp->u?$vE#*#dn_A#ws(oUBj9Bxo;0MIaw#JM zx81vT?s&@7eyDKDAqHY-Ncr5iNzBTDOg*Apw{{Vdx3sF02PW05mLLhLEvZJkl#Gao@}w_NwhiR#jsaK#0|xbfQkz#O%#s50^hFAqA8KsmEClCG#Xe#C*jr3d z9=yP4deM5|vdt%?`Sf2*wVI`p)xD=(#&h2K8+a)5;%wm4acp|hU7hFI1`NZ4Z>B!4 z-#85QC|rGf%)wr#UgB$0nNsaU+i`W$ORZP&-lgghGLcD|=I(YyuK<3)D(tgZwP_Om z(q%MCRTy6pt9M!xQGf?4MKv}`;R)z2gg6ZimS1&q=0 zCI+$Q>(K=^JQN(ZkhBoIHfSSoEHLNPl%1+Y0Q0$;&Ym;Ua=)PxcJS3vb*C8NN7&d~ z%+6newBUNsSbO?+BukdH#}jh5BK7z5rh=|E=v9A*W;{KBKW+_;Lig^`w0; z>Pe?`%hXK-u5M3(vGw3OE)+FIuQ2mnE6v|ng2U{Cm}K?N8d}G-h|b>wozz)UF1bxvL(&Y*YIJ2i>q(_;H!g>f0`4my%HhN+tUzm~?D9$aCS z{{rhw@wFOqm+5F>P5FD_e>nX;mt%?gAWLpu;vTFQTdTUmI&oj=;*_cP^0}DSA%ZwY=9OwG5I6x*P#=Ipoog0asWfIsU?gTh z`OS)0e>ppG z$9qi5l^?#cCJXxamLeT`@Tg_W>S`{+yt3>w7~;p*DXwooTWRx*UL4-wpD5FuTNQd` zt>(>hpKc-hlDOBk4UfXj|cQ$*vh6 zEQ-}u{j3UDI^eYRk4>P~<(O0KS#}kzQ44La;nxwL;W+CJyNEX=vBN2CGHvlsW#v=W zgw=h_6hjacq~$^WbkdBGcZy}qA46N#Ztr**p;%zn6$XCR48wUitL8(<_ejWg_1!)z zHbF%5Beq$(Q%70)Ee)bkit*ij?~&qwYZph*1OPlMG=8ItOJKBVHs)N#=7yhnAu6?e zE2lSmJ)RCHGcuP+>Y}7`c{1}FJ4L49PGq`bFlS0Xw&64J2WT)MrLIJ~E7B~Z zLaT{%- zNGMtNuReR*G~kLJkDeM94I?Z{)QCf&`lPR&0XpkH_ovU@maTgM&zjVupMa19{KI!%!<&?lDp*eQA7}g1rbj;eVxital~> zj7mBt=A+V{&sY7`B~!uqxMJA{4Cc`a;P;}3?O6v3F!cT!KoWRast?q9fL55jrj6a? zD1LsqZHKTI*A`#kH*vURG^NDMg*@028^nkE(8R~-l~9#0LKE*mq-h`62vVv#;9sZ5WtB}UzyGjvWt&NO*fv15XxAbu3M&QIKG})oQPMO7I`bpM1Nb?zO)XX zP7W-9e^3GIKM2+Wnlxd5ioUdiCh;3)vnRBmzQ6`GTSeCwFstUzMudO919aj3Bk?+q zEzm3u+%u5#pXDA)0QWhsI@|;cvGTm(i);o=EL^B~bB~oPt^N`REjJCGN@ZZJv>WwI zP7WnhM7{~9SHkO>jxek|>5XJ|^Q+md=_!`B7Z1{Io&J#B?w0PgG1uJ=PSz7BXRDIh z1Hy+On(W8I${rWhH9f zd>4)s*oQOWiE^~kv#gQW2W^!X@j+K4@89VanB4U_%F{{t%-&{O?5>p2DtrwWv>3GT zw&B~QUbHd_oIAPhYY#CEmjK;&%qs4)n?9P>L9?93ZHI`gi;?Ej2}4Lo&JN?FG{lKa zQYPA~kFd%FK0F_9v=spu2Y~qpFanffI37EYdEZ=OP461Usun%Trt)<7gf9Ir> za#3(lND5%9B>~`+627cF3##Y}8+y6@@gT6z&(*fL?QPBEWvk7k7vEhXn@8A5NF^j6 zW{EKE6k8fLYL3kc;pIBXS$_1@y}nnR7H;aOSwAdR`~)`4ZIo?HXA@5u85}2tLD~+Q zBA(AgPxgAXe-}-O9XXchgV9t<(TIdRhrC;!<(b0`(heyCWL<_c?F{b9mC|Hw^-L-Z z?H3Wzl&yUbDOzrwu{qqsv=|gkhQPv-2E<&FLf$($M9G3c6g`~bT{bdHFhk;_7&lC* zc;v30%HG8g{qIl5$k1)klB3S=u{S|qN>pPnVh;)0ngc!gjT$kcGWPeai*~6z9YE>E ziHfG#VdGjLEFVKhiVIWGEBI(JK7In-YL-5hghf(XEKDH zl6-)g&pv7^6{f;}5bp{u-b|@8qb~$_#n9E4SM2Q4)#f!DUGl>V`%*k@5fVI zE~mG+;1pK1XblArQeb(I?imHDJxlw(g<60!44t-jYwoUwDJ=%O!iqplWG@PBvC$=4 z!yH-Y3?hK6A_NdP0YatZs$CahF($V&w^ScE4U{5@6qK+Ow9o8 z(|EQ^s~8zmxa$}4bVZ+|b)L4*Z--+psBofx0D zlrEueK6^SmC!C4unI(c|-OG^dq}=WxQ&n8wT3b{Y^1z~Jg%{Rkl8z^R0OpEt_eQ1> zvUuoct6VZv`;gn=nigw1p|s>Ab9_H6?_|D>>WyKZ1!DKjTl12OQ!;B2{iY|;{G9J> z{DX&lM`6t}vr#;#E zKWeda>+k9ctgon>9EHth$YsUNQ& zK_-8zYeKDCq%RSRA~)z5HTW3zP-HhjcsFr1Vj|S$vqdhQ&Ff2ZdVATgbx2#hhb4? zyBd9FGRO~i>Qgi25U?)fCS(P-Ie=PLNWt$Fq&2i{YB*xk%5PS<%4Hq(wa;P}MbJTi zGoU+SK(~GUetrqJ^>br=+**5?g!Q4hqB)>7NUg8~E>lW&%v+(Ym?`8QM+SgvfBUdN z=+X?-wDdpXXPHf^_CC8!+cw@)HZ6-nSGO+JYFj7v zs&Hrok!Q2btD!EF%Yt%6&C9+I#Nxzl)m=#N(ueYGKTcn$%o^ZP6x3$z6VZeWo{D!_ zhRwzMccni{dT<&jT@cMbLQeUOPc5g1-Jig@6cNy{Y`Ch0-fwf&x)7+6{R^jhvWHGr zXf}u4zG*Tr`;6^lBYFD3Q6KxFdSQ{gYH99wN~#S zlXHPlxfiY5>7W>xrwvo=^f49xI@~E*#rnKyUy`ZngLn&3%?rs0xOWv91*s`M*!pK_ zMobER*~jdfSk$Wix5!e4y1NtnWG+nOPbj$V4){hPT-I!dbu4OR?F4G3T#%L)sky64 zrd?701RLmAefC|LoWMW8fepFPMFRV3-He?`*s6iGnKv*ZK~Ge{tP?ON3@syl27nL5 zbAMnC_#wK}^uKP6Xk^7c;RbBMSIep*cs4ue0$G))O@4x36+>Jw+ z{VRXpH|5v;MY1Vbel#SJS!N>;QFhj-3vc;pm{!T&o(KAaQV;6_Adc_bbUy zQ3}W_T6XhlERb^wyf(meCrMA>@%)Q~@4+!DOJ3ts5|{jYKmB@GyTTBoas2aA1~QGZ zF)~3B>9&D#GSYn@QwF@RMU{IP_p%CuY7tXs4 z+t%VASx*J@W{{un6^&-ETE}Z&J7)W-`tBY{&{7YPZv>N*&||rxYJThr4eG zfIJHJLg=Uvy@t_dhUMaReN>QitN-B?j~brTs8gr!XxiOoC7UiTA=!??-tt*?27 z-1C}$Sit4KOyVLIh~^u=OCJd?!sB?m7JI+|k9M%~R!{_yP7+I-Iuh}MQzT}k%n#OV zEy6cKldVT?m*5z;CQ%c^C3_=ARGyu@drai8oe+J^AQk0&vCW_2nyH$c-7_S>2J~Ga zF#;(1ETU737w{v4lIrP=nn{08zAmcv9DVw#e)m~+(9P=2<5DSIn)4ni?< zHEg+e2D08|k0-3G)1oxX3w#397s$gj7|y`7d%NM|YY-n?$yA`0JBxGmUQz z`0)!2xf_lxIa^m&`Oq~PBHVK_AKjq4?V&VRc^D(4RG94=ddu5mI+BI?L}HME>)}Mx z_rUj$9RutU0VY!fca*ZDP5)*>@N1v2SU`B13H7b@^a_Q?cLxz=VFURn|DsVcHQh5}dzW^ds)Sd8 zCdyE2QP|~HoC@r85Sc+1PwGmwxSpAf!w*%mx`bR@Q}JvDqqh@BrMj!rlcjb-$=`$f(kg96 z$AyoCyd%V};HT#qV%;S*16{K+@AYbg;l%K-;eyBYRTh6C$M)i;{f_>A15dur!WmF% z)9YTjNXMHWdHw5PxUNQN(cNAv5&~L0Z8D1iQpsWUVJ=k~>euTbxuFN4p?coXGXMfm zBaPuxnE(Di`KxNnkRI)(oa>_1VYKaC)m=zJ&umOe3f+u@Ud`i}5Ej4|HthKiJ40XM zDV7A*P+-D)Z~$Vl4gCxMqTD+4-#>xDdjL{ldtg0%%x+E2Kz@J38bw%ZL_>Fc;HBcO z^*!0`!tv{Bsz;)qav$^P#IR?)VnKLhF|tAgEUr-XUVf?Bi!k&`bBba4$bZ9J?Xk4F zdjtOBPH!n;qdEye%G5V|G9G%doBN$Vet+i`58{G13w9bL5sF{e6F?bVSGC<#(GmFU zoO@+!-+Q7~fom-{f8kT$BZXO?XeFkY8cmRBS9K>Z)eaB$I-PBMwPUqcN*I#g<)~ zhw`tuqe7;xafA?41sG@JVdZd>v1e3m zG!^AZQz_b$u91|qy8Q0b@(pDz>Xj1L#RNQF1AQ_1G5ZM@L1bfS@u;}_OCy|Gw@>|X zZGD2e)!7FJAc{ag3?(=7;`_r-C|X%7T3$;s=}!F6Hvws}*m{f`QA!vQo+V{g15(Y( zSa6ZvU$A`-VczyPoP)5WzPqb-YtTMp;$Rl^O#Ajpezf&METp%}6I#^js!!atB+fd$ zYNp(ICeI5yAovvRIbM2S8ZZ>zh9W}Mtpx6LW>4PCBU-U%udp{pc%}t9o(bW+_n> zp|_8f0<-G0ad8fVj%DtPxdy>EdN1-l(v*}Wx@*@(IatbdJxU4g4*P<_4#ITI!6 z2=}fI#}hy-!f|#&&GAPn=(?-Ja7_?XN|931lXZv$HGEDzZqL{x{gl6X3yB3}6up>Y zb>Yp2dhP7o+q}9n+Z#RIkNaE>Ivzj2Mmeme=2AIsf>%vgu50!TO_4AQ!5tl<@rJck zy)hGZM+_xf?!?Q5i*Jd|!wNaVz(F#QFih{n_#iQgGSoCplpD+H~sd2WUu z70h!ErW-}-e`gSUtGo}gQ2(OI+1>+GEdLySFrm$QwN7ZHxvww^GH1Z*VR76A;zhJh z8=c9*G>L$lYDS|EI*p1>w$EVq(D^r`(FGx`Jn>~c`NjKX{zO*^pub^(xeA5-C( zBDS{bi}nomQ_&spY9Qt{tNz&Vsb7SKX^CRGCY%e6qhEDQ@Z>lgTP`7U`b;m&02rKT|< zY+i>4EKS+1r8w)T#r;k!&c41=y_xI!jB@N7F5Q<}2gf%|GUJoOx;+}7D10V9D)1g5 z-9GKbZhiY{gSyThD@e^*@Lhz5 zG-rN>bxypKYO~|iRAhg!R&5Ld@?@)!7r@xkyS4=MJFjI*4R(1yS=edG*Te_~{=_h6i)AqKVmbfa zQ@bW0^nRyqt#mP?5&gcp*3TCorVmHDDx+6fjr}X=HBnF@NVxccN-IVGRYk2DBBR%C zIy^PD#D(+SUp4bAT4RK`Krun_E&R`g-4A_w?%co684+vQXLEB@hzQ@b%-}z zGpsz=)g#zcj+NAUMAyva^7RJg!(ijqSeAn9>U2D)=>}kjd7(SmIYkfJ=lFDMSduHo zTmzIIol!$~M7$Xo#B6`*x~(L9!S?a~o&oQ9h)z)X&R7Jj$c4%c_h=TC60p}$eo2XbEF3B4c zUBjOcyUp7NcESmHciWn-{RrKNiRFijv@@xBo&-^)8KxP4cnTZv{>VqV6qLXbee9{b7*2jj+_7vV0v3!mj)#xXZbLKIuu8EY3m7;*xOjj_3?r&1b?czXx$C~qI3V}_k z7&ODG#CCq3xPq#oSX;Zv`>WYVi|0f-I=;1Xqf#i6{C^fBz^q~uqMGkvksMt8vQdbr z*OQI(t_3>fAiR${Tdu?yR=4_&j#x!!^?NZ&oXULR>4(=^?FL*kqrRyvm|9-5&r7O&n2C*3=C_?Y_1tETa{zO{ zKlHD!+LfnRd}gk!%Y5u%$9E+wl~yWt55~+)Xfu2 zXf1ZP?>eS035dOTKlJhkEYtJkhV-=|iq;Pr98Cq}dD{)+8y6UvhLRtQ*Ci_| z8z|}N7s*YjB$BnfI5)glH{K`(q3I(rKIbogCF7Par7~y{5$_kia^as~Xv}cbM)_oe zwp^V7>UJq2DYwJ2aYEy;FQg;{liN|A{D% zToB|VM&PufH zOiJ{tkln5cy3T`w=-L1%*uLVq`H10LY02FJ_T?JeE@$_#3-D8tlQ(3$ZlE@{-l&ix zxlT}8wT>K)ZngF%!WVjG(r>3)i|f7D>^OBvY`G5S(2WsOSa5o%mQt27yl9&G4W27S8ycBE zokkrdh2ecda;ne#IR`zdfsafBb$+pgA}ck`n)@AEsh7x-Ym*kbGhHJhI?2`Tz>7g~ zfayMZ!dBC=?=k?R8c~HN16HtNlrhW6ON@)6hrvat?&^!rpMQ9EDNk3 z>!S!>tDA^k#&hVjTW#AUuL*aY8LEU`XaV)oyVktbnR!f z1%rS})&GdZJug(m3Wh!Pl^Nn4y*|Nl`s>%HS^J%PFp!^0Krd#V@+IIdZing#*z; zt)2p~gdc*z=TD9cIJSD6t^FFf`}($@A#5(f z07qH7%$`afFI?<1IwJFmJ-x}vta+!0&_>*KYB=L5kJo4VAYN?#tyb=B(fJETv2(kU zK{Vk)wAxqlh(wy}YDc9+Hn{JuxiG(Lg4YmPc6_0)8%Zx$5aae5rdu`QY7})e>gE#_ zqLnB&=V$H`Lz@}CN8PZ=qK@NK-VR8KThNcW<3*hzMQXoTTYO!=E4dM#(<**MEaQdi z{&;{(N=PK#U{4-@kQEDUO^7E$TrW&s`tWR)K|7}#!@0=j@N`o*{bo@Epa z`?~EvO6r1PEYQLU!FcYA4&~nP-;9*(m`-z4%RhJsI=QL>5+1vyU)AK6&6BJK)=*!- zz{NjPJP_)5%)oV<+IQOT+FUsi|I*({j{Srv8nLsZSXsA_#*rpKp~hVpON%^Qr8?Lt z82qF7&yW@t;p;xMr-a|oM9}v(*HpNSau{z}b@NbT_1eFbyMtfwdtl=qsQsI6;*la* zjdpo0QNP4APt`<8nDICkT04U1e*rHvFG?s^LS5Sbd5w;fUd&i}I1i@gs69hq)Vwm&9e#`W>yAMDO!st2Y>u=Lm7dh+)5BDX%| z2ksL#6My`e@jU!XQ@H#NC%Iheg0ooUl7LPcnl7L0b_JdZK+gE*SMoNfhe% zFu;VWH~APtKMt~9YkeG}ZSmN!Ezw;Q*UI*{BK5P8cavA2f4HG{ZtH1PA;lO&dPSw9 z!NZoPqooSN?BSO#O{Gkwc52^x&YGyyo006z#Jr5~b? z_qRUMbAGK&ukmZhAW)779{k8DUvFqJKfhvM!LW}l0~>KUPu4E;q2$=NOv|s)XHY0_ zt+_X+w_Lg~duy`&fyQ(@YtP69u9yCUF}3`Z%Q!SzsY3pnRL8)=tD#`Lx_;Acfc5aS zCgtXOo;p{%ZrS0pklXq;pxyvK`E9+}!!h08sZ;*6Mc7&nQAQHR!INS?cW414h4p-) z(>=%CdG8c{^CB}R!Zjkn*ZN}C;dm0s)WZ-#l%l;l)yS^vsN96<>(kgZVYFeWQ{AKZ zP*fMk&||tLUz(yBVMy@8_BBB}s{!%h4zY6n4tVSHKjVe`(*%M zJUv`}uic2s1z9|=>Mf-%s;4h5PN^LDHg8yrQOHl3op!dq7Q zbdakyrET!731d4KU;2wxpQ(Bt%9!#=(a6f`>Y$S7!L%a}>8eaC%x+Y!NlF;f^a8nH z-$EU--OFBoG>A0^YXo-8yb@B(-6Q$vox%rSVCDZd*|0J&HGDl&X}ehljS~|%_#aWO z{qzOt9^K)ptX!9TQC=z+h6U{^S$S5I44-w)Sll4aVwB337UR;_V@6^zL)C`SK>jcc zljNDE70f-Yu7z`SNl_N{Z@d1ms014C0aYisx+n913*yFhdJZ!avv9=or6Lb1`P9X` zFu{)X z+XJ^o1i%KUSS)&KSX@8C!UW|O{4rEPA@8T!+C&sLqN;YZ;#o+iPLr2z%^9{Px)I-1 z(A!Drn4y**5Xql@v^_!+aJBkRxj{*TG5;7<9lTr1;luqBkr*7=@01*(LI12swqvW& zeZ^T36Y^%pH`dA0J9RwsBrzCcEn2@IRTX~Hs*u1}2c0Jcc&9FT7vyw2a%sH?n4$V6^j;lWkF|?mFIy zY4o^-#}UgjKPq>)!|Amz((e_2YB)(5D$05t-`eh(JlCB9eP~{|je*cru&58}HbC5M zLh;2};w1b}K40g7-mp(uQZ&jH>yff>r)DW7%SZ?aHewP9=1A;a$4r zp-pb5FO#Gs((cK=&?ExohV%AC*rKFcECljZRtz4R*2{~vod<5gDrIDnXgobx=KxuW}H#H zBpOJTaBx0=F=YQy9GlTbGFRDzl+Spz0Tt8*_dY(;BeZO>$On9Er0>jBy28uQbxMQA zYDOUiI4V{f#w@*a<6QKdN!K3Ol{xpQ(~O!KpHAE|nGvMMn8V9u!v=3u!I;Z_>~fVQ zjz_yhdS%f%d{|MiLMm`so)Z^iH+-vd`^@qP?|eQ<+pBaYoM~+K&WK^^vl9-+G?7`} zm`Y|cyAj)zjs&9mA{S~0ixUhPZn%M>bqc$yeopD~j`Jt*amF&%r6vCSm%?VZQchzi zF}&L5OHjxFfqai8SW?V;4zDDp=_9tw0Bb^H*~Cm06ID4v(!s%DO0{I^xC#rfkGtswi#F0Of7f4b88G{H6)c0U zzDVUfnJzjrzLZuQA-O8^bzN&^y(CTzeK;X6e0L+yDc%xf0_HVo`8BCX0WgW;oR=PY z&SAtu~HefgR9P*opPJR#w(qo(yQbU1pV zpcfa1bDR}eJS=iS|L}n%>JU^re}3~dpw}EdroUe~#wRq(v9{6AEFZG^5ukGMJC%pr z+Rt%e=)1lIWV)V%enkF);Vz}By%&Qjg<#lSg+xL`cv&{K)?za5yjYjDEQjbNlMk>g z3HaCK#m$+B-GklfL|lkX*4bQFVk1~9t0H~I8s#eE5ShKQ<#Je(ETwuDN`~`)L`DV@ z;&DjJ@D^@K5krdQ7YUU7Mvc5acCRiE^3C_&HYMNm3~C||Zh4ULvTXKn_*Hsj?nJ}G zLa{?LN-}k~J7&3j$|>TkTDx)|H$>(YH{PK9L;#Wkve2y~Vwwfeb814g>gAAGJ#p4; z6Pv4Q(RR6%N(al-aT}4f7wiqMl#VMAs+S0^iJNVL2D$MTfdE10)XVF)yW}d7hJ0 zOYn6X9RDFuv-=+jLJz1CsqJ#JRG-34c;Pz+j0;i@5V7|=n6%BYv#w8an7aoPRK6W7 zEIpi>FP~fi1EwzybmGik=BHs|%8jv4lCd4{50Gc}A`uv?3VAO10he?J^OK>0qc#Yy znde2e7ZMts%rNJ>p61D0T5Glq;{#>CxyzIF#=KLLMl{0ar-szkm4hR^$UVRV1 zP%~o8GP5|REWY&Xpg#TNUgRCO5Bj-yB|~4tGp@+gO=*|C3U^*z4u9Oo`qfhdUIxPL z7X-o5koMhRpxR;U)+n!dHN17qB}qjXjHeubQg)HTndAp1hGbE zijQ+NN;WwY_BVougWveGUm`3zE4d-~gv5E%p=E>Mg_1h~X@c}YtmOmV+?ml$R@3#3 z8n&=>6sdpzgjgNkw!!dS8QJrKw;AoY!V)Q68>F#QTB-dyJ2;O{2HZ`sDK^qaOS3P@`vzE`a-s6pt+PFs zI)$_L%3H^mD{on$tG(dyH}2+SZ6o z>Zk!Vca^pUUw`xAm9wR5KmKa5Y4eHsJG*GxEG3>7FwK-H|I++Cskv$Nob0r1W6QDg zGyPM-W8e+B(cwfftAc*o&9f{am349IDGM)DX_G=KIhph~3fucNxcRj&YLjcW)ila~ z0y7!c`WsckItSfxPZiQ~`EYr4{bNB039*ib6&lmi{&im+bT3msIl!%L>2_dbd!(Pp zG9V%^d(XZY(OPl5Nq_0VZG$2I5<+FY6I+KYj-^8iJ*tZG$*s2Q- z%^#gbm4E=glDL4ol@r;uYWc|;dGla<;su9xZfn&V!vp@7=93z+7I)0NjW_;g-$Wvd z+WlHON$tLGG>lyKMg^qQk}t3dV^f+l@l0Oq4dnLA+h#mP?}wru-n#7`)m?v6-h&gF zCX*SSk<%frZ@S#lM5a07+kmZ)#Vvq27bqz>YRf{!s9PaVFHEt-1_lIxX(k$*b-cOw z{)N^yZ>|}kLX$SLtKU0bB|ejSRwvpuwpU~8lhW;+Ypo%%5lx?7P*w)-$NvGmH6ghI zs!JG`hxxU% zKP7sv96kRQpHqk2EPuEBW{hZFs;!wX?0-ZFV-g(88MXJ-Mk7XZWK7u_4Kg%C8g$oQ zw>7DfPq+1dxKA-#+AD#f_@F!N5_~Bzjc>*7s@%AJL42OYNkQi%&5)ZT3y)-OB5W3G zIn*u)%CgF88_ls{I1>*o>Z5%Z<6M1JQ$)Z-sM`V|&IHBBR4|nFGkO)AJNzJ($ot)+ zYk-<-B#7&(6J3!(N$P6pb2j0L_cO=6wYl1k5XBGSx3DvQS^g4m*fAWa5j9NzykK_#S3|9pY;&y%vZ zF<85r$#y{HH~FiXo49V7vJw#{^~1knCR0T%$`=2!`!Z=!ei2<1W?g3Gs#*#cT?bv|Xnvx-S3IV%?E_f`^ zW?F}7SwdQE$*H83$WtU=Jg7rLjMP2V?-YPSjDl2gu8qfo0l)N%@uyKC+YWQ zWW$MlV`5`Zoj;z7POaVFr>j3_bayzORH7SyL4zdcq1wVnyckuDeSY#d62AJ7fzx;WqW+OwI<;>Yn>))Lb^(mn+FkeQ1M%)Bmb8D0cQXNdG*Olay^U z>ywqR8r%)LZS>S7yNzjo{jzuQ=E@vjXv-O2Zs*!?H_JfADfbJL{$JQ>s%{jF5; zn5a&cl`B29)xnr_o>)zDhUAO^nehjklb%~qu_!ep=K{PrBCx-q8O(|S38sIsPyS*3 zphnRFAZ`j1YjGe5znV&RHT6hd?$?n0&u8T5O;ED%3y#_$2!q%zM~L<7b%GlAFODAJ zD#E(~&_NnZa`pKjjkF(*y(Op#>U9&+a=|K#Y2EB-?YO{9qk{Zuo~>8)F3NW6;s{?}LXD?yA; z_YLpI>0yT(=#@$5sbDXjx9y62YgLPx%}ERm{2$R(fu}yK4fpL=}AQide5Wf&|MXyHCxNLTRe``|e zFWdm~8Lv~PIgBL#bYUk0cBT${AMpSF4;swx`s8(xTU)e}|GCyFXDfg|Ala097vgju+>t-N=?@xx$d= zk18$c%6Vz+(rhPdtY|A>fvY?=8cD|Ur2J~{ug>s{;ZCs8eNm@&An~hT3te-WQi&xO zMLy|y+owlcNZTwhR{#$l&YfTIj1!I!bnei;{#wp?OuOLuO+S7$gXu>yc3XxqR-;l- zZQ&UPz&qI3XL#CAY9`d&&7SgI9y^d{oiq?*N?+#OlwVtb!lk=+O|x;Dcd!TD%;ry0 zi;At4^BX0#n!w1LvE_{!l6oRxvw5CTu|_T7ShT5Hjkmdo5Ufe_&Sj2!y1Y(wnn^T( zMc5{$@l@5T@Ya)xHiZM7mFB+j`*4BthhJo;)U-Ok$7QMN3=lcdr1qvH&SDyP25zNq?P z_3@#!`e05}phw>F!!rTa+(r-iFLPx=ve&mzBVj21d0?&f(wEwl+$>n%<(&n0h<5Wq zg3leFYdtkQc|)G(o8ffI#b#dFpYb+&6wmXgeaaaEnpb%p1Y@%8`bsBz_75Ij$t_xs zsa%_3ts4senj-qf#SfReq|4sb_?9@~;UleCOu6y_W1DAHOwQe55G-q!moQ0lU z#7u2I^t)3|>+QulzAs<)u{nMte)6)f*88+FhZYK_OC+6k*^FP0g!ed&3ft5P@*?cQo1}E zLm77OV*MZOTU9T{YhW7|ZF5!FYw>3A6`&W;5JH18(pkc~V^Al$x!#ATBCt9q;ZV01 z)C8gFJ%TvrEz~h558UfGaD>0^{ckk-PqC=e_SZP_QHl-m4E{B=VFQis>A&4vPT!TJ zyj=5m0;7OpkEHay?IM_%{h1YzPHn@$785Gn2_saG+ZH=r#7IniW#CZNIm{|4|M-4} z%Sz0;nn##NB67oHhQ#MeEm#6$;G^|cD@@8(R3Y27XM1HU#@N;KLJLnNEzex7%GcCx z-j%Ycj;q^~8ihngp0X^jo;i#;W|5x+9QsF4hX(v(jx$m(;Ib-xQS$ZnCFU8{)0J+Y z4YLx=D(9RUr**fu!aj(`+tZC!$*@{qrHWONkg68uEw9~6B4M{hJn_fH!)?*=B=y4Q zzpuHShBs~E5F6WC5_~xiL?UGh-Shgg`);Z&OMY-}@pYrg_9#Xh@tG8cPz6UcY*`-x zw?#7$DKWdXm}vaOJhdPT8@TBfPw({ldST!N)>gV(uNzrMvtu1+UZlI#h=yf1(MQoY zxYV!rmvgdS5P&6ah;)s%deop;-Zj>qpZmz_5N%g0bL$UlF6KE#FadTA;VaR2i%F^( z4EwfGnshie&n!Mc8&Fny{*-W|6M4vz3#)ti+$#=w|QH@(_n4M}Sa-B|@|Wp~?I zURJ9m>=p1SVm#&>ya7T_T?xj7>6p)2^ubL4J_L%L69~cVakvJLIDh+uLXVxs3j8MdX`^u-ZZrRHL;TG_gQgJvF-`R|RtG$}U(pg^6wQeNvu%=FK>|(TgqUdHhve7y2pu-Zj{0u4Bl_ zoKwDCj8lHg#J!TH-DB)#Bxs`^8ZvGs8b;ev1`@Bi0$Ut!w?9CVtx=i#+$tj_55Aqh zb5fwQ=l!Sdg*MslAK}UK1_!U19ZslTNPg-v%c^YUm>eQ7Mq#2V&Ai`hOy$tlfkDX? z+z7^7Bu3Y5T*I3hqJt4E;nnA*)VSM|?RXyDba$$KT>q_}66P35QM(_-X}&rFHf6k) zKr9MajR?HLw)~}GmMLM+ZESe4ld69B4u(;LF*E~f62w%kvxKLb3t)!%F4RjLi`GK- z9f}$WigTslUfu)sIRHM#H~I)f2nKUZMFIC83UHwJK(gXT?7ySf^l0YvCrCll3v-BS zDSsH=^yU2keo3Tf%BUqwEJu;{K(&QC_G?@}{#>mR$Ff5SHt<`qxsP!2)dH;nVn%b$ zMQd+jkA)xaO>HOdcd%*Q`Zj9RjnIK}Z}rq|F7xq>Bx>vJ&GV5m8c&Xw%bpbVLS!7m z_hyJ)nIB)uB{$5&J$qlZbwJ1&l@V8(`YT$jo3+5PwGk3boE{yfDEZk zo4)RB{^5gbq{8eUS@UYf25jxLFGyZew8AQy&7S#iBiugJ7}WYB1i>hmlg{C2hiGWW zf>rJYGN>G{;i{&*W-&k_cV4n>Cj9h3{^=}zu1^X`F+cb!slWp~B0*Or-nm2b%_y7b zxJDl%*Q%^yxz_S}PRt?4kbe;i!FvW_P{Ng4QYJFS8ca_c!-XkHB6c2gyjHsRq|#lt zD6Flkf2m6r_BO+Z3?H#KPlm|OBPo!Cp%c6I4APUVg*-Tl^C{XRd(8Oa=ELuDP<8ao zzz}8~!sw~DScY%>i*ZIL%O}E~G;ceUu(@^IImud=UgHCp!pHLGQ||y}+J4e4xo#pk zpkgXlMGgB$g5UDt+oH29W=(7X?h7(^BOhFw@No05jLNzZF7)^Ov;65XU)@*%iA4~w zn8+=V@H><8-MM0Cbe8%3Y&FlTmt`xoSh5gOMi8@7}@is08qaBgWMc z5$Py6CmMlq z_Wk~$+kKPbr}2(8{lw2MJ`HP!wM3YQcVLf2a!mK#Qa%;II73Ld49auDdcR z`Zu=C##p-P9|$v>RfRCD#CN~E;dV#-4nUxuFtpN(FW*^9OUl&Tg|Osag2m$vEpgmu zqTJJCC!A~|%&R5%7wS1T-t#QH32&*^Sb2UL+wyUsPHi^X=&AMw`3ru79Pir(H}54d zC$Djnd9>ud`P2Gu_!)BIR`si1?k)U8I8hUxd^qj-$CMaHxs-Woq=tq;h+>8fomlb4B)7K z2w}{9K;|k16+l7~_iv#2b-?$IJrLmqh`IASOoC3Dg)m0?ucjZ`5nDaKt23*3 zG7sa?ZCY5WR(h6po1w<`AsB#!q5wzlSW2@^AVQH_hjJyl;ZU`dJWnO1F6L79{GD-R zZbHk3ai;d}wcBCoG?Z42;{y0_kGC26t~XLr<#OXjRyJ}T_NU?U;eEtf7{ee>;p+;5 zzMGUOx2|a2d@29Dg7$+Rn2`S@yx>AKFHziSdF&*$uYaXU7dof>;X=VjkL09g-@m{1 z&TMVKUVgNCxnxFN__o6(ef@;xh7@h%*{9exz4MaJzM@1;BJR|^PRfoNw)wH``usvA zE=Dy8l`dVvx3+sTZ-fV}^-^=+(rJ|1Bt{aGF9y;zDMq6d6SNdfavIFdcqVU~c{)_# zmc-sxt$Ve+%3mz^wWrCsBiEL%Teef7p46VY`xTVffS$ahC4;ScHM+el=TxeT6WDv# z(u`lqXgS$3nJ1636|vtsbtjWStF{+^ZH%HaQLDJqEVGKSqjy-;ve2~mJu;@X{`GMD zi%cB3dA009iiRV@GtEZr5p$bdVUtmpYBn{Q{L}({=b$sq^-uki>A>P>?ZrQm!Y9eU zLRxFfu-AfR^O$^Jo9pGbP!(%V^-^}kGjR2XkHrW1m~*voQ6X|K_*{lNwMV>9gT);= zQ8cJ#K=%S(3xWXv5D+s5#Zu2{Q!0=YYZK@H%p=m|7aFtyEq+4n<>yfx^{b9p&%de& zkgY)?l8xtL(OX7{wJxZ;-cPW+BTb#7Uv`XWKMk?o5)LLWfiZ&Y=wCPqUGPuh{jbmm zXpRz4>L}ZSwHYdH8t7>}zQLgpkgLr+^f2?h&cqnK46oFB4Sz+5suSD&BCEo4f6^>& zOn<)ny09OlFmv*lTEl?ps;p({7~Yd4*8J(O6Iu@@pS>L#OxWwQ6w`ECrj<$E&ae&< zIEQTP3F6yzqfqai(--Uod6_Sq$)F$Z_#8ouoEH(qibeRA zlIzah^2%@bS;@3>V-Kkm^{{<7Jxv3KYmc4W=NxBq?*^f^>SpDc?^e|ZOc3|Q;D%F!UjD=a5 zWOAjimaHPE-gqF__^~c^22_3d$9&@7L9Na)feMPRAkX%%l{M?JIpP;yqR1-Hd)8?$gyB z&dn2jCv7;A(Drr`3DX7W!fsQKEGMellE^Px)=F|`zByTwtr>^CgLQ~JanLV5RA}gW z?J+~e;UcrS;*4~i(iJoMD&jC2b|?HnuhI@VkVSuexELzEY#pvODG7Vygc>I7;s6P3 z7Z&T(mi{%(@U(Gr&5)9t+f6}AOu**Gsf!NIPARi)AMyhQr#-AW&OA0q)HQcpUFKm# zX5TWQHG-M3_0CNz4jEWw*%$aFu~IS5P774BFuY!Sqr4SSLsQD%!Q6C^qf9UIc0Du0 zE7`qzD#QhUjrTys$|sHU(KULUT~+1Csgtk$6LW?O$hk;oU47O^ye*a!zjQgauuwF& z$XItIhEHmLt(l~{rqGHGr44|T*Pw9YITWw5s5xmh-;`QzzgJrES5{FnBM>h$qk1O5 zn1$RyDX7+0n!9+UUv383aCNK*mOkZ=s{9A#`&qY_R#Nr9u6OE58sY;iRF zW(hZFOR8kT2zpnFdMjGF8a$l#+idc55ndc>Pk~_Th0XQ1Soj>h zB+9sqP)_@8OI8gAKQJgofiJ;(*mB-`vgItMF;7kPh!_~lN=66ZXnu;a=!kO3C?kz9 zDyT~ED{6z1EL$tE`he30e<>`FiaqL2V<#9Yn}+{ zVjLHK&2(OQEKfc#({uRdZOH@Am|)s3hnjExJnMXJ>Rzvh`}JCy%{s+f!6qc0UiZ^vvLEqef9W4$QOwJi?#|j~YjjtKw~lQmE4AyKXq9R_JIZ}Y zIcs94Q@pbbsBt`I#!9}pzqv9Y8tCqmE$T_c$=aA}ChoeR!;;4LfQLFe%O~R8X!Sxp zwTY8L>a+J26^Zsmj+U1*1`{qalc^O|@Lr@81LBn0j}64jao=6$sgS`LM8eBF@oc09 zDF2OG0a?-b5xCyR#GOcH1-$1Vg$s$)qr>}yh#ZA%xY%Vin?tHoXC5q52$RX&X=l)G z`3OjEQ9hTaS?KSHAMY_HRQDjY^q*t0Ab*5NRfMYY`+uJ4!jb z6DUrNar9sE>^$kah_@(Vj-l9~WR!L>&rt@9tVc3ZKE&g5w>=h3_RqDUdrS7){LCpVu3!19L7T; z2tKJ2-I=IK6D*`AmY8lIA{YUVDgeJGeN zufx!x<|LY|_Yd>Gqi}7Pz%DDG)paX7CR?LED}KJgD>UjH0PjeV>~N zd>pFp5xdqO8^w(+-;SUiM$BRkV2;>ZY}aWY)@;X`OjS#wei4))?VbiZprhB;#EwIh zi)wudWrf!6OFE`nx zSsHh-?rXUdJICZm4gbhkVg4VDJ9(Mqlwjfk?YXn(zxzE?xSiE)VK%G4iAX_5v?`6$ z>TaeaFP3i6a)*aH}tM+u;Q@0 zw15IA&O0y~+DxQjJ{fu$OyFx6cGlcxv&LLY3&QIP5$Sz`{G!eAYZ9r7kKb8?0=m#f zrBO(SSL@z6hu1A0L_6cR4!aurDcVS7vqtrslRelnB4!#XgNCk{wO2Us1R7As8TR9`$|fTG4=x>3857LDT3Sk6kg?*`)~ z*}WJ9^9EVBabpWuTZdrxBcmhJOn8ND0=xR23q$UwlF*U++50^1N&rR`gjGj8rvIN< zti|FVqe9RLUp#Nt%vVcbm{J8g;z zgdF*`5zwYXOg1W=r)|lO`<`PE|&#hah3{%PbE#q&8 zZs{6!3|D%qQ$66nz$h_8Z}ZVz%g;>2=J9(VBiny^Ic9k!a($SRWHu3z3U&!Mi1KF(q7+RDmn* zi(}iss=nZBEGUFu5m)e#?VQf>7!MoC-XCSC3JYEjrEkKH&eucB%wd0rj0nz`*~tE$YaRLLn2>hyI;^Btjkk zdV&pr!H#`w;}z;|1tD&A$s@jf@0TU335C|PT8i7 zLCoY5+JW_Y#puSmGI_{Vw!1`HYOJ*xa<5PFb)u#&Fpf(72nl7D!k*IYbN>?~`Tbxb z|E=GERMpACTmjQjHQv6!93Z152vN2T)s3_Cs^R0sMhLGL zesvKn%40E=bSCw#?I7yQX9S|tzuF3Bt|zH_B5bK*y8BRMI!lAdn9jZsec^>Sj2}yVkxiWJ8%Y!j2DY$5VGR_*Kux^gLC_rGlmPb$ zs;?jUxnI&gzppxC|Sd+N}e_P|;yz~L) z#>9|u1142h(hY+}4=KMqyIc5?)1~u`a+5FQi?^C8+CWJg@ve562W&UrXpFy67QqnXiO@WgHc!5(Ye@3E@?E1FE%40?E#;u;4u+&iJ@3=>ZSc zEl<>ARo$YP_5Oqt!T>*X6m@_9F*Qdb;?c+y5BIUb=5ENw&vo>*vQDjJfj-E2X=ZS-OT+U z?7_)x1hXcVGQU_GGgz9o_mno_)>_10<{3M>vpij~NZAGk41-yLLXB1>SQtr~c@e3e zGRyHyb0IJ@jncjpw%kNgZjPl&PU5d3lGMp|gE zm5OB=MIVMbBdh&i)Oe>;n50T*`Q`ZHq;091$`l@qnY%`(ZqinV-z!Naf!pD=H4zN( zoQ7P+TKuXA!Ki?;;pu?}FI!Zf4BiGu_MZVLa0vt|M?2a66~FvW)gVU)?KAlO3b@6D zhTBp6^z$Phy;g=n-A3TH)=8$^sT=JTMojW_7{vTbBpWYHB6C15?#m&5u(XU^N4H@XVUQGUl{fK~d=b;Z(`W8L#^tb{u@|j|C>qnaySEk6l9V%yTl?+{o3~byWUwz=Qh)@m_nrEVuBwviKTwqC z*Ua$O1{%t{t;rYQ^ti5{$rHp%J#rV|#0U-p92H2*rVn#LUE>%pejDk}7m@)Y)q#Wl z?aaW5L-PRO(v!y#50s!Ct?2{c_LFho40lgPQ3svn#@1OQbp|j0! z+O7Q@)o4{D5{LGgJ?s5S;&Fn?aOGKI^{SkOw}7@s$zwe4TVQrwZk6rEM&_c~%&P~U z8eJ*zsm$U)>E%Sd&~4i!B}~QP9+v$S+9_wnYX**HPpreMJ~HOK*KseA)Pm*(_?)2Zfe;2Abj2MX3EKhPv-B%&!9{|Ueji!BL=%6>=qi2|K$n!E zsowu_R64P>PpyMo|N5*?Y>J|LjW-n>A!&o&TgOr(Z>(Fq)X~6Nq^mUvRckg1XNV58 zw-vL{`%5vibGhHKWBXhfU6|Zl@umei(=I!q{pmQ4s53mv9Rs5uxHpaqy}Bo8^tE$l z>m}&lf2$fj+$(TVP)ELx8cA5?GL$BHYVqK)NltNuG)tn+lE+65)m6zW1(PGOik;Jj z7sLyaL_4;Vf#&=IUpTp`t51|rbpj0*MMV0Ku7(wK_RyJI6v5S=siuO+8?KJ&Objmj zG|;50$DQYZLAkUNcdwbFbqrAE;=m4$EdjVEW{@56t!wL&X<)lo`I;R2H)4l!S?a$B z*9`ctS&-(Y2tfjbFQE)pm^mXnOC| zw1g_qClw2$#TSVtX=&~uSoSlynX9k;NyAeSv$aWL#Y!~%@vKBl@(=Tq(k-vX{0^=V z$36G_V5iH-h3-2O;K5-M@oaWnw8~RBHD~L4&xc*Tcii6(iVJH;9JV*7nYypA>16KT z>UX+VG_+GG-aP*PyB+Sl0!=D4Ujh*k?A-*fh<}U+BR$QnTqA*e#!W7g-sXDv+w2?X z67i^we1+P#ktlNTd_!erbz3|7>o~ZlqC%9_Y%z+jPtT1ri&JQXeys7r%hsMGKNj`PaB}tEd`ozdGn1BV+$|^h-ON-`lkMycG9F$GI`w9%h0O-)X?M^lGv2`?!y9dJAF=MN--`(BM|xaW z_L(O!Uzr*4=1vqp8-gdcklVQSHZos?Z#hhzir3)oq1&0rQJ0jsAJP?vXArLyIKNhn zQC-V_;em7RJi^U;DB2);SJ|Ij7=_~VjLT|^vJXXGAD(cPhj%ce*E*FU_y#BUY4wLt828krrlGuO> zoQyVL`-9rWb#R~!*fzT`NG;(fxS)ZRbl|5ZRt1Yr3=Ts7)b9t{;P9s}`*axWXvEG~ZGD5Ky%Dmy8 zjcE<{w8=eOZ^&AIo13i8vE-U-ww2I);QG8`0P%%ovi&^5!`B5 znF=QgD+Qg?47kVA@t(DE6HLnfU8sJX`XUh9$TW_sfved)kLWwuK@X?A8t7))h@NMx z)qfT4j4w*|=&lKMf}5%JW`?HnX)9S$>T}s**}?gDZPZy*rO```%u|@lCVT&nh-0{h zv6@N2`VaW7ga~HogE4y(B1xABk78DG0`jxRUMLk2S4adz-B9Q5wq|;YoQ+{yM>cgT8E3HXv`I++qlk%$cgmCG`y5?{?M6T= z#=3S%x$H^*vVPT9Kg6L0*9v&14owX-L#MZ4Fp1jR)SyL^0DS_BY586)= zYu`14p~n-Sn(xP)&@FR*nl3*iq@rzruho-EHByy$d1Ko}ZfNNRR}*)J#sSytnPFsO z)2sZD0OOtVOD2W)zC4>N4QBoS2>bGQDEIb%dk6_5d-fPh#+Iedv1e~Ew`E9-C3~oB zEeD~DCA*O&>tKcnSz9EGwbfpfC6uJ1O?A$BexG}W>YV5MyncV2Wf=E-=Dx4>eO=f4 zO5kI&yV4|1z;xx)(oM(q8Y*jgbiVDkd$MussxLI&d(W#%l#3b0Z_Le@uM}Rn{cNvF zJ~^t0RSLad%9bFk5^!Q*At|XxK=s9ZT!#COv-Wd4lE6l{*|X@W>802v41<&O0J29a zxhFL>F=^n6a=-bl<%+Z8K8gC);mxl^-VxHRqTtH6IwQX3(0#z>E`lBn&T4VtA;fM- zw?{oTpIYk%%f6Bo9d}xD((zRI?$R=1A=0R%5UHps*e(&xQDgST&O-TiXhzLybWd<5 zbQZh~9ykKfh8Zw#G|vHYp=*-Xkjb-MT)D_~g|Jr58l|N9buohBF~mbDG)l^-y+}OW zDAy+-bfg^Y6Uel5)f(v=59#m|r0z!a=uDx#*)7|RV&HYk_njV_5gQJ)_nuCP1^?&9^hiM`61OON528kZe zBoh@{ghL+dfz3^9m>iIj?ERx(T_z!~V383Y@H~VAU43@y#R!J0FSuBel=&vg&ZFMF(*Ms5~eN~;_q zJa4I!v}MZGExfq9jbGIa@mr-#PLBS^`|bUnV-F>F_(;j!9}T_4=UT?R*IMX;a`JJ+=3OX#E9~2P9Hz!GLV8N}a)6MSglf_-pIpl7rHR_y_7CwjQ;6^#hE~ z41h}~h}NueLWk2CS7ViYSL~tTG{Wm!<$dFmQY)W%y)Q^6kJ|f>nHw4%wzlN-xPen6 z;=Z5B{xdW4DhmN?KaGEiL6IP3TKL$G!HAVyV?X5aC0>GHEV%2XjYd^>dhvOewh^ED zV#hY_6wmc@6@~UI(kToMF-tzTkc_qf`!!XPmohyPB$e7~&M7nMCP3TkPXX{gE^5U_3ug@wBi}LuEVEkW+_zZZkM%N(XdJGz)uPw%F>^`Z*;FWjJ?ike;dqSm=cr*cM#bu9$btz?keiNzvBSc zO8(Oa!eD`7Z3XL)tr3?;xHgNhG!XQ5BN0gDsZufWDAH) zE=De8G49ON=A{b?`X2J*K{taFq=Mwq`Et-Kcpo1mw{Km5 z*;jp|8>ZH2RiMci`9y}PUL_GUDuMt9Y||nBFcw><-5$DxV|u_n1m}6q|8#Lv+b=ogVp!M*a_#z0q|yoKr8&c-kI2p)1o7Y5vw>Ov(b z_3cTo%a*H6ubs!AH>&i6*<(9}uZl@aO+M7D^iz5gZU1_c!+VqWM5hs!(Bm;AVqq0t z5`AV^`?2nE#u<^ZWr<*uOD7vvDg=)qs`Zd~h5Me_jn&FlJJsAbpB#JwDO&7SfmXAU z1EsJC-orx<4jKA6Rg+Ig3>bH)CP`zLGT1cgzGk&7L=uBg{wqDhHH_(p4^5Q*iL>ut zDj3<_CmyOX@JCC=iiB~r>)1{Ci=R*KRBhnC_+l+WsM)vxGzrbxZ2T!0%Ra{GON_o( zoD;^rVN=ltiED@r-U_O1`ZOSaMg77k!tP1{Iu$Z{y(uZ6dF?DR&x)*JE^{I5*=1|Z zn({a6*}{}3h=39KJUt6MsWn%W*BP_l@+n5CZfN`2o$y11l1{ z9njE?N+XCSK-I|_Vm|%XOX?_Rt$NNuu(I9M_<$M;V+LX$`02tA=BJMUdD8@lX|^ij z@I!ZWwnI}%0&ZF7nXK$PIOcb zibO^Y`Duu^z$f@;5=Pr@hZs9OgCpgIPkQ0YA1n|^PUI&km#%2c_}VHy_8XRvI--2L zHP8azI;Q)m+s$xU!#Lh*J|^w&KUq;(?J_SjeJf6J9AO^ia<)lYMKy<$$)hnM%NVOv zoM`ivQ-4_H%N!^ zUiD@N%#{Q0eLFEBCCfb-R>-Yw%Qqi@THkS1gdFyp?k&+<^H28hWhv(JEi%-VUznAC z`s`4nV_a!H3KX(D@giEp@3?w}?z>T<_IJ$KFttK&A_nq2kO}g!Co)=xOrMwx%zdFa zWZ9bOf!6+{B}A%8za0h*3V}s`9`tTTK+xskxS9xnziG~envV8gfwT(kVt_A*=Af3;HO5uC<9!EHs7Z+wP*8A@k!wm4{V!dH5 z_i#Y@k)4Po0xTQRfTH2YKU^NoqHIINut}j$U%-at|Bz5F@WaDD-0)Ux>;#R_;UJ%=F%YHpc#B`8yvt&A5-eC3j+SrkSA1J=5VV9nD<@q1U`W`I35V^Zul7F&?Wg}MG*&|cGDQ(YirmKY`;yop> z={1L_zOplmi|+N~?lM02cn)Y%Su&}(8K;Ot;lm4D(eev_BvlZ-Ce~Wx3(;x2sCkzJ z*)X{?s{w@<4D&_YrsT#$+aq zAH06uHLBOEREV=2a?I%>*}Jix*C7Wmzr&Y}M-7ExP8|;d-s<^}cAS4b8k(gcNi0iv z%_0mQyv0MNHi~$X|D)!x*uT!qBMDrbaP^61e!wH8LHx8wdkr-8fK-b%JWEgX|7Wp$ zeLIH&l%fc`ycH*TY3BmDFM8e8Fo{$^o~8M_Tiu7B=oWD6ryTs4?Y6pKF6yr*pr)eZ zFoxx*idGmO4!FXVjGcb>?R~>d=efl=95Hq1?#+4W$Na9>kL*9TVQ2fKcFgekTyc^s z{|7Xn&Cwlh0N#^6k3yHmSepN|D?p+!A_s_@BwSJj!l6JH6r0UG+)s?Q|30z9c=7<& ztyuiG>*6V1yE~4ZoyrU;3QBeoKVnTOTpQu6S65I}t*Ypc*oC@dm*0nZWRzQyBO~)^ zF_mDZPaF((czb5aq-{iA$5hegRya$gX{f+-TRNLzsHMm!p(f%J&Ue!chFC-56G=$( zamZ~&27M>g2Q%7uX^H=&SgdeWk^gf8B4WsuhK<4fCD6`D_S2F}naW4@6$XB0dpdvV z)pY8t=U^{=i;;-D0JBPlfASMIm2*uhaW~%KF%Mf6R1_$GZ<1b3lwS<+3bc%kIHd0# zEQ#D{cCPOUzuxirtmOEv?9PwhHwu=*r4kP`j}+fT`-*+g&FC(4ef8>QrURj0u7g=z z9_7mNN-@_}%O{<{M+5zS(P;l^z!7lKpW{%NV@ECm?3Q1%0~6IvkLtr9wP=x4WQv1b zoU3#@=}|igEDWS2OR!%H^SD_gg%O$iD2#Mcx%)0Y_e=@y0u!&jiQ%w{9q%D+U12$U zyI3g;r?c9W(}x-@Q*KcVh*QvRvGcloRj#8H)=*2YfSW{CzdK+V!%MiWdVQ<0`hQp) zIFN_ebHSzsglO>M+<%Eb?IkVbr^&}JzlTw1+Alz&P`UGVXk0+TRXdEZw`Xv8v25hu zp3Zk?*YCA^fKBuPxG&HBRQ&~m!dA$I7qabl$tEp-cFjj3oj>2Z%gDRBq-#FnU-ebh zDoeiOphK7MZ#K@NJCr1Hiib}6R7_)!nCEqVO+MIXcrRT_!a81`{WZJj^6r9g6GKPA zQqI)4Y#$}=i3>6o3i=80oCDumT}4$Qasu?`%>U*uQ0unv8_Xh9eIeMeUvr!4XVp=E zkkA_c-R`d$OUsh)i%SZIsU|Nsm8;D+6a>yx4BpaUR=w77zrX68&jj|M;~Wc`;lB5E zVco_)KsXkcll9E9%;4^Q)hQ2XUk3z=gM2$gakU{8j56~u7ow3NQTl+^>WhS2v-<1B zg+x=%^`~)`fUTigP83ISc(~5uU=0Oy91FwO`}JVbs}YzsoVE^WMdsjFF325ah$q3Q z{^Nh&X}SZMfFBKM0zt(*Md4ckhx#2fh<}%UnK*+PIqB~OwW~)T;qIDSKVzYPBR#)} zE0`5&LAe?}tWiD}{&;7gdL?Pq9Wy01n9lcDxjFOs*Vwjm*Hv{Cse?6@#9-4|qIar? zj>ALhAiML`mdV=T*syyK9V@~T8dxm@AMcv&+%0NYtT&8K-{>ru@c<*EK7LtatC<|4 zidv%QpU5BZC&r!f%8r{xo$Tu=CYHQ27!A>_*`YZq@V4CUt9cOI*YRs9e+qmn$95|E z0BST0U#{X)LquNkv`dhZnLPEWBcTlO{$8ENyo#`AuY{MTJcp%L#I&B!5cO7{`TIdf zmDb-QOq$qp-w$Yjz^F_oI8Eha?2LE4z-@oGuZ%MdCTC{Bu* zH?GLPutVHT*4c;O^on0(*_gRTx6+4u{yyQJiCV3Xo&T!8E*M=juu%DQ*4q3F(Mve% zxs3U%wXU@c4=axm}IXA zslpaFLMH=n2ja>72!K7c3kYpzLbTau$9(bkKs z5rt-5V7Uas_fMk&m&M^&9oV8Dsn~Yl1m3(IRt0XQWr0JrlX8yn=skMneR`uR>FU^f zwK8J|2O1|AzGLeaNBIaQgqS-TaK8vm|%Qz#)%ex)Sae@bv zuIJ92X*~>6fxu0WWDm-+E5)Hx!hOi+7`%sa#NSZJE2f_>+lR6wavwlTl*L)#^olYq zY+VkS&Pr076VbT(y0|npWJMT?A#oL78{_79#}=97t!h#tWKZ!y=bor$5^XvuxoFCU z#Pi}O5tyB`2!QAPU$&oq=V>JYxIf34ls1}s^yWg1#^8C$z(hHImYF0rg+v`1yWub4 z59k&IT|4Ppy1ZRG4+T$odzk2#rwY%*x`BrK7*BR(jr(8Jv2oIWW%(D{D4V2E#_#o_ zWX_L-*aovOoO+2;Il0uKzhJ5*{B$EqQM0D6-l8%e8oy6j8&yp;c9A#t)7n>rxx#!q z+=NX(J^1yjHIZA#V@{;HG94{@ekk!Nd2igec|t=$P{eV>?D+&I)Y-y3weRV?@!}^G z6MJxnd9CHTT_zIG%c2eMYx~(nf(^&nQZ*D0NH?WHi40eG{*l$Wg<%d3x@KYl8HhrJ%1}MXy+Fl zzU!H*iJUb`9|kJ5V;neJhAIR!_1?X}$Je$?Of|U}PYNi{+gx@-7WqYX<`nI+rrzKA z?BVEQeZjNDE)NYGwY3Gi9we}su%RzUVZ;|1$1dcVG)TRSS-XI>wfq>|cOvxRC5Q5{ z7xUT)X{()BEbfx?lh1B(no}}py1AKEpE;M(+Wl=|Hwi+AnWw z2)fh{rmJ0XU0D%g;3^%OHmJn*SjR(5jA=?yv~nZtk?3o{X{De=eO&EtWd{d(M$nO~ zSzPg-UCeo|2KF!^9tr2A>$EBaxnu?O=dqf~8Y3L%z{J-UbmREYnuZ^zBQItwj-OQr z({zv{0TIXr*}M?eR15-^N?OA%!^)I3^l2+!F$x;%s?Wx^40ESm2Ae-AoarN;$NI*e*)o;Ek+>QK^3K>$iHWI?=8JL_8 zb0k|swie@hMS%Xt;AjBpf>(Y&+^{1na*>d0ST?I6Roy1y(&cra6Bli>N1OMqZD&lO zrF?rd2ksBCVP3{=u%p_FDIh{uK5Ob=t2oN0b_gnk3sC{7#LReh)Z%<9@4xl)59up;`#5%_s`q790Vy>@nv(V`#Dcn z26@Y4HI6AKbSy~9@JXuQwAs%@P{~z^W_lNg4xe2Q0Bz$PAr`+)k)~_oYfm-^kz9Oq zqkr4m6z-#V!R5p1td!qrVoBb#WOcfzrR)sy&4I2&rP|M;YqJQWyXSX?rSiW^${jtj z&N&CU)<#hcXA#B)vN5u_3mx4#$H$F1#n_$js?rw_5cBqg-;dPyU(a(pG6J*ldSEsl z?baUjtbvqR+2`V#7R4o}d4*`Ya~(EVFj7r3Ty6kPf@z89J_lfta8mhe#2B>Yn^*a(J9>e%ZPZoKbA9*?1J^q<0EjcgJuXO?j0jH*}7cL_1r z<%fnpkS&m~_^V7_j(d#Lhj01W?!c)M07NS~?03Jwt#;Gf-8u75Jzuuc9bv>9$MbA0 zA1#;IK9U9$t@QXv=%-r*dvg1b@x_Xr$V;smYD8q+uw8=E-VZ#*pK659q!$!i00U$i z#;FJA|HE)+5Q%EkfS2Un|$xG%a%B9n_BSY|X^IT+)w z8TWq2)#Pdo$ORFM%6xeujI&xogf6X4iel_Jx#Ho(z1fws9=_BwH6>VN;B24Xo%Rc~ z2)_^b1{iFH@kd}@envN7X)?*zLtBSz?}og57UKHCb->FoDKFQfeb?w&&Sdy0!eni5 zJp^8VjL~d6Xp1cSRCP@{%*p2bZly%@PPLq|cW#RAvZw=Y9Aul|#bq@$CtCRxd}caw z+@Kp_W|eC=30CY_RNh#1vcvMKzgm#1dypmi9|lw+<Mv7nyBu9}I(9xZ9~$MskjRX6Efp za4fDB^)Qh2(M7H6>V%JY-*_?D^!5x_#a-q|HE%gbZk{jpA=WzkI*nW^*dDSuct1Uo z9lWM+4{pt2J8GAmFRqg!lQf7%pSs7Nk*{o)sVL-@NX*Y-GtG_zJH0e#BE-WmO|Sn9 z|9GPfP@S{Jyot~uk#wJ!A!}7KOX3C>c3DG+;DE+c0#rLt9Ies_yEcq4bR7~Y{nSlq zckeIjsy7)HT43uz82{yNm9#v|PU+z!;%0am*Ww^Sc*?Jt`Xj-G$FKfnTWBv~va>&O^oyr1FXugEj!utYdHWK}HY z5pGj)`4Z8KZ>Ba56!lU6?htu_FC$_XNNT^c5s~R^6$s-Q>Y%Odz6chF2WKA>&tbz~ zyt%V_|8wC!-fClVkJ*S%PR;DuVVOu~e7uxM^E%P-so1U{&W4H3C-QPt45z#Ma@nT% zre-O~r&l;Vl;cY&PXvS2u66Q~we3MssO;dQ6P#_#qiP?Fo+WWyPZ9pgHD;^&TuwAs z11yy^pgL-nfu5OtZ%`Pq*5!~66w(mCdQMAbEy+QSL52^s7OP~Dh^$~V>K{YD0H`HN zxJ13RK{-|sqUJG{EoM_RyaTL6-0cQYbVzesj|&6@P6+G>>nbSH4<1tYkg9o>Jj=(2 zcd*NFyb1{D1wDAfb=4Afs}BTdyERBsYQ?|wCP-IoPD^G#HKOB3nlRYB0o& zIAA||K+*y_asXBg4|&2d6HOHUS^eI!8^C&u!D}-D=!KFkiD`ZyfLW-CR_zi;VOT~# z9P&>!_ppvLA z%H|N1Xdu*}P{avp_x1~2r+dH{{cuz=ped+oi|GQC(;)bi{2<4{uzXhYt>%Z= zjH9nN%zH7*r$S>_n~oa@7d&iO&A&978qU|;{)Hka`!ogRNDg+(cF}Arj(LZt%n!A& z5$biyMAI)4wXfKFT3!~DY|z&f+C8u^*vb9dY;2@U~yJwpoxVR-#jf!w4nG9K_i|WO&3wAO|1l zu^U?;?Y^nF*$7Q?w}fHzOz%h~V)KOML-i!PYMR>wz#N??I~8I|g#rimc;Q<`+DY5p zng0knVRHb_m(c9Tc02?3M8LBsSY{B%f)c?NFWe6>8FcNynH21BV29R2D}R8PTL<`= z_5ahv)aB}ONwHr1I%@N2?OrUD9rmI%=wpG76=eeVBJJQe`OP@-8>!Fp3+f+dHm-c} zF>OZ}B4UaXug{0~8_!!?_;g$~L)XNSQAts5*K-$Ymg!sw{xk#D!nm$bdyng(V+K2Yaik zSZ&OZDQ)MC5$>yYmkhm1d`C^kl=aj4j<3Azc2h9}R6<($St$3MLmH)QU!rUAn)NL8 zXmu9#WD~8JrJ%8+MQhkaLN2tK^nG_+cHl%i>01OS#+xf4jY zi-@|~D;b1_XO|xPKDC*ZJaFI4+2KY)_F~3vw;ojkC3g=sbvys$6?{D>KuKyEaU`-J z@|GNG>n_vHwit}O#o$OPTHs&1LY)wHw^HxrIwU<|6;2pI^6>G>N$BA1h=+=z|ghj<5Q({d{cTLVphN4?2Qt%YTT8dHj?JgpDveTKh z5jpX>H{=PQ{q9WDexgC1WvMp=7?(Zq^-FFLC@;bLdjD4;;lCvTB=wbq)qY~!B>w*A$;aV>UW z2zPMW3#J+S_)zxX$H46Oo4#x|9%3LLYNS?B{l%ow~do-kc%W|H28K{0%@Zw ztr6njE$6#!i%b+N!*+a~heB`lvAW@8(F>$xb|wO;G!;Y$%0SBVjK#NWiJr<^k1AO!? z8KpBx{UbbqlW8!YuzxH#p~f+{X1H-Et!3a~q0@D3Clzi5CA(s6Iu5A$}!SVc)&%n!0HRV|AzAXW!Rv+IX_>1>&B?%eN=Xh)32mO$@YG2=)&^ zykQ`p@r1-vknZhjU{dFCYJhL>w3=K4T&|#*v;PCC2HP9Zqleuf+b5e>a5b-;5QfjF z17c0;o{}H-s;>|yUNwAI={%)(7DQ1eo$zT@nc3K7i_mz*0|@;k)0T4LO&p(5HD^eu zdUa%JcVE6jbr4q(!y2B#$#E@+15EX)K&;`iquySDe^dI;tz>Eh2|d*O4}*DTj>Flm zd&bwhg~hL5=b9LNz@C^|(jjO`=0TDgM-lF9ARnN!yMd4aZb<^1@CZ8%{syX(Yyu!r z9|6OSDV@j)*@K`ZGDB)FGZ$1jDs914)9f|LO;XqDBGLRjE#7h_Co^E?tL!!6l5)cb zL|KtVV8ou?7&9qfGjJ?}=xE>3Hnu88t=b_FLQKO~HjEcubnOcSs zVHCLNvl0i=mF+&5ZCgb19=`;kbYbd&%RYd$2Z$2vU$Dy;5JtDU#2+9#e3wQMj@)+m z(wFw!X2^@EjVa(b>z6M%K99Q$@_gy;5rbP4(d?^StagL!LS-0JTGJ4SxTXh0QaP_sByEt9{0awjfaU?6fLvEO~Qj-hTXDW}cnk z04}H12kVrX!Z68lHE3dKA`(!X`llGNy6I{6)iH;P{kV#!ETEt+2cX&D+ftAVVU8&vs}S?7a>#8+v=h)lFWVmwf*iG%6*HlF zgc{kdjUR8fFDwJ%%N@j$4mS{)NCX5Jct{0P%=Mn2AiM>c{uBm!UFMf(wrwYLz)@+q z#@6rPq}`WQ9&%<<#LGNd)v!LaQZ7GV_Oi_pH)N#c>6D#cGd)IMRbEZL?3ub# zvgp;{?!2WMCH4*{V@ruAox`VsZ3#?FKqr&0vCb?N=ZrCr?xPwNw!d;vi08fOEn@pB z!yo2kkL)p*V?J5S`pQ4APjJFF``GGG?sgs8Luly>A>~cAMp<>}%NmR0x3Y|^_^t1y ztBG-V%W)Xl0P<`v3+*NBsdtH#qVEpriuPWHhHlD^JG82Ukb7DOi2ZXDeCCln z%@jF`Pt6`~d(#s0@7$`(>vtlE7Xy~~?+bM}zqhomu--5i2)ul0)Ay$8HJCu~V2qmX z-@o-iJ{8mu22+MJT$4u>4-Q`yVjM)JGu*x|HrT+5u3+@O$yAqR5Z_K>+xQT>nWls< zTe!ic_#!FS==I$1Pp|Es!quE6pc5G!KyA-Pp9YAduQFs2b05mX)_{lEN?n zshn>?KC4JG`lEnOs@7(Vb%y!#;UGzp#9WEgh*uUK-W8?2&$cn+38+hN!fqxx}a_51PK@`a1m7ZiGP+_Z!4cby)p$|@qv zH&0BO`ZI9dvBe!hFZWm%xx6wj96j4xf^(}`VAM{cqB%jqWCm}lj2S?dTH*l7C->HY zgVx|e=`$2Ww8T+VK*4CBHw5Lxwg^(95Jo2+wFFX=?q7>w=~Ro0*!cS@1fK&F0yeZi z+<3DslK3G)$N*yQw7{+;a#t3$>LA*?*e>#svQ!z!GE?(V1VUKUG85mS1YznA4CID@ zzBJJFHjD_&1MwozNzgVRM0>76WCJ)y+j?TZe8_gB3B%^w>FYQ%0_!bAHbR48dDxhwG$o ze%d&_@PbX(*39tS)N{HN1rz=BygVA zwf@kjiqRa&^to|33x~gapzquVmwe|V`}?1+HTow+Hx(X5NijkO5>F_3M$sMR3S+UV zP|zzRnhWpWX#jvl%@+uVz&LAYm+V5(H4Xkl8F<9=qQfI%T5WH=$)ES@+-{3Su z2vq-m?ji%7CP}ntlO6%U#W)(f^g7*e{I`z=h66SczXl9&O$JuuUou-7R^SX4u5Awu z)S9*L*;HokEEcvS?mUq|`~x?>dR-J#0nf1X500(2l#G0pWtsOtxIl z*rum)wx9RXs5I3jtDThMs;Ko2Pm(pjkw^eYXNJQ9>#oz&2)n=*Fvhg}9{p$`Huv@; zL>htQ|xJF;Et_g;BaGl6pI zQhT)R%b=m%v})?ZxIrz=(uv|7XOSIe4~EJfu#7zt1g79M52p7Z+E7dDJSCeWmRBT* zcMJ!$E^D`)G-WGc7Oi|=5N`ePo$@nx3Dc;mGs~(r1}Oz8=0~($tD^OG{*m0}yMEvI zsEg`|h%+_Q)ma%Ai}n>DTII-?_g(@s#iyqAs@?F8-@4|ZCQCL@@w>&T2r$*StWQOM z$|89xUszOm?ytLg^LJYXC}}DeV299k_ZLi*0QoK_M8E;utRNEa2A%&kRih2qY%5%& z({6k{5jly%mz6B$O%suw7U!A5FeRT~m5-nXHw|5`!&k*k$gj7@`TO+M07blzbGlS26+S zqp|I>WDjtObRdoHZD9+zU3}WsU)cMCCqjQv!14PsBaCKjkNHd^3@z&|Yjq&M@Sd}r zEdq}=aby9tm@ru!d2xjGE^9Kw3Bv>d`3w_YR`&}`c9J-dLO8Bi!{10h0W<{t6>Qss zFgkiPTvG_z^}2St6VZI*d**i?rqNKWu-S{VfVbG#*LB{rl@$tPC{KyAPxBgDtyM^O z^eAEOwA`0i^+IR`j7tKu|TWtwOjeo_kojQ1OKgY9b$+m*m zc7tM-TK-_%GsF1dOBRG=OeDp&DBS<{=jnc?J#4cl#MjZ|x+mgP(!Sv`I+IFznB^$b z<2P^ui8Ug};tuR1%3k_ljq@4Q9lcc%cJfw#{HCwG4O*vX! zHWt}n-M7FuQ7r=nMS^`{u6gbyuEc31hR9N%@hBKy>YZs{w`o+mb{ZAF)@8S+quUs1 zay`96P(k;Fzi7xNrCK3`#d29G)z!fbvWNh48x>Rt zwJrI6TY)G*^J&irv3fA0lc%2RGhUf>~x;jlMfYEvI^H(&EYOZZfr}gpu<4^h#=v?6G4iLww3W*(D zD5Hd>t3-2w-F`o6W}?O&Yt2O6`rBA- zbA%&aja6Otso*d)gX1HMZXmksYdU zy1gX;b8wdyh@AG{HikMhxE!v=!)*oNe!=eoCx_Eu5Y^CsEe4hQPa^F7zjN7FsJs+U zS3vv6iQ>qP6m2T{T|~wKz$53kKrw%ES?aSbGSv#SyO=0TazRlkv_Jr~d&9Gq{^`j4 zqeFprtnCG-Z|ru{?7d<~NDAN*%)H@JJMEVFcZ%&{?uv`2+>5I%W%jxptbCW&q%Yq( zv4Co2!Ai;_pIAfJj&vfQ)(DZ-w+>&AtI;3OtsRy^0t()}8Y?DH&Y9(RM{>tshq?0G z^*&1ba6Dp#lxy_mP>eUGmHg$*LaFM!wcvv*Y`Nl%#-Tm3(`FpMF-v;O+;7#nm%XMx z_~D3IQO2&Io{~Bq+lWK^{&?}m=gNtEk7nk0{GZodv(esfyb=xrV7%t&b%DWV7Nfo$ z)^W;R?rbd0&fE`!K-1J&ZJ~r_c`m^7n~(mmIx0}>I?XC(K-4ZYsjs))Y;<&n!kjpq zN~xIV?l!hlXVt%4O9iPIj4eq57Ta|y=Nh`B-OPPM8Y4!ra&-t8%+ub2*a?B&nIdlyhb_e8za+{ z_B%{+V4e!=a;D zfM5O$vO#=E76HS-c7Yjs3#0?~Y6v$)PRCJAvioMP!uM#4Ck;}^Xm$lwhEs6A0>~mj zO|GgI-@F$Q40r{KYt%h4V;e%~Ya(MncXaIm?_v45TD;q6Tnh>~UoI)+V@ak0J_xO9 z>N=lzw3Ria8sX>EiJU`$D{XV|(J0y6V5{Xur;;1AL*=nP7lfN>_=>w?DtoI>!T6}*!+8Z(|%I^r_p^obRyN zr=d&70~-KU65Q`mJJ2HZwDQXRj9m7rllui;HQQ=SgpsETqdC^54}iAO$G+6=IAh!D z{i>2XQ(~3UkS$fY)W?;6=0nvvrgtP1hu^zM6fgE=B`vN^rh6=o!=&v~J%`=?!?4#2 zIr>o{*mSp9;Bmq*`Kp+aVr9hq8DHJpHv5KDkUE|=V`&=z)cwP~!T2)JsC5!wrh0sJ z;PH+Zm&{$OXPRI7lz*RBltO(ZPckD6g@njwlaLn+hR@0%{9mO2DiVRKDV&sOoqAhV zOdD5souV7WmkiK*cq1RY5g7=+8||sU;s`fd(J>}CT7g?)3N9mp?ZMJB0@W9}wT)L6#J3L5ieeUI7Z|!oFSbOF9^qDO z0XZZtj9xLdl#s3I`tUu#cjjf~MI{2)|Zj!!Ss=-{EU=k>?sQ zhk7AVTwbZX$wEvjGO4BU%5%qqDKZg<8ftB1EWOe2vX-A8YZ3=`IlZQcIQD>XT68X; z5;w6l6Tx8Hq%tAlyb5+m$E=^j*!P6h+Nr+WI~a@4VV18ZE;2%IVm}EzZLN(TN2{nQ zohBm3EvUeSJ%hI<(2fOc1`LYPzXiQVV2XSc&ZvHb=D@yecbS2}mWZs9Ss>v&fQcY| z0$YV=gCMqzP#scEueo-2uF%?%Am}u)Kp}iX*K-PoE-+=VB_Q17kvEf&08?y@pkib* zQXy-1&QU~&Rg0(?pqKrIk58$QEpC&tO+KbPF&%?AGkn%4VUpe4}=3**X*qoRp$gXHk=HM3zsTdMnVvUeE91 z@4BEhv@uVdafvD@E}H4OkGasm{y0|y@HfKWI~C$SxkoBH8E4G>K|c519^vM@d99(> zaSJ%@r&{-W6YY-h#w?Gxlrz_g`11{EUXw?n@2~IWn)Du@1#B+n(nCQX3G-OwNCBn9 zVc&0RjB4o&9fsOZEX&m7PAkS?dU3}nyHMk$V`m=*vA>-#tC{5_xwRR_IjUuum2RRvG4@^TWnHA# zIaG+@7ukT#AT7qAwXTA2j2`iUUJ&4gYQL2?Q!y_yt{@jkfpV-o$XU?hCQ4mTY z_~rt|MX7NWU!M;WUbaPrFvtj0LPDvI>_#o18L$W8Mz)5cX!nkm#lrl0=-ejDN>;#4 zqu5U6EKgU8Lz*E^B#oavV$+Dcoiv3AkmFI{zGZj=Ki}M(k+_mZYN|e)Mtbex6b@+E z;5ai11cvk+@z-O4Z}IR$D9y;D@qF<4$mI{->#e-sHe#x%-j zeJ@5!uIBZNw-i)PK7DO=Kt{bRHZOUJF?}{VxulHec+U~rJMvZgO5R%7Slnx55YzRM z<2e$u__Q8T?M9Gg@rjo49K5Ti{2vC>F_x6cvyYehZ9j;uWg2}FPB7JF#C8bKC-7Uw z#2hmmo+#h(b9$NAhw18@pjOxY;mx=)D3(xaDv&ql<9)pfUFsd|PjMC&?1`kZ`@gKR z&}2qfiM$yww`+JCO19F9=pwD}IA2a6mAi0m1P@Z}B-#5mN%L}GtYYLd6>EWl^b5Ed zgh;zy2k)EK5>G;yOuK}ZzCvL>1yA+2#1@q3@G&=T1k>&_w&oDSIr|VuHEw-dx5O_a0Dxnh&&@~d2%eSsi z8|L2nWbj5%8a)sG63*(k6$QT1*1>?8j-m?_jhpEo8bOz_fB7Q#D*r}A;lKLFR}Ir4 zWp^U0v7dBE`E>%I*C1g-Xh!W-Xw|6?>bvY%&xRS*Iea5=0|&pGeIYOoS_gmpBw3lNo6s3~g;U2kh)lU}+nH*ck5dqkVG9qd8$TFb zL5`z<^8}hlfK>({%UidDuHFA%eW~BWjBt6K$~jvHR;SJ_0C2fX6`4PCr+QstiX?XL zI=;UH)T1)n);uvW#3_W$!cr@85r(~a%L(L;RHy-@fP)IwpzX*feX9Ivv~I{V6ein# zO!x@pJ32j5O3oGIRQLy1}YYi&LGfv zZ{)4brug!o1;m|MdfewMJaYJtYwUNmK)>`_UkO@h=^f$d!>*IR2`Nr4jz`L=Khu1f z&0k;_%cD48nVoUgVo)<5CF6V(Iib9XwtuJ|9_Q~Mp}#p91KlWw~zHtQKIEd+jD{N!o;6} z)V9h@(dLl0w2bVEFdoJR<7-T%8zrw^!jU3*C&Td9()Zd$wJJKrt5+m>I#59zzo}jZeeZQhhW30wrkVCExqxtE+v}c737h(Kr#f%J(~F2wj!owRkxv) z?Au;Ez_CBoM%G?aaeV2X^muN~!Zt6#|{v{AZ)lATkZX%$6-X=uXI90tj|K}IIWpEe7T zEx^vQO%kb(4LN56!p*%YL6F&LN)N(dh)INhl9S>as!(w%gw9aN05Q+OEU_q?6dphJT7h8FbD1s<+0$^7WCsBSQ$hpTvnwE z=mb!6y4b?G)HbBMJ-G_!-CI`jR{#(Gq&k|n0QIzOM*HhxGVd{uoxGo!wmFl4z>| zC}mGdlY(MD4Pt9A8R&Zj)RALJMAm?I8k;_tp^+ybY=}h5+-~%=m$}~|9z`d|ngRG! zFv%GB=hs9Iu0&7$JTDD82k9axVPzu#xqRT#E89(@D!ueloV$5sH(x^QBv(bsf}Q1K z)B9h+H+UGb-tOJaNt~$hMu*GUy^p^gitIp89p6)rb`!{FxS&9bd3>FE*h~>L>mmTS z8}dS*@gNiVL`FF<*6JWkY1%KJjFV08-6ntgYmcX{k6Ii;Hue20d)2GXUmJ{c@Y-|vxH%D!LM6FDyE z(zCZ?QUYhLTPP-}!#_4F+fj8~wqv-9u}6L1m0ZJpLD>#zdfz`%v4Cbt$>!U+mC2xB z%_+;YZ(@Q2JKfnG3`dy)@-8B#cZ$AsjDPBy#H1y=6i!Gv8)j+ock$v^AE{;iC|@3c zmKUcV+IL2MNH>vrtoY5kPFfMyMiN4==#Lr^3;xdCS;#mJQKVR?b~A0|7@6 zLXi9Z6yghnNpCe18SDXogBO4Y0iA#aA2<*=iQkq=n#%WfPDId548+9{yADWfuZW;oSWDl$!!AV)PBQA(3Zz`HR{m{wr%9dx zG0)My-=HT*yQ^9;s1lueSq@6)E#yMhu_5n(n@AjSKp0z`y?_hD$mTDCboE-eIrZr3 z1{SPke%W3iJN;(y0@AG8>RlZEHy!;sjFAe;NoW=Hje(HfAK&cE9^Yh%eUdWpUKhSDRh}3zhsgU)J3Y*~6Lp#xn@acgGx_(2Ts;rkO4kL5o2A3$ zEySx~;r_gozrVU|T>224#35)=^;IRAJ2Y^OAgp!n!LZ;Z?uP&d&%-J^&aq;BTccBp zWBYcFd;a1&uyYPH?D|z|}9l%-3H84lJ z4K9J}{IdoOYtXMT8eO}7s2%-&`~!jmSAZv2U`!8oMzA~GIvw~XssDZWC^*y_z)|E0 zWHZA=EFW_k^7R~~-HqVtkMWM*d7LP~qDH!HX5sYE|8+>UgslB575wQgn<~M~MGngv zfq^Q3Lv|Rn9o=v&l)lY$uCJYvMLRS|&%_JWVw;?qB+w%MCYBEtrQx;ddLMKv+U` z3esC3TUQW~vo(tN+Tm~kTzq`VnloAiH@L4oqvAySpgqZ4x86y{-1WrSgm1Qy-H>4G zoAYujadzmZ7dnLM%N?H`HpG(wwOXC;dOhz5^_( zvk4cZhzJCbML;mpR1QTzP;3cEkuGHq9i^%?Wf2wBB!Gah6k+LDLS0}NRzS*PA&4$T zF;PH3Au2_U5(}27Y5$$?92PKt{(J9}Jc-EJ({|o(X5M*czAa&fHs_nC98Us6z#ZsQ z`0G_+nvzh_odGW6C!4Jw`t1f^6ghVZrEaY#y)jhPtwYObTDDYapHsAgvxA`7Fh|FS z9`aYDX8k5{f2UD#35jP1)=BnmYta6QVK`vdcG)PyT9NvuOV7gSiT!E&H!gIer`gl? z!&5`MK_B(&%r6~dAc))kJO0KYyyQs(vCi>|31w|NfC6@5HXJkpR z1`_rSiUPulYbR0M3;&KF<{vHA4Ius7n)ODG=Vl(-kdchlHhe%lo6oOu0o5T@_fJq< zJw>sa6%(xIcV}x$e0~7&V*JH?4zMJc(5_>AH$#5!XuK`TjtJ6P0sg-rI)PA&{E0XS zt+KU*Ekm6Mr8^|QjIK(Q7>WB1mq17Gn;+1VEKHT)SA5VSkWCT~ie3l8AiR%-u^s8h zY6<3G-bB&TKfzY`I4)lChBoG3>7zNNouO)9JZ{GwpwU^DYXaj~zbR0@*ea4F$h20q zH97n39gcOLppT_bMkVf2sOuhiHT0pq_|G))A@nZvyYjA2+BUT+hq#~U@xwPhV~7_x zR(iGV3mD9PLs2lR*tDW}W?Ll1Lv!EX@?-vNszc-8A3@^Vt6L|=l9$?Qh)?Ni5Bk9q zki{aR10j^le@sCC8N8I<1RXR=VbGOXbhd$Nd}|omxTJNr`$n8s40=@ZB8M5YX2S=~ zi;peqyqtUdZFhXfEOMX*#ltwV`UcFTM?0=TE8$+*OA&nocPg~yO|W5LQedw=zL*xH z{)dHxs?#+W^V`EpZpjrsw^bS{ow#=czZCm;TFF{IA@7s1O;tV5E~=w1&-cd-N%x~S z3Pk1x;9p8*%fftWUc9TxL~}@_Vs3|4Bta6?ER+W zG-KQ7?9i0~9$npr;|XISPZ0%|V6#(h%i&GmOHghN|Ku7%Es(X@wc}-9wzRT(R$ksr zXPS2Wnwk&14c=4QuQIf>6;HYj`uj(mxMHOh9w(~p5FWaOxq)}yX_+bZO}$E6^@ZLQ z!KJ%)>+j#{e*N+KpA_0m-VGFd-G9^Qr`47ei6537d(yhPy5K;Xty-^c^k9mJv+4Rp zo=Xl|nC{piplGA>%+sZ4h3zA>{l}bJ?5{^q6Y|>{*DTt>t zRx}r1Qr1v)KBM)qQEQhnw8=eQ@kq-;vdhv-o8H{XW&4N3^934iX|5EPJ!@98W0ujm zY`sZJc!N5{cj+s85%ElY5wo#1s`!K<)%9Dxk)()F4IP*)>trPs5?c_mZ>KgOApp;=YtW8&?`i z$X{LjU3J=GSvC2Gk%qLN&sa8YTm5>hOZmLglo(UPB|M>AM3r`m5)uGFSdQj)#IU+^ z*SXS5sxIxy<4GKI(;LmfdwGmeldGBK?S^>jkL$a;7fJtq_Ut{cd#4@*m3dxYt+ea= zD5n+QH$POf?Qw~Xi0I~6CaB+moq4g-@y$E%9}zLnH{O^)fz4B8wn7+)?3}wTr@UV{KPcFWVMy8|930z zX@^yIT^g=`mZn+8JblJ#d5y~7>5`e+u0KrD#XI*M{@PJq_svqe&!!tOT@`19Q{Gyq z7HGgw+}gKU{o;$len<`ox+~awtGj&7_YnRa-JScWMIcWEL=aTF3*AhaU%tN@)IeWb z(r4p1)}UlrArNfQSn1c)V@TAvkzV#Q17BdhpWFSc5mQ$)PD_9!xV+Q^Z_qsIhi6{? z5v#v1)NFw7gYobZIIa#aeFM);vD=K!apz#9Hz7UX5HzICr9Q-0PI@@7dbpUvY@r85 zeq<$xp$7qu8?FHi1&OrJ_=P!OlP(n? zWAgb+86oHs(r8gYMfx`6Exk-wW7ta%R`+)js??&>4cBZd-#0Qd7?lTmeP=54 z#oUy?%u?LaY*{FeKjduhAB&6}6{+-f%Tzt@A#roQGvxuRR?P z!$8isbN(Z6!9H8OZn}A+A|?9ZqJ6Z=h>@!625!8k2DX0V?oKUOyGczdR_m@F60U~^ z-$UsY?hsxl>FVLJ=i}}&8os6=^REV+eYgTT1iQ0FL9=J2<>gL+U9Od?6a2-}89S5h z*Oxix|MsBudIx<^c#S z>t4KMofKr$#mN16=W{yeTDPXPr(EzKZ(z1*M1-eO?&zZO%;NKH(fY$(XhaB3Yf!F=UOt>FaipfmxTmj4|{w8qfZ)GSf?u}0&{ zTSJq$$ft$53R_#(aJ(J2jiyn*F{y81|H*Y~J$ifG^oT>fp?6oI$N9=_GkY3FGS~ys z9gOz-H^P!9m2%z%X=YiT(JwNT+}PWHBi1qGMWLs$U${#0wl7^_Kd?RDKOWIdqF5gE z-e#85l&rurq2JaEDB37iCa;s4gW*f;)35`?8a7W_!#f6Ffk+|L^70RhB7Y=B>*e&4 zm$8Rn;HI$mmTew6NzM(t(M9Z9Qz*O%KcpN;0mYG5xB`jAg0sbD%tVT#8 z4kMBsbWMplEGh8s0WcK<-h{-(zugF!bvk((^8OIzh+fXZ7sMg1DgaygUn-Y^ym zQIDWUOb-D>rZe(-1^0z+sXsbCj_-8R9~E(t?yr-KGK;`H^slqyc8zTG4yh3GPtG6g zJ7la->AO5sS-#+?S~F}U0qi@ABjSs;9iCe|$eTtF=)q1Cq}RO{#U2{ax%wuodmg{> zTrPDN{1%yP7F?0|Wy*o%+{C*Zdm2I}IU_8U+S@NJBkxBXH5DC^uFXaTL5QP!VT>-r zkIOnWM7gLO;%x(Da2s^kRCx7$(Q#4AesfS+k4CY_58S}_xfA7;R<^c*_KdEZ$xm1D z45ib>t83!j6PXlWGkRSJC9*$vYinH4bp_ct?qI_3@fMT&eKl(?593>&_~?&XTrl_p zM(pG`CAXahDq@?h!-i6gD?fh9HHaIx+r*IWv3aN~*fr6TM*Ty=sG_BoR@u5}35Dm| z{`VR=uF}~V+>t8R(}-Cx4zKt)#VLHdMeWz7V`=3UUP8Gy_0w`Bw^sBHGkX)v2Z~`rA*`u^EJ~uL{qAV8t~vHh}dHKm2p|$!-Tuiv-dB7I!j9^ z+k_?cb@NZ+?9ro)D~>(-SQyMez*ulaWQqS1qJRrBxFe6_D`!z=gwmAxyX7nKNTW!Z z#S*z3KzV5nK7`{z@PX^{qd787N7OmLRqTHv2*8CRHsa%GkQ5@t~V6s%{-)-OEOoxMMvT?UO5T|1b5pl%ud+0qeYk zeBMk<4S-b3Aqru)s!%wZ>(Y^{RDzx49COS#jPwAw%yZy?qQ#Y%rHQKv$zf0`fNiL@-e_ATv(NLq>9ZVZKil23nxyAZBaSI))f%WR3 zN!TN(+BUT3;#U8M>(x_soKDqY`Y@)`w%ilks~B^}Jj2Ak@?=)1N$JW>SsNpJ)KB|Q zx7vp!wK_YT`bk{H(ObxwzGB>WIzDfMh(VC@J`qQ3EY#zUrA>#m@;$MfUgLp3+YUyD zUZbD;l$PV9-rLt*vF7yJf#anTMteV=Y7c7+O+D;1V5O5h>R_EDCs-KOW0$XPJ@Tl^ zzM{84ys_IpqfzjP{SwfV&A>mi=CN>9{rAL$|7FZuZv^z{JfwtW@u>C{XD>p-!yBI4 z4-O6ZWnIj=~N4Yhm&p3kKq(C5ta z=?GQ)-yxi#u~oO$IDKwGY@6xKX?O?+YqS3D zpNdeGK2iKC+xE04`@KR;-f_3F&3>BBrV;H{{Ubi&P_wwSh=lh%<%3p9RF0@KU_yH( zZJJ*C;L*yH6x&y?ZgC%viY>L?_x^gJ)7c+owWxQB9_~LW?p5?aR5-emV$!UOCC~K8 zsh5fzHlgfJ0>*!W7<>b07ovFdr{iC2eb-qne{nGfOW2sV-vqarC@2hiMt8n*@952$ zN6l8F-eNEUa;+8i2zOMdyh<}?iNYtvP#k$OTN;X%%SZ|9;Rh%)oeC{h5TV`37r>zY8qy7E!eN zl(hAFTQ&@_*4^~NzAEFa#>JsH4X=3fB9DXIPmu`<(Z3xCQ*~J%rkJ`pWYR#}a>ci8 zY8^?x92U+v>9a)Sn;n)j9hB$|Z*Ey;*wpmc2I$`D)>?D%NN#X(a-ikwKAY!pQ%@)F zd;mVrwJJ7XX{4Cf_xWK}HJT&l0;epVFO^P+b16#I8fB=ZS%>6}LU@<0sokO=%Y2)} zqZUIH(iY6YYEOZL7Yv5z5a0uiU%mjNU9d{?e~U~51vhi{@5Q55YW!+|tL@W53%OuB zcIjjwog)J>l^yh!Wt82vBxUw*lmj2Rd_BYMId3vV010JE3qUy^+JW-`6cS|?#B8wX zbx@cL$0KYY%)9e01YrQ+hfs@3v81CT{7gE6Egw;&7b1Q#;YWY-QPF~RfX`AA=Y*H# z!Hzd!!`2j8pdb74uEy=DXTEBK0f|^2&OKMO*|PLyrG9+?CS|=p&C+sJ7B$GRSG;%S z`ySa`cL7U7`Ml>%9d!dlpn&eNMHCzG$CsPnm0sg+_jOx3*p^5A&Ah-pmGQ(lW>n?O zqE*#<>CCoOPN|vOT6-9I&!l%1>S}-VYewoIRLPi)NYNWXW3He~L9C&uwg9_3yD4hX zYs2EfE3MPyd2kwP9D>(q{`|%6@Ir}%^n%Xcw_Ti&ZB#L;XT4X@a4HVT?%k!Q6;F?S zuaRwO85xD|Q+@ZFihiDjNC)c#;h`I*mbPOh!y4~+ZbzTOpcSJ^%Se`}!}bkwk3l^l z_f#WyS0SzdDe{LYqf|MH4Qf&YX_;N?xBtZ2OhRcaIbzKET@>0 zsq2~}JNQ^u$Z36Wy7Y9^3TrXt6w$Ub)9h#mREp}bV1BQ|^0bI;=@y>)u9@+y-L6J5 zGW4L(>ho>)Chx@m1cOJ|DpK0C*J~9y=K|M!Wpdrsq;AnWMYFute$@b`dq|~IT#)A$ ze>#`TqiW_*%$o{v|8ITuu?ky9jBd%qE;k#vT`@IqOOKnVTg-D$Cd8vePeAu)`nkFI z2q6!xnxuO}UI>|EEglFHUk2avyqD0=!`uf#Mg+1Lw8bUR(|mXE*;!CnM_4tndB!Y2 zNVmp^vkBPtoZ;`Am$fbBVhH*-h<#%?H4&3iStz#O^&eSDsdAK1l^ zzy3UbLT+pJ#!xiaOrQ0Fgh!hQq1WjcxV4=aP{Qjp4XLbn7G{VsKdq`>n*S&^=-LWA zIXL_9^+c~TS#ONH?r9$tMb>zi>fWZiZKn6?P@PH7Ce7*sjSSS-pvAst9z^OI+4KdCsVxeY`Eu{t9DTZn zWf*M7VR0(8!%nl`dfCA^;k@PTw#TNfE6^%0gqk@%H5AI1jgy{mlaGWr^KCkeJQCn(JM>J`rGGlq_ouoz$J#T)5lhV!;8Lgp zfU#U2b=PD!#o6%9w!@w5JM~z}aop2^hMUqBw%`5hnm->+61G_<0pI@)ig)*M1TnPe z&NAA7miYu~fgzI)6M3+Oz-@(}(Lph@IGe-U=e`Zd4v2tLA)w*8F&vQ;0Y>41sDX)S z_5n!5NG06@rwoun(ITHXC;4_#MuFnYe`EjVa8BORA(Za<;A6>H!-GPnlkanj-PhRO z@ne=u+ScqcU&M+3Pzs%XtDw7J)1<`OD1ULc?4+$xF51@PE8ST1TqySP#~MTffw;YT z%Na&XIfW{xAkGrS-mSaQzftbOVfWi;fv^w_!`{sVIA!lv(e+WlBSl4%o?(1MKj^pB!gspcb)ve_lk zV1v)(U)@u$*(P7o`Kn9RDI>&q;`J2OU#rPCIZ^j_Mz=@>^YtjLr@P zUu8ekyk%2iairIF%4FH~a=j9{L6t^xrI}K_LwersdYPNU@+~-FzlA0}d|Z%Xq{wJg z+3@5AE0oRHq%MD4xGMiEFYCV_UjOE;?9qgW$~IFvNYbQ=W)@t;mwd z5-)pmL=oFwJa&M05tzbrDxb8VvDZY^-)n-^GIhc{Tgy^-)nJe=HU+f0%>>8s?vC3N zu@bo46+=yL7_aS{1nOh#_R&Z92{kE_7S=BhNf9~~*(?s?q67C%%i z=J-@heAK6+7dP&D=H%v8T+y{=%hV0)on@-6ie56Rp&Mbh908hlbfgFjq6uIp48RCy zZgBw1AriG-<2lY5b!JI; z{$@XX+yMVW0uQ{gTvF|J1T$@@>C*I^;HEol&sq7t*7uJK2LRjvs`rbrXbpBf1OmX=bDH6PDyqvDi816cuir= z;}eNHPV1?~s{Q&dEcI4UwW3dB+&yKBw>xW_6eb>={`f@W0^@PF>;cJ?UH8)WMwG3s zel(d^v|i9Nu;+eAi;-MM-bya(1rR>HFv{atp6J;bcL7-5Sy?d?tORKT{5gn})G40z zC@l_f#2TuM9#T9S3La8+wT!}u85cMNhw*hFGC*24GQ;6p*(^LD5kMG)X- z6l&E#R!s(fh%@-VkcZ0PR~A4F#e{HbiTM=pAD`xPjo=p1ZarQ)2Kbp&)IIkAZ6Jy` zQZleayVxR-UHd0yph+CDiu^E;|9^z7x&B=~^2k~=`NU}UD^TWM9-sxq&V7QMmu;KA zO4V^m>Zr!{TJD||6Vi0a5_sU-V4UJDp_!>SC#Hl-@jwcE$6h&{pG4QR2%JV+$mDq zBB0I{0t5Ui4c-_MT0l@f`(2P<4SLb&)jPd~0R+0|{_;0*wcX9k5mGYunO?f{=a-R* z$s1#j(IVdkZ#^1x?SObhrKx8K_r}z)1}|FS?3L|b=r#AswwxZl*5Rs_=)%D+WnwRq zvHBa(f6#Vpf}J;mGg`2uCxQ?v)J|2L zd+RF{yeUU{+Y8wq##rb<`L+im6)O z3{~sYT6w(m%Vdinrj>l|-zVP%Jzd!WK$5?<^Q6t=M7GlHUk^$Jj7~P{*_GO!$V<6k zXt%%Fuvz%=Om~C}!}7cDeyZ}hm8k4_OGx%sWK0+H+$MiD9W{xZV_#@*tvWMxD!l7l zS=%P*fe($|DzYX=CEtpArXEyJ7arQ8k0!ib%+?Z9T)KyXYh~O1D*gb2N#beHtzdJ| zyc>9mQ>yf){E`Ep*b=0l7oyPA(YUT*9o*IjH`R2+YTifE@aS~C5!T-dtVdk7UjV4G z|M`{9LgVw%5Wj+$2Du3YFVNH46J#EFO_C{v3Q<3_ zblY^+5i?IvC`e{qwFG>j`b9c4Fi4VXb>^+RZ4$(wvNWczFWdRA{XG9R1 zH5yXrUm2P0%`~br=%`x`z3=O(18Z!{H?J%i5X0*oJt+Iz)D)_7o^1HeokpL2SsT}w^kOxQmU!(ahAma3*5WFE9lyBhCI)-A3MqR6ELwczd8>Hb>A|NahEMi2#;tADb?{{sH!Z zsTIA^zlB4ws3@d3y)>X+{QH5;^7K84Q;ZW0163(QZja+ijjcoC@u97_(+)Zd8=xFn zwRyY}GxSO773jKsaCg$!nR>I@Ufqj#6U2wtWon0QT4edn6RD2BHx(#L>AdN;J+Sx6 z(Y0HTA6%DUAhl5@+o3Le(#&n_k0(Y|`|Sf3ZC1D@jzzq=kzN%4jf~q+)7N{7s1^65 z3wMM!)lhEOpAKpDi_1JAosc)SORZtlP+hh^DBs7I{$}@ZL6_tnYQ-VP?RgKsNFMIq zcbI3nTJOju5egw5+^Re^uGx+Luqk^yiif6ppiwY z0Ya1f^qNLkj@5)H2a>TT`%PGdrzuAu1d64(5I`aAnmjLw zidhb1VU9)~xj>KTwHScJXGzLF;{KtJAWS3*edvF)aUKc6-B2Z)gBfy^^Bg}3$AxlS zQqO_1wivi&AQTMv(1cxFPAlV;WeU%#(SWh(4q0P#8MQU%qJnSE!nu14e z;s`Z5ol@MvalDZ7oF6_QIf;*k4XLbld>nXac3LfrIqQw=>z+3g#M; zmop@C*Mu{wZZjszSMew+G@NJP22F*pk`>!KPY=NEy_Pp`1esq zkKe`jD8uxAwam;hsUqi`2<=*P)$ll~xad+leoqq&2tXZ#&U=MmfPvsJ#be-)0g7|% zG|c1aJO@J|clqkGDrouB@i?TlEQmSAXl~Ewt=)W!1Y;V8JeN=C5lyiCP!3Qw4~E-B zRD!x}=ZOq{+C2x5{{j%HXZq}~5j)6d4F7>(2#iqJ2ccov2p$p&2i74JkuqHVtss!0 zsmQybQ1%&$2v-Ck_UI;s| zL4-*u$D^{{8|h@NoShkBW~m=rmq8225o-^sazJw3?TfDx^D50(tTWGcw>r8?YHSzz4ecWy(DlMmDY95?IMw37bWslyr%*@E zoHY(~Gmu93kpBU_Ye?w08{``HaCCGB1gK<~L!LweNBjz!n06k@Vtxy5AK*_$fn-vJ z9$_}#BR&c3=H&aEuTmp`A+{S|qkwNWcWl#ilHK83KR_8(UgG&|aL~W0%wlVjl-1qi zTHWCy_@YNJaM$h13{lB%6Qs)(`!g!NpM(0FMYEmgrr;T$Bt+cWr4w6Th;2>w5@%V( zGA&AP-B-+j+VE6NUUuQzi0xw>K_Y6dSx7nQ-D7`w?d4GO?j z8Zhx=Pi}7*|k;Y1I$?aR>|7hVX(h_zFQE~hzUF~v(hM!B} zx$A(`9oM3thrw4vxe`Wc=6136g=s5H$9B)WD%i0?NQ3pt)7kf}nn`i6QU0WBaYQ*M zc!z@WSgdvKy1T(e?S@f5s;+d1wl(~6-J72sKlxm^pipPr+yE+m#Zy`dKP$LwQom8b5Ezl7({-huRdydX zZrcvMQu+EIuYk_#4)6hP8?gFxyxD|RVnPHrb8Z1y*S|ssGWQ8&h0UNAM2-SJL8>Q! z*oJM#KRdw!-$mLv{FbEuNGL*hkLYs*nOb5+ojV!)9z6`KvUp_!UNJ0=^h(g2geFn& zg-mn6?{?HF3P3JY@eXQM4Zy{6 zD6Yo7uS?xFJdSrivm{i@q*+Q1Yq)BAvdCN_%dKuwV4y`7_ZdCWzP%r)JrQph-i<`F zAVpsoh0eV?C=jP2-39EpRZ}z#n;YmY8JVanYQPmwhr)E9>1D4cSlNrZPEwq6Y_`Wr zaU<~YmdSYM5NTrWS3?WNm7%wIQ(PD>uqD{AC2SM&#rp@|%jhKsrXF2RjPthzNwxXw zW-BEJwE^4Zf$5{CBn0|T*kmN9=b!xGEn(A~-SkAGq=QG*!(J+|t3|P9=F;RWTP5!f z!tG=&SBg`4Gjf&V0=$C{wQi^VjbvFb&0OK+SAEK(^g~}M7Aeb3DF4>}qk*xKZFY8k zUs+7fzB(m`?0xH%Z*eW9TlB6(UDyz?_M_UQd2UaOK$@J;@jAv((5^yLhmxv6?qG0& zq?L`slN$3@``*kAIYvu;Z6f!@ooP)JRlc*KSecO(y!vFzc2Cni2D!ft*m(RdaXsHV zDp1^hqDf%SrkqHz+(e0_IxJC0%}(<#ea%3d5omW>&7(SWtQg0a=v~VA5o1dqf!=0C z*?~oDJ?t`A157Y$!A@o$gBhr@R?q5rc)$rLCrLK|`v67UB`9_~KumpQTSM{$dk+DF zE`c8=y50CO66t+B_=Y&Y7S4JpR5&66IU?};A2O68pGOCBfZyYb zpgV+=;r&3Wqt7Cp5;A_`={{#Bkn2HhBxqdUE(`^kfEwgQNa_(i2>Jl($wT^#kGMZ}XI3(M+cfB7wJCHZrvoUW(!mhnoU`PAcT-I}#X672((JOF0S7*2h`rOM? z^|*s^;As>?bNj=7KIC57SMA@D8qC~M9h-Vd`KRP_XZQQs+R&12g0Yx~&3b9Nq$ zA6Qf-i$(MBIC5AQvcaZn6jx~2ft}71VCHTD^p(MPMK}pqC}9gXw*fGsVSQ-clxTiN zZ-PA(T+u8RSgau_M_UB_cl?GHZ^T-oZ=Fo-N%DY%(=|BX-1k3^6~2{!c0PV&H|uEl zK4bO-I5cY6Elcu3bxla`7w`onVw!04xE=yzfoPROR-bmw29PmxC0!4AB2tzPPc#b; zVkW~ZeE5YY{%k%1)PUV(bhrxQrQMg&YxgoKu7=B!^BUSQt`NP(();R9{0o|~D3qGjq@oAZr#M1?Dd zH<#5NxmlhLW4c=J{{oHek)4x2t(ExpgU`C(r1qLi!Z3)6fnApq>95*@#RMMR)M6~p zV*WAA@pRZQ@U=<1d9rL6c-PQ6eLZ|Q+sLY19 zX?iL?8lfmb^OCncB3|yo2ok7S`$HEXdR6l;i(G{Wzl z!=3~ylnn;A39VK`WV0aZ?+eQJ4P~8${3MOq=wkED`6NI^$K+2Y65qbvweB|utG(tz zoZf2NJ?$Zv-c1*&D@bWHGD>PK!}8_xN4~!#r`4cNYv{ZR`*g+xRF2imNa&bVbPav% zIx37dtgSboTdDQkMDeO)zD1}}9 zx|x*SD(5plvQ$-<#N+X>TgnlhkE8+KF#%FV@B#!EEHR4$l$3*UkroKH7{eNZuzHx{ zc{3BM2vYeSh6wC*wFCxD+4AopjGmzddMpLj>?b~-^8hE|7hQ40@};H#AWJl$3=-aW2QZo-r>jDZCKMw3I=M9wWF?paK64zYb{fY0=O`IOF5KVVL8*6?YdYUHam^KgcMr4&}3 z1)?@$_ll+VI1GTYm|(5bhtb2dwWaFfCH)`WPI9os%U2A}1v2IOWgVj1@6>&pZj`l5 zceB;$!=r-Y+aqLUvI9~keelwiSX&^-F=6CZQRjHYTAOH5YfIshs&=7vuT?{6=oYJ_ z_=yG0xTs>|{om`3^o{tp%6pmyw%&i+W9?dWyK}nnd4zWH%+&LHS*wKNwHOs$6)z3_ z^jnXe*m;-k?d%9rz>b=l(hfT9xscRZDPh@$(eGq({2VBEMz>uR6kIbBByrtaJ@MAo zf#c;y??Mjmmbli-67u`CVc*X6(5{zx`Z4!KM@h}fP3+$4R@x~s`Z38YXR|`a+1AK@ z!!1XHEiMT91`4T%KZ_9va=CLffGxpg4Sj1)*=}9PD!z7;gRxqmwQiT{(TTdFanTCw zFmuRsS&vQFrzt+rSOo5N2Krctc2B}M{xRnO#sCb^Yr|pCmxfaSTX>}jYg|l$#ez+D zdEi~x!<1c81QGk0eG?H3KgUi6xFI&0pgW1F`m@)WL*Zwj0F(DY@cc(A;xTJML~(xt z>O;g|ugEfc;Kv0EsB16Hio05^;>qpEO=$Eo%ax1uzu z6$(jaKBem6O>$bEKi+*b5=7DWI#Y+;s8YjS7r{u#d1?(kG;tnua(*}A1PxCg1VZX4 z%u-j{eTm|lJ3yn?X6`x`z@jHP(5POO-@@!!K~Pj1xC=8eE}El_cB!i~bT0+fV-%OA zG+|3m$F&(#C$5iEb+Q}$Zqmzs*l~2)e0YWJ70wy;5zdB|Q^|unezVfvcip#M)ky*$ zge9~;>6%-bd2fuUy=5vxbmGr-$1mzb8~Cm;LCg&Cz(qmU=bQg(*>?G$p@Onc(X&;T zhKjfEbKKJ{x%t_1U2VpMjpNjSp@k_;Gb>Nsb};DvO>f4w-*~kgp=-zGdqkYQTqEA- z6fZt?C&_D1)lbsbKj|er@J^t}<-G7ouTZ-u;Zpn^Gj`kKk59K9+avGb0aLV8q^AK8 z-=2Nk6EP^_r92cgsJO+$UuNp;)je`9Mf5#WacP+kikx-0c%Hy?R*UI60f{9c#f7XK z8+wh=xp$qcJ$}&A-3#-7;+P-VKGXotx)$t0#t{>?1HBIFx^BNi>#)ZM9-a`u?nHtb z^qoS?#5X<`V1qtZOO5wS|CozDao;DbyM$i<$2EE#(t8?GhpromkvgraLGp=SwmL{>^J_lWof zS3@2(A4V+<^vHxBT@Y$65PlI>|9PguH_Zi3L5N74$bgGXmOg_66x`Esv)4&ZSyOj| zn{R!wELHrxx;xVXgFO+)apss%kAJg;+LSFToqnFk(>{N6bf^3Pig`4EwdZI6$B+uL zjiQ@^D<7u^tvAJ%vxd((sAV_-dZBpx}xLRy7bNFdw@RzM@nfHJDMzlCrzL zVp)>5%WsBMXpqVb@}k0j=-1s%VSqu zV83P95jN~cQ5Yx}e7Wj)A#?;u^8O>B^5eb86Q0Gcv9b)%*RJoa>-e_jlrzqIgrcEP7uO)2KF%MG=kPYE-7njWr}FTdpq z-Q3^&cx@%U4SIe&{Y!Xa0(xP4&-C8wmR@q%&Ht!Gmtn-XWrYx?SucX8OUU;&UXApk z_Kis2DS+Bx%tYR%l~)XKJ4v`lTTSww0xd0e-HT=62uNHh{J6Yn3># z>kF{UlsjGD0QZYSDhGE#_`=aY{1oFK=8703fCjYP$v}nyv|>MOD}n?TK6P{!>`qC9 zzsP*iL`KM70bYD#CeMVjgn60fze9l!!vCU=$uW-vOEp`>Tto44prNV=T#~Scxz!bc zubg-cAV3#fXE+=nfHLd`oD2W;KyXAg69kD77Q)AZgVB$0BthvS%8qy@ggS-!qsVL= za`3rJyVB^@jY5oOr?786Nt|V=x|JVKXi>WwdU^E*c7iKvu@t|F^C@$M2kzK=UJLsH<>*UZ&nU5o$oS?RJ>KonXWXstLC^ z7~mOD^!mCsfSvC&!b*SSdK_2(AzxENs%^Ek$za<@122aK5CSPBT`7Xj zt^gj!WP+n1u?ovA5b&2VTkCmV>!FSeYA>yh)AqS&@}jnP?d74oV=zXZ`=*c3s#HYx zjrh@`LWRC;8(QgN`uzC%%_gRYTnA<2VwdiBi27Zyd%fnqK(BKj8=A*+N?*knKYm!< z)jw|a%U8dOk9-WXov0HX-T(Ip=P#L-&6cg-;w3)>gzxoKGzfXO;AfV!pOPE{!%J zNlrg~1(V!PB)JshOd9trhIUw32UXN0H`+|x`E14|JDO{cHvTB zvJ;BB(4uD!r-DQk<)5`+PxH$&d1jA==l&P_Lq{RhT!WJ=L=!>>>Wd>H9pWT)vRjCR z6Py+D3-(YE_y@^3=OfTbXU&dZjl9kCK^Ul=y~!V-7UDodhj}Au7LNLm2)9B?3=DKdInKf~ec>&Ig02(Hcy${TzK!CO%wUrl z+lgxJqM-CQ_F6t(o^DqnOQU7-yY8yUMUM*N6XHtg;q*5rhl`u9EG5 zVLAAL*t$h;*1vyN5I3nPTYtOwi+uUAVd=wLWMI3Cvok?LeolYB)}$Ft4{^_lHnC%BVD3+ zKHtUXYG%&bI@1@qMcxr=veIZMfyXLExq(dKCP%EP9Xnb0f#(!Hbq2el`)U7?>nv6O z$##+5eF1eR7c;R*XF6mq^*@&w3SkYC{kzNm*8 zY!1aYBmWN&>>zSi5{I)j3evz(h>sK~+!x3Td3l^cMi20uV_^9cw$U{_2yzVB=Ynej zK`?iJ@Mnt&lqwNzdQi@Qxtes;U`ks0ocR;5fM4;G>kIHysGR`D&Y8@yQKagNOez5X z=)6R;1G%)2D#!&!4>!o_hdkiE3shIFqJZRH89Kpo1Rm)NH`f-7#WJhm;rGX^HwSR& zQle)YaDTBnD~ttJzZ8iAQ2ewL9KL9q2}>zKZElT4Fw@m$o$(_~?&W$t1?7+@pb+uV^-{TDXrioKoOo{HugBXCt~Ff@qFi&RYxp!4 zbJH?*`Z$MqHd=f9wc(+>NJvD@;CR4;r{9iLy2xmpOb;kmvsNDUU#4d>boO!4%e6sr zUuODG4aS2>_UGKne|8SfRFb^}6V@sU!cIA=_2@R;&CHeS+Z5Eg)>dtXnrShRXC9m2RHN(=^5Cn8av{}4=u4W-5nv7H6QXx#*Uw+^ z5K!lEuwi9HQ$!={ivjmQWCVOK@tM*8|As%awm~KagzT7%#*r^UHtO}5UbmWy4?=SY zm?t8E$wc-Jx&<ac6@SqtI3d#aki-(q8(546 zqoRcWp9?(yUAfPCPDB9l9Rvw*Wdi2tzmS5{o1N(w%Rv?kEuZQ5So|}3Eo`;)IKPlp z1ac01soEtay%9WUCCm6r8oivMgZrwrzy3pKUgwYd@rdaR=D><$dU?}mBCb}8hMv1wdD;6{ zm``I8Ypl|1$$M+(>g8@&E`I23pY6$-9;e~(mSU6Cebg_)U6jv_$PX+1RUV(G>sIsZ z-MHzK%1sZ)dqppwvny#Weso9f&u0m)GJ7RlzPB=~{IuOhEaYO$9#J$-nyE`U_~$q2 zm+as4ISIZ_ADS9u{5oDG#P}wniY0l{{(A0STxZV z3ED*xa2>%qwXP<#-d|K^T!+!G!n9pR!b6*T& z=%5)Brki;eD6r%erc${rlN2kRZ)R|33Jtfk$Egq?jj%1_pzHr6kau?Nu-2`JT72Gqi4V3>1?I@~9pTw{ zKVLSw(z+3^3)j=zCu2kDv0Ih)?N-?wwSlc?e~NAGleoy5`qjVu%Tq85@VkQYM8WGY zhpF%E83VB@x8GDZ2pLGc=o#N(dBDE5w@`d~rJJ4RC`rIdW@hx-Sp2%gA zu&xEaoD1DSV^^m`b3@Hw$2Q7$N>1**O{IkSypd5DAZj?gXiXkrzQQBJT_e zb3Z^K0ogmsQpnimWg%hNXwD}>&}HIX__<9dq2Px+gdfBMpeG3ENs-pg=S;7&?L?k>^q1p2r}aN8cwMQlY#O9yJB1{=xJDe}l_%r)^Dnx2w=)uw~hgrFU98 zdzS@=sFvh#)@s$tpNTZEOcCs0)ldv@OPEuVFC?yO^HQKweP5H#a0zrkDHLgbP|8wy zGoAs7?xmsagYL&~B(I=V=BZv8?zn&HNs!UY4fgLCKGQz&n~Ic;rk~a{HHO%otN8nJ zuS}JAtgb~+@%BeMb}LI1i27*_%Z=KKePp)?57@tdex$4L>@fR)W0mZW>Scz*Hr$Iw zPNpAj1m{}b$SigVD&Cz#mF}lM5p{4s*u*QBc{uaI@U)7gnt+Iq@!LMTv&w$a4v!j_ z-rl3byblvzw4k-H@=&@&yv}gE{gQ*q_;wp8#OLJks9ul0Z#1yWGB6H@gyw4rF$)+N zRz1r~fSkso6JGsF6E-lPaVYhI4OU`U1N6FZ-0~{K&MZglhXL@c;9pJH-V!*+Vgg2= zuwEldCC_5b6*|3N4rXw8~>D0lwPGoehRKnD9?J`}gQ)iTMYD<9u+A2kT7#0O%#0+(E=N z{B9eKpWmX>0Ji+xUIQY?Ni(xXAMf1>@eT5{@CbZUM}Xuf2`I)Pe*&!s;WuEwdfjpb z_K9@dVM>VNx?TtrsJGTh&fshl|2F98T_#g+;&Ni|q0n)vb4p(`==~oxOI-Fikijy1 zAhp78yDs!j!00|@R5RZK(Y_|AYFbs;^pdIepbxOuwDB)l)I&T~2aOO|$}EMt{y24# zvg0%*Hz4U)*nypa@y>PvkTIZ|+AaYFwq?gP>*h5Vg_zcs86{8Jg=%gJdlYF7Z!WQg z8L)~Wx&3KBhn|%Ry0#RJYPmR5Bk&|z-^9~cOSg;p=9`=!#*3V<3&raU6*w3Kgf)Nj zW@fp3Y=qgA^G>PmH4dU?9DI6osV~RH42YjB-G8J ze2*N6^7f^O<;F=QHCE!LhQiE(D4|7eMX=M)CSVIjSZUU~p~7DNi@|ieR`?%FsXN6LwOtohW=ZqAY9ckZ$tOs3y{0lz04JPv6ANt=;fUMM#tMdXF)kzSxMx4qER1`I+ zo1Jtr|KvXJ(m)S2dw)bQgB;T(Sm>csWZxOcVOmFFJ*J`C^dVz3@DC((ron1K%Qls@ zs)py(`Fc|c4>yk)$Cu{6?x^1TeJDc6?O7+`fifL?GRHU39HD1c>P z8#}W)ZX(Y{cS^Km5VoqV`$VtjIXjJ}{2q{WC=C)i=#DYfqF>4aiIK5^TN3o`J(0qa zxuFvHspm>@DFc#*m23CB=Y{!TY02IB!gApown|zFtGkWKv}3Gn*Kpj)5(Jv|kc+4H zJ4G-AQ|+w!U)Jz|I2&LedhAD$ICU)#U+f^g;s-^VVfMy6BL zA9mK}@vG)zdpx=w|M|j!HXu3^~_*L_VYS_+}yP(HOFP~n!q#*$EPrp=;w7e zvUz`eog5YwYIFGGyO~o~mLZiIeolKN8X1qpT!JapyYAV*qzBc&rFo1W3*6Pi9G~p) z@!T@Bcu~z|E=%Q!Pz}$^a}yB0xHg^h?xYVs`c8qpD-~4WTJ*BzKur^z4}!@oWq}+3 z7z})shq0DIwg+n{O9HpQ>4OJue$T<`d!(?2id|1B&fomq1v{=R8ylurSwW6PRbc%& z+Z{_9B2FxVP~jq%U&0ztE(H$X8idfAr6N+uJ+zqOv zmWl}w?C|r|8NS7ldMEx#J?02qlvR+(Bzr25@QxpQq0bVcjKwH##V=o)s0se->Y5au z(+|EL?b4x+HfyW(HXVDkmik@Ffq0icfNl5zN(&|?VEX7j$7hACz@I6w>yg_aPznK5 zyl)ceEudJ@MciyHy!C4Yc;>aTWb{Qq2BwTdoy_DFy|g0W5(v2MdTCVSzS^_U-5}BF z|AP;HA;NXnM^;UP8wG|4sHKG}1<0{J61d0 z7f|!fP=WHOe&nUpBU^Z5moDYAKdZss ze=&3KxctOpqt124T~pL^Mcm}Yb~)QL8=pI9P1|1%tP=N3T^q9cK_1L(<+wJlb4@9G zG09-&U>-j15zix=XY?q~d?XSkj*JD|@?Ul6%fs?zxGrppey;HRbCe=P>)Tp>J#d`) zMmqSReq(6d)Qg6FY&0(zVKwE@S_R3d?^4Qj4>(@D`_%-npPj-NVSNPT|8UtDVM@l@*Z zsNWOH)!1uJ6hbFp|GTeQP^>Y-ifCe$KDO~j~Gr*O)vnntRbJC!MVc-22? zq{KhgU_5T4qNgk>s+l`Y52Uuz`j?SBTK|fMkVdvmto7&rU#xTX|NY%Nw`OtnH0N~A zJnrLnKi}8q`}kQsG=KoZBW>?NZ0Rc7{ep$lr?0rb!#K#R$*esqCGu&LmvV|(rhNig zRz}neO7Xw|6Fk6v7$u!1xS{*zTm!o!L%EH&yL+8QoV2t|;f?>E;OR*k6N_6Mzxq~w z#M~zE;`8|(8p%(bEre|e=U4Z2N-|rG*&eNZ)_ga<+iw^gI`Vt6#aDRXBs(@KCMCF)#lEI3UG7Yrr!%P#n0P}Z=q7s)EMB!PJf%VPG zmNEJ@uZK6C`EYv`}3;N;9Y_RCg<^#LQ?r^a76mm3qI7Nh*gA~zsHm{~ zi5PXR8`X-@ZZ&W%zm=$qkmrTcG$?#wGSegO7X1kl5g0{%Pb8>=W`mS}5S7beD%~qp z_|<$&C5Pb|TPI>6zfU_-MeeCDf}NcGl!`FiZ;1ROIF+?`d&D2GKjzEP$79_e<9;DN zZ-u4CeBblqF0qBnmQIAM+F?F*tX69O3T9CO91gi>Y>muLE-{T=v2Ix>ci&G7Z`otl1{cQ7s4=Cq+E`BtT?_qC4ybiK1OI=8>TEEf5RPhif=UgegS zli}+yl{d5F!`Xew*y4AUZDg|gLg3U4b=&i8E{C?=N_}niJW{&cZLf;I?Pm0XKJzG{ zRBTq_dFWE=TlhmN%G`UOz115L8xtL2YiI z^+2OcR0$UMz!Db~Hq zN=>eUn9@v}0<9aRW(0a9>Y$AYTD$QCFx7Wfu%M54J6p`}&v$}pr<~*pmbl@2S--0# zZj3$NtRQ4z;rX|`xj|#f&C+rTJ^FBq3&&{3KIOZB)TOB*XCn3;&<}>72y#hl z;04J~ba+bqv}Yq*G1XYdLqoY&s%8ssTC6E9*wCW)OWpCJ$e-&a(o=G&L$B-Y{OK?a z)z#;c$BpxT4c#q{ZL-m|rqWa2WmIPmHU;yoox`(u@!eD^cvG8YiL*iI=JxTUoex}_ z8`tW*!6wZwwY;rLKPmnAm{8u~GOl4LM56QINr*tbZ+uF~MYd2mjT8{k=>sN_aL#)yOT;&RXqJ3gad$K6kBJfzSd(8SHOs?eeN@6t zex@_b3xA!xX6cnken-nHi)W+WiOqWKn z%k!TaO`|J~Xfxa-jX`Ky87_7L*+T3lE<6v9L zDtyfYExE5+C3wAIkf;dx0O9?q(^GR<4VkLdGGIKy*};jHWVYnTjppT6?T-HA*+ay5 zHcE{wW213$%YnW}8QIUc1taQ^`gAq0MpE4L1(ZX~z8o*&obdNZQpMabUfi#dq`Z(5 z#TE;Z-iKzyrhKjQ`H_IrNnQFm{JmrT8Q?;3jgG2CU!eG#O--zebzldmoH7MYq{J93 zUJ&Ujn2}OZ>cZ%(1OoB!=_uu_hH50l`Y3_a+TXZK#^x7mUUsOHnlaC{HM^biV*+nK zAoy>tDS}o#i(~WMd(7@Y?_L!29eKw)c4EtBr;y}KmdpU<_qu^W`VJBXPI~oq$low( zz{Y7xz%+@SDnPcZCCDjLK1R-w;3^b-IH5$fr53$3R%zY{jGlJkD5Z^Vz=TpXRxs$0 zZ|g>`-4O)a;IhN!7feXAi=JV9b#_3l^AF~w4u+ok_oZJwloYsk``hnuT5zR|ecw?z zB@xQo_Nr$-tmgZq8Id*+v(mLeuo$pES~Qi3_!t4f<_<199C2sUDT2FJn`8LiN9|&g z9K?=7j5k2YImxnK%*$g!*LZ@ojnI&&{rxHzZw^RS31NO_vjb7{j90-rrwl*9y2RQk zMR7L1EQS0H@=S4TL5m3lkxag3BpXRRQ>NsX&St_;I$szk9h;;IIL0+-fwa`uZ77nm zgG~g|&g5KZ@eRP9NRPG-CYfFA-os!c`^hPUUeY-b%Pc;j@ViPgdJQ2?*8 za@x15Aiw=~Z7_CSxSh>g-Yl08&a+x?_u~^M=PV4yyj!3}VLGfa=75Hx&^`-#q`=-g z=>g{7g59qwqLKw-C%9H?36-k_G{TM}RUlLwv1EO)2z_V?kXTXw;fu(-W+?m=T_fHJ za}n75ako4)^2n**wC>cju@B90b*qlK5`Ik;ke>VCVk8lGTd%Zr%$XU+J?5-E^T=Yz zSUJEEbzN`}Lq{Qjrwh1&yBiFx{Ulg!*V1AYxQ95Yvk7=w?#Lr@FPcI#6f82G3SQ(R-@Z zKwH>bx3h}%!qZuGFv&A7odvt1R$7uD;go?*IVGCI*RF%UVO8@PSTVl_l+g-|(M>O2 z;Roup5|7p!xz@mreWF9@U<`2%=wZ|DHahobNfUI9km^qbPs3v1^uLKhL(T5-Jfi%m zhwIhG604|`ptb7^^ZivNwenZYCLbMN<;d+xc37G_3lmm!yFXlU3F#`;XF|mB4t1DQPOvwVv-$>L zOd;ICkG{kikj0gu&dK`oL3d74&wY9^@dHIV(HIKtAJqyP+IY>W>~l@LQN$yOCxC;H zVC&Co{lE4X*+ffoUW7U@$>0H^`ndly$R3UYcLwe^7xKHf|E}<+$OpQ-w;oSEW~+M^ ztMpy|&*^C>_4}B?dchF^UEN|Ey0p#_`{#75#}sd(1J_nH!S7= zD10d9s|2FKfmB8JZQ0D9D4Tcyg%yZl2rMKd1i39+K#;0uNNFjWk{F8FrJ*H3wlceXYJh~K>bS%bd9=;Sxno)fP zlLS~+rn+njy~;e5{OsFe4;mg0&{UC$zM-A!p*>8v=&tEncSnG95Zv(f#v$T?t|5($ zOaI0*Wv#b27p`qb^4VqJIBjB=1MoWFH@-DBVEJL#lvk*38T1tcKaKwD2P*SY%@SLc zn)1f!OZF~ssd>#?C3iu_<|3x9{iS;TCx0&7LC?wUgIlz5?B{Sy=s3IPV^$w3iB@g7 zbC@Mtk1W1d=7Z5^na&#Y>WmXN!^HLbx7G1VXMmk<|9l>1rJJelSpkCpFlYU5fTno| z$xUKMx?X)8epd4CEQojt|ZCdtO^`(H$fGF(cJzLw#c5i$DJ4u#R2Jq5rQ?MYt|c;)UZ7@n40s%lN55FYyJq|RoY(oSA_D_a;IC+s!gu34Ha43Qdl# z29e}X$9Gfld;ba&t?PNVV|a!-hA9b(dpSG2LXId7tsU#-&_-*g4Lo&HXd$nUkUx+u zZ(}`ndfTH*NZh&%i6noXCsEjoVt)W|!j|sZ*CxW!RaVL-RO2}X{coZ&wlmM8yvOQ&AmQFP>kY$})!dVm0nL(YqO)&k|T@wuiKCa)xr{a2)p=+Poedm*e z1h(p1a^Jf+I{kFNPxGtp4q{?@lFm|~XDJru-8CXR-U;zbMPT?^LC{tQY`UC*SGY7@ zjqAchVTnkvo1oL+0)pTyRUFxJMH}g~g>f{&-cmsbe>Z1~DpmBkvLOh~FT$pF2%pQvC$+jWVDz zjOq8<#Xg&zy;l*s@!8gPuEAkDO3zzKo?0BS5(kroT<6nr3K2{GoQ=yfs=j5%t!~Gn zD(%Cfd4dQ}t)Ay@vASXT0k2hD<%qT|UXvELBn5_Jy9#-*NeFpDJ-r*Svl zkHb1d)*o{iI{Q04;chN%4uCYfOi0{h5-Ng^#JUWUy*64#iB6Ylbe^3GWmU=<=PqKR zeuLla_UgWQ)l7F;TR-1vKIlR8T;tXiLtZAEFJeYiX1AAhF421(rec3JbNPOXo$`V`!E0r@~2E*$y^T2B?aVs8XSW4N5$!+B<7{_kzX(qB(N zm*K9{3G=2(lbzmVjOW8ELV12Qar6(BvVj*O_?TwsgyTaGp|+euhkG@3E4G2uhD+%=VK`l9N4 z#y;{A(3{HtusK{hl<$7^+bIAia%Nh~d7ux}cQ!w>pTX*@o6snft1ir`s`bSM_hs$8 z7ncAwH@4;{op*JGbdR~;O#l<}ycc8M7o;0Y>CLjsxbf3mYr2c=@!08#X?5~nw>`!w zfF)KunBHlYf8CpoHCwS!Ts9X?YuF!e*nc@S1!%9LXj|L2AfFhP7EQUDUEK{P=ShUB z0ku_0x)JyBuN5O3im>JtT`QNb$FL zN%oZ-*p6`npINxCMAfKWTb}Bp%~~LNPcmSbkr$8A7hx9Y45L5Mef~B)m1@}$2`9=- zuQFnWlLKZEzjYn_@k9B!C72|(Tz?H__>O`y>+D!>5)R&;!x(3^glxvC6Kb3QeqBYnMTU8);m1w zZ7YhbuGIK5!58Uw=e5b}*@#DUo?QI6z0$PnHvQj9{dA1{XG zz=OFM!^%{z(paL>xTywP)z4)%kbT zMc;N0bqJZ))40lddt2Vdpy7_YVq!lt-Ug&OK0AaSL9UcFiS)=tuHyx+M z52Kw?5SG$jhLcY0M*Y^`R{Z1~?<9wTa~%DPUywSEq{bp&P9%b-xS1)?vL9)+lhrEC zcs9`rAtiZxfZFtZaZMFIZ429DJ&n_1hWxM=FX_2844l4Ejx3=WC&bU{? zbKb=`cFlZtoq;aH%iynlSr%+5vUNoIkG8E&6yFK?Du{u7=er(A)E%fp;97eX%Ul_p zQ?eBp`>QfIbW5WOroq&ox#?xw*mso5Hs%+n>^Z2I{8BJu=Sx)lr%uz10eAFWc4#iA z@>!H-Wbk(qxGG5xcO;U+eO!W%bmWTq^_li>7X5x$d_EXS6@L06IA*)6_JEmPjXK3_Db zBcW4bT3SYGsbmw25+c1%y6*Gx)qw@@^Jd5T#}n;taM^Rz8%5r`^=H~&^B?nT<+n0( zPSh#Zb3LNk-AFhs@27PaBi%Z1^mll4sU5zEKif#m2&j@2bO7B0_#0zK&Ko@;hr)xe z9OwSYe#_J#e9H9pyBNy0J*!doR0v!lN03;W*v$nA{OG@h(hX7{-tq24QUzpO&->fv zKhaQtxyc6TTI7F=QNb9UYIh zbq!YOeS8p_jrDGY7O@{>8(4b_D+lE-)xadg&f0?>Sl<*SA`;9eulil?yT0N(j{{c8} + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/zeppelin-frontend/src/assets/images/zeppelin_svg_logo_bg.svg b/zeppelin-frontend/src/assets/images/zeppelin_svg_logo_bg.svg new file mode 100644 index 00000000000..9e0b95d9197 --- /dev/null +++ b/zeppelin-frontend/src/assets/images/zeppelin_svg_logo_bg.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/zeppelin-frontend/src/browserslist b/zeppelin-frontend/src/browserslist new file mode 100644 index 00000000000..37371cb04b9 --- /dev/null +++ b/zeppelin-frontend/src/browserslist @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 \ No newline at end of file diff --git a/zeppelin-frontend/src/favicon.ico b/zeppelin-frontend/src/favicon.ico index 997406ad22c29aae95893fb3d666c30258a09537..7e5049a11f67b8787f7a8cf012c4cf2fa90c325f 100644 GIT binary patch literal 4286 zcmb`LX^4$c7{|}J#iV8|AAIm-hz~-D6jOFmBvJN4$P%)KC=F#vWX}gme6VYTWGOQg zvZTnC&>&0J$vPNw{r)rWsnhYE_ud;u7=9X7!(xD5GWl{ybGE1`ppqi$dugmhiI=0)FmDtv&?a1r*xHE3t^ zhDu_(!fPmlu`mFv#&7C3;JstBKDT^?e+nD|t;1-T0Nq2nuAR76a1ZvtS#WcIrM((P z!fB`h*Kh0Ju+M9#KrqjNC|UIpT{4$nN_UD}+!@CSeYsE_uttLfK&;Mcxw4*LSJ>#%E2 zx_wX5&I^qIU%x0`{kA|=)OK^Yb$zE|mRtzTo@47P2{Q3?8;oOsRcBzWrBY!0`q z#=jQ&z(+4u=i-59)7-}HN9+ZlGiUR^qdpW)I5z9^%S*%z0o?(v?~B)#Z%V=(XinY! zT7zjY4>GOCV`An&=nUDs(umy$*tKSHW7eIf^XZSz=Cq~mvfW_Yp|VHZ9MG>D+=PcQ zIdn&LfsNpX=v zxczMX1a_U59gs8^Ha@QG7|_=ol!DHW);u%@?Z@>n9OBk1PSdr`j?g_a^A;En5=h(p#=OU;EO};kNY#-U8i8u02%0k)S`F9Eagt zluLWR9UP6?p`5wc-z4a3{&k1zy|uqTbSL)&%}Xxx92(QlkiP6_w2sOV)NKywE`VaV zf|2}R_|ZS>Dg$yF$d<>?uD)YVGR0hQRwE*wobnk zXx;zi`_If)FD?%4e6V*@uH!gE-`KY=PAg=K#-KYt7k#}aO+qR#sxYPzG>Ax1 orYiZb1PgtPsE(ngghb^L6_j+U6{$k38fr``oi1sRPL~(`0f&k}9RL6T literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 - + + + + Zeppelin - - - + +
+
+ +
+

Zeppelin

+ +
+
+
+
+ + diff --git a/zeppelin-frontend/src/karma.conf.js b/zeppelin-frontend/src/karma.conf.js new file mode 100644 index 00000000000..793ab6b3641 --- /dev/null +++ b/zeppelin-frontend/src/karma.conf.js @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/zeppelin-frontend/src/main.ts b/zeppelin-frontend/src/main.ts index c7b673cf44b..fa4e0aef337 100644 --- a/zeppelin-frontend/src/main.ts +++ b/zeppelin-frontend/src/main.ts @@ -8,5 +8,6 @@ if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule) +platformBrowserDynamic() + .bootstrapModule(AppModule) .catch(err => console.error(err)); diff --git a/zeppelin-frontend/src/polyfills.ts b/zeppelin-frontend/src/polyfills.ts index aa665d6b874..a86410c8e11 100644 --- a/zeppelin-frontend/src/polyfills.ts +++ b/zeppelin-frontend/src/polyfills.ts @@ -18,46 +18,65 @@ * BROWSER POLYFILLS */ +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** + * If the application will be indexed by Google Search, the following is required. + * Googlebot uses a renderer based on Chrome 41. + * https://developers.google.com/search/docs/guides/rendering + **/ +// import 'core-js/es6/array'; + /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + /** * Web Animations `@angular/platform-browser/animations` * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ + **/ // import 'web-animations-js'; // Run `npm install --save web-animations-js`. /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags - * because those flags need to be set before `zone.js` being loaded, and webpack - * will put import in the top of bundle, so user need to create a separate file - * in this directory (for example: zone-flags.ts), and put the following flags - * into that file, and then add the following code before importing zone.js. - * import './zone-flags.ts'; - * - * The flags allowed in zone-flags.ts are listed here. - * - * The following flags will work for all browsers. - * - * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - * - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - * - * (window as any).__Zone_enable_cross_context_check = true; - * */ +// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame +// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick +// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + +/* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - +// tslint:disable +import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ + +import 'core-js/es7/reflect'; diff --git a/zeppelin-frontend/src/styles.less b/zeppelin-frontend/src/styles.less index 90d4ee0072c..3c136acf26c 100644 --- a/zeppelin-frontend/src/styles.less +++ b/zeppelin-frontend/src/styles.less @@ -1 +1,7 @@ -/* You can add global styles to this file, and also import other style files */ +@import './styles/spin'; +@import './styles/base'; +@import './styles/font'; +@import './styles/global'; +@import './styles/rewrite'; +@import "node_modules/ng-zorro-antd/resizable/style/entry.less"; +@import "node_modules/ng-zorro-antd/code-editor/style/entry.less"; diff --git a/zeppelin-frontend/src/styles/base.less b/zeppelin-frontend/src/styles/base.less new file mode 100644 index 00000000000..52eb5e2f876 --- /dev/null +++ b/zeppelin-frontend/src/styles/base.less @@ -0,0 +1,6 @@ +* { + box-sizing: border-box; + outline: none; + margin: 0; + padding: 0; +} diff --git a/zeppelin-frontend/src/styles/font.less b/zeppelin-frontend/src/styles/font.less new file mode 100644 index 00000000000..1703852d766 --- /dev/null +++ b/zeppelin-frontend/src/styles/font.less @@ -0,0 +1,8 @@ +/* latin */ +@font-face { + font-family: 'Patua One'; + font-style: normal; + font-weight: 400; + src: local('Patua One'), local('PatuaOne-Regular'), url(../assets/fonts/patua-one.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/zeppelin-frontend/src/styles/global.less b/zeppelin-frontend/src/styles/global.less new file mode 100644 index 00000000000..29042aba826 --- /dev/null +++ b/zeppelin-frontend/src/styles/global.less @@ -0,0 +1,108 @@ +@import '../../node_modules/ng-zorro-antd/src/style/color/colors'; + +.tips { + &.warning { + color: @volcano-6; + } + + &.error { + color: @red-6; + } +} + +.modal-footer { + padding: 10px 24px; + margin: 24px -24px -24px -24px; +} + +.transparent-button { + background: transparent; + border: none; + box-shadow: none; + + &:hover { + background: transparent; + } +} + +.padding-sm { + padding: 12px !important; +} + +.font-sm { + font-size: 12px; +} + +.opacity-05 { + opacity: 0.5; +} + +.drag-tag { + box-sizing: border-box; + color: rgba(0, 0, 0, 0.65); + font-variant: tabular-nums; + list-style: none; + font-feature-settings: 'tnum'; + display: inline-block; + height: auto; + margin: 0 8px 8px 0; + padding: 0 7px; + font-size: 12px; + line-height: 20px; + white-space: nowrap; + background: #fafafa; + border: 1px solid #d9d9d9; + border-radius: 0px; + cursor: pointer; + opacity: 1; + + &.cdk-drag-preview { + box-shadow: 0 0 6px -2px rgba(0, 0, 0, 0.2); + } + + &.cdk-drag-placeholder { + border-style: dashed; + } +} + +.interpreter-box { + margin-bottom: 12px; + line-height: 32px; + + &:last-child { + margin-bottom: 0; + } + + .refresh { + font-size: 18px; + line-height: 0; + margin-right: 12px; + } + + &.cdk-drag-placeholder { + opacity: 0; + } + + .interpreter-name { + display: inline-block; + + .main-name { + display: inline-block; + margin-right: 6px; + } + + .child-name { + font-size: 12px; + opacity: 0.7; + display: inline-block; + } + } +} + +//.view-lines { +// .view-line { +// &:first-child { +// filter: grayscale(1); +// } +// } +//} diff --git a/zeppelin-frontend/src/styles/rewrite.less b/zeppelin-frontend/src/styles/rewrite.less new file mode 100644 index 00000000000..cb1f0f1888c --- /dev/null +++ b/zeppelin-frontend/src/styles/rewrite.less @@ -0,0 +1,13 @@ +nz-tree { + .ant-tree { + nz-tree-node { + li { + padding: 0; + } + } + } +} + +.ant-tabs-nav .ant-tabs-tab { + font-weight: 500; +} diff --git a/zeppelin-frontend/src/styles/spin.less b/zeppelin-frontend/src/styles/spin.less new file mode 100644 index 00000000000..4316c03f4c4 --- /dev/null +++ b/zeppelin-frontend/src/styles/spin.less @@ -0,0 +1,138 @@ +.spin { + height: 100vh; + width: 100vw; + position: fixed; + z-index: 1000; + top: 0; + left: 0; + + &:after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("../assets/images/bg.jpg"); + background-size: cover; + filter: blur(4px); + background-repeat: no-repeat; + background-position: center; + } + + &.transparent { + &:after { + content: none; + } + } + + & > div { + background: rgba(255, 255, 255, 0.5); + text-align: center; + position: absolute; + z-index: 1; + top: 0; + bottom: 0; + left: 0; + right: 0; + + .logo { + width: 160px; + height: 160px; + margin: -64px auto 0; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + position: absolute; + + &:after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + animation: opacity 2.0s infinite ease; + background-image: url(../assets/images/zeppelin_svg_logo.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 75% auto; + } + } + + .spin-text { + width: 160px; + height: 160px; + top: 50%; + left: 50%; + transform: translateX(-50%); + position: absolute; + margin: -16px auto 0; + + .brand-title { + text-align: center; + font-family: 'Patua One', cursive; + color: #3071a9; + font-size: 40px; + display: block !important; + } + + .status { + margin: -6px auto 0; + width: 160px; + display: block !important; + color: #ffffff; + line-height: 24px; + height: 24px; + font-size: 12px; + position: relative; + background-color: rgba(48, 113, 169, 0.7); + + &::before { + content: ''; + position: absolute; + z-index: -1; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #3071a9; + transform-origin: left center; + transform: scaleX(0); + transition: transform 0.2s ease-in-out; + animation: move 1.0s infinite ease; + } + } + } + } +} + +@keyframes opacity { + 0% { + opacity: 0.7; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0.7; + } +} + + +@keyframes move { + 0% { + transform: scaleX(0); + transform-origin: left center; + } + 100% { + transform-origin: left center; + transform: scaleX(1); + } +} + + diff --git a/zeppelin-frontend/src/styles/theme/dark/antd-dark.less b/zeppelin-frontend/src/styles/theme/dark/antd-dark.less new file mode 100644 index 00000000000..c4329e6ec26 --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/dark/antd-dark.less @@ -0,0 +1,5 @@ +html.dark { + @import '../../../../node_modules/ng-zorro-antd/ng-zorro-antd.less'; + @import 'theme-dark'; + @import '../markdown'; +} diff --git a/zeppelin-frontend/src/styles/theme/dark/theme-dark.less b/zeppelin-frontend/src/styles/theme/dark/theme-dark.less new file mode 100644 index 00000000000..4d06ba060b4 --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/dark/theme-dark.less @@ -0,0 +1,637 @@ +// The prefix to use on all css classes from ant. +@ant-prefix: ant; + +// An override for the html selector for theme prefixes +@html-selector: html; + +// -------- Colors ----------- +@primary-color: @blue-6; +@info-color: @blue-6; +@success-color: @green-6; +@processing-color: @blue-6; +@error-color: @red-6; +@highlight-color: @red-6; +@warning-color: @gold-6; +@normal-color: #d9d9d9; +@white: #fff; +@black: #000; + +// Color used by default to control hover and active backgrounds and for +// alert info backgrounds. +@primary-1: color(~`colorPalette('@{primary-color}', 1) `); // replace tint(@primary-color, 90%) +@primary-2: color(~`colorPalette('@{primary-color}', 2) `); // replace tint(@primary-color, 80%) +@primary-3: color(~`colorPalette('@{primary-color}', 3) `); // unused +@primary-4: color(~`colorPalette('@{primary-color}', 4) `); // unused +@primary-5: color( + ~`colorPalette('@{primary-color}', 5) ` +); // color used to control the text color in many active and hover states, replace tint(@primary-color, 20%) +@primary-6: @primary-color; // color used to control the text color of active buttons, don't use, use @primary-color +@primary-7: color(~`colorPalette('@{primary-color}', 7) `); // replace shade(@primary-color, 5%) +@primary-8: color(~`colorPalette('@{primary-color}', 8) `); // unused +@primary-9: color(~`colorPalette('@{primary-color}', 9) `); // unused +@primary-10: color(~`colorPalette('@{primary-color}', 10) `); // unused + +// Base Scaffolding Variables +// --- + +// Background color for `` +@body-background: #fff; +// Base background color for most components +@component-background: #fff; +@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', + 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; +@text-color: fade(@black, 65%); +@text-color-secondary: fade(@black, 45%); +@text-color-warning: @gold-7; +@text-color-danger: @red-7; +@text-color-inverse: @white; +@icon-color: inherit; +@icon-color-hover: fade(@black, 75%); +@heading-color: fade(#000, 85%); +@heading-color-dark: fade(@white, 100%); +@text-color-dark: fade(@white, 85%); +@text-color-secondary-dark: fade(@white, 65%); +@font-variant-base: tabular-nums; +@font-feature-settings-base: 'tnum'; +@font-size-base: 14px; +@font-size-lg: @font-size-base + 2px; +@font-size-sm: 12px; +@heading-1-size: ceil(@font-size-base * 2.71); +@heading-2-size: ceil(@font-size-base * 2.14); +@heading-3-size: ceil(@font-size-base * 1.71); +@heading-4-size: ceil(@font-size-base * 1.42); +@line-height-base: 1.5; +@border-radius-base: 4px; +@border-radius-sm: 2px; + +// vertical paddings +@padding-lg: 24px; // containers +@padding-md: 16px; // small containers and buttons +@padding-sm: 12px; // Form controls and items +@padding-xs: 8px; // small items + +// vertical padding for all form controls +@control-padding-horizontal: @padding-sm; +@control-padding-horizontal-sm: @padding-xs; + +// The background colors for active and hover states for things like +// list items or table cells. +@item-active-bg: @primary-1; +@item-hover-bg: @primary-1; + +// ICONFONT +@iconfont-css-prefix: anticon; + +// LINK +@link-color: @primary-color; +@link-hover-color: color(~`colorPalette('@{link-color}', 5) `); +@link-active-color: color(~`colorPalette('@{link-color}', 7) `); +@link-decoration: none; +@link-hover-decoration: none; + +// Animation +@ease-base-out: cubic-bezier(0.7, 0.3, 0.1, 1); +@ease-base-in: cubic-bezier(0.9, 0, 0.3, 0.7); +@ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); +@ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); +@ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); +@ease-out-back: cubic-bezier(0.12, 0.4, 0.29, 1.46); +@ease-in-back: cubic-bezier(0.71, -0.46, 0.88, 0.6); +@ease-in-out-back: cubic-bezier(0.71, -0.46, 0.29, 1.46); +@ease-out-circ: cubic-bezier(0.08, 0.82, 0.17, 1); +@ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.34); +@ease-in-out-circ: cubic-bezier(0.78, 0.14, 0.15, 0.86); +@ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); +@ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); +@ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); + +// Border color +@border-color-base: hsv(0, 0, 85%); // base border outline a component +@border-color-split: hsv(0, 0, 91%); // split border inside a component +@border-color-inverse: @white; +@border-width-base: 1px; // width of the border for a component +@border-style-base: solid; // style of a components border + +// Outline +@outline-blur-size: 0; +@outline-width: 2px; +@outline-color: @primary-color; + +@background-color-light: hsv(0, 0, 98%); // background of header and selected item +@background-color-base: hsv(0, 0, 96%); // Default grey background color + +// Disabled states +@disabled-color: fade(#000, 25%); +@disabled-bg: @background-color-base; +@disabled-color-dark: fade(#fff, 35%); + +// Shadow +@shadow-color: rgba(0, 0, 0, 0.15); +@shadow-color-inverse: @component-background; +@box-shadow-base: @shadow-1-down; +@shadow-1-up: 0 -2px 8px @shadow-color; +@shadow-1-down: 0 2px 8px @shadow-color; +@shadow-1-left: -2px 0 8px @shadow-color; +@shadow-1-right: 2px 0 8px @shadow-color; +@shadow-2: 0 4px 12px @shadow-color; + +// Buttons +@btn-font-weight: 400; +@btn-border-radius-base: @border-radius-base; +@btn-border-radius-sm: @border-radius-base; +@btn-border-width: @border-width-base; +@btn-border-style: @border-style-base; +@btn-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); +@btn-primary-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); +@btn-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + +@btn-primary-color: #fff; +@btn-primary-bg: @primary-color; + +@btn-default-color: @text-color; +@btn-default-bg: #fff; +@btn-default-border: @border-color-base; + +@btn-danger-color: @error-color; +@btn-danger-bg: @background-color-base; +@btn-danger-border: @border-color-base; + +@btn-disable-color: @disabled-color; +@btn-disable-bg: @disabled-bg; +@btn-disable-border: @border-color-base; + +@btn-padding-base: 0 @padding-md - 1px; +@btn-font-size-lg: @font-size-lg; +@btn-font-size-sm: @font-size-base; +@btn-padding-lg: @btn-padding-base; +@btn-padding-sm: 0 @padding-xs - 1px; + +@btn-height-base: 32px; +@btn-height-lg: 40px; +@btn-height-sm: 24px; + +@btn-circle-size: @btn-height-base; +@btn-circle-size-lg: @btn-height-lg; +@btn-circle-size-sm: @btn-height-sm; + +@btn-group-border: @primary-5; + +// Checkbox +@checkbox-size: 16px; +@checkbox-color: @primary-color; +@checkbox-check-color: #fff; +@checkbox-border-width: @border-width-base; + +// Empty +@empty-font-size: @font-size-base; + +// Radio +@radio-size: 16px; +@radio-dot-color: @primary-color; + +// Radio buttons +@radio-button-bg: @btn-default-bg; +@radio-button-checked-bg: @btn-default-bg; +@radio-button-color: @btn-default-color; +@radio-button-hover-color: @primary-5; +@radio-button-active-color: @primary-7; + +// Media queries breakpoints +// Extra small screen / phone +@screen-xs: 480px; +@screen-xs-min: @screen-xs; + +// Small screen / tablet +@screen-sm: 576px; +@screen-sm-min: @screen-sm; + +// Medium screen / desktop +@screen-md: 768px; +@screen-md-min: @screen-md; + +// Large screen / wide desktop +@screen-lg: 992px; +@screen-lg-min: @screen-lg; + +// Extra large screen / full hd +@screen-xl: 1200px; +@screen-xl-min: @screen-xl; + +// Extra extra large screen / large desktop +@screen-xxl: 1600px; +@screen-xxl-min: @screen-xxl; + +// provide a maximum +@screen-xs-max: (@screen-sm-min - 1px); +@screen-sm-max: (@screen-md-min - 1px); +@screen-md-max: (@screen-lg-min - 1px); +@screen-lg-max: (@screen-xl-min - 1px); +@screen-xl-max: (@screen-xxl-min - 1px); + +// Grid system +@grid-columns: 24; +@grid-gutter-width: 0; + +// Layout +@layout-body-background: #f0f2f5; +@layout-header-background: #001529; +@layout-footer-background: @layout-body-background; +@layout-header-height: 64px; +@layout-header-padding: 0 50px; +@layout-footer-padding: 24px 50px; +@layout-sider-background: @layout-header-background; +@layout-trigger-height: 48px; +@layout-trigger-background: #002140; +@layout-trigger-color: #fff; +@layout-zero-trigger-width: 36px; +@layout-zero-trigger-height: 42px; +// Layout light theme +@layout-sider-background-light: #fff; +@layout-trigger-background-light: #fff; +@layout-trigger-color-light: @text-color; + +// z-index list, order by `z-index` +@zindex-table-fixed: auto; +@zindex-affix: 10; +@zindex-back-top: 10; +@zindex-badge: 10; +@zindex-picker-panel: 10; +@zindex-popup-close: 10; +@zindex-modal: 1000; +@zindex-modal-mask: 1000; +@zindex-message: 1010; +@zindex-notification: 1010; +@zindex-popover: 1030; +@zindex-dropdown: 1050; +@zindex-picker: 1050; +@zindex-tooltip: 1060; + +// Animation +@animation-duration-slow: 0.3s; // Modal +@animation-duration-base: 0.2s; +@animation-duration-fast: 0.1s; // Tooltip + +// Form +// --- +@label-required-color: @highlight-color; +@label-color: @heading-color; +@form-item-margin-bottom: 24px; +@form-item-trailing-colon: true; +@form-vertical-label-padding: 0 0 8px; +@form-vertical-label-margin: 0; + +// Input +// --- +@input-height-base: 32px; +@input-height-lg: 40px; +@input-height-sm: 24px; +@input-padding-horizontal: @control-padding-horizontal - 1px; +@input-padding-horizontal-base: @input-padding-horizontal; +@input-padding-horizontal-sm: @control-padding-horizontal-sm - 1px; +@input-padding-horizontal-lg: @input-padding-horizontal; +@input-padding-vertical-base: 4px; +@input-padding-vertical-sm: 1px; +@input-padding-vertical-lg: 6px; +@input-placeholder-color: hsv(0, 0, 75%); +@input-color: @text-color; +@input-border-color: @border-color-base; +@input-bg: #fff; +@input-number-handler-active-bg: #f4f4f4; +@input-addon-bg: @background-color-light; +@input-hover-border-color: @primary-color; +@input-disabled-bg: @disabled-bg; +@input-outline-offset: 0 0; + +// Select +// --- +@select-border-color: @border-color-base; +@select-item-selected-font-weight: 600; + +// Tooltip +// --- +// Tooltip max width +@tooltip-max-width: 250px; +// Tooltip text color +@tooltip-color: #fff; +// Tooltip background color +@tooltip-bg: rgba(0, 0, 0, 0.75); +// Tooltip arrow width +@tooltip-arrow-width: 5px; +// Tooltip distance with trigger +@tooltip-distance: @tooltip-arrow-width - 1px + 4px; +// Tooltip arrow color +@tooltip-arrow-color: @tooltip-bg; + +// Popover +// --- +// Popover body background color +@popover-bg: #fff; +// Popover text color +@popover-color: @text-color; +// Popover maximum width +@popover-min-width: 177px; +// Popover arrow width +@popover-arrow-width: 6px; +// Popover arrow color +@popover-arrow-color: @popover-bg; +// Popover outer arrow width +// Popover outer arrow color +@popover-arrow-outer-color: @popover-bg; +// Popover distance with trigger +@popover-distance: @popover-arrow-width + 4px; + +// Modal +// -- +@modal-body-padding: 24px; +@modal-header-bg: @component-background; +@modal-footer-bg: tranparent; +@modal-mask-bg: fade(@black, 65%); + +// Progress +// -- +@progress-default-color: @processing-color; +@progress-remaining-color: @background-color-base; +@progress-text-color: @text-color; + +// Menu +// --- +@menu-inline-toplevel-item-height: 40px; +@menu-item-height: 40px; +@menu-collapsed-width: 80px; +@menu-bg: @component-background; +@menu-popup-bg: @component-background; +@menu-item-color: @text-color; +@menu-highlight-color: @primary-color; +@menu-item-active-bg: @item-active-bg; +@menu-item-active-border-width: 3px; +@menu-item-group-title-color: @text-color-secondary; +// dark theme +@menu-dark-color: @text-color-secondary-dark; +@menu-dark-bg: @layout-header-background; +@menu-dark-arrow-color: #fff; +@menu-dark-submenu-bg: #000c17; +@menu-dark-highlight-color: #fff; +@menu-dark-item-active-bg: @primary-color; + +// Spin +// --- +@spin-dot-size-sm: 14px; +@spin-dot-size: 20px; +@spin-dot-size-lg: 32px; + +// Table +// -- +@table-header-bg: @background-color-light; +@table-header-color: @heading-color; +@table-header-sort-bg: @background-color-base; +@table-body-sort-bg: rgba(0, 0, 0, 0.01); +@table-row-hover-bg: @primary-1; +@table-selected-row-bg: #fafafa; +@table-expanded-row-bg: #fbfbfb; +@table-padding-vertical: 16px; +@table-padding-horizontal: 16px; +@table-border-radius-base: @border-radius-base; + +// Tag +// -- +@tag-default-bg: @background-color-light; +@tag-default-color: @text-color; +@tag-font-size: @font-size-sm; + +// TimePicker +// --- +@time-picker-panel-column-width: 56px; +@time-picker-panel-width: @time-picker-panel-column-width * 3; +@time-picker-selected-bg: @background-color-base; + +// Carousel +// --- +@carousel-dot-width: 16px; +@carousel-dot-height: 3px; +@carousel-dot-active-width: 24px; + +// Badge +// --- +@badge-height: 20px; +@badge-dot-size: 6px; +@badge-font-size: @font-size-sm; +@badge-font-weight: normal; +@badge-status-size: 6px; +@badge-text-color: @component-background; + +// Rate +// --- +@rate-star-color: @yellow-6; +@rate-star-bg: @border-color-split; + +// Card +// --- +@card-head-color: @heading-color; +@card-head-background: transparent; +@card-head-padding: 16px; +@card-inner-head-padding: 12px; +@card-padding-base: 24px; +@card-actions-background: @background-color-light; +@card-background: #cfd8dc; +@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); +@card-radius: @border-radius-sm; + +// Comment +// --- +@comment-padding-base: 16px 0; +@comment-nest-indent: 44px; +@comment-author-name-color: @text-color-secondary; +@comment-author-time-color: #ccc; +@comment-action-color: @text-color-secondary; +@comment-action-hover-color: #595959; + +// Tabs +// --- +@tabs-card-head-background: @background-color-light; +@tabs-card-height: 40px; +@tabs-card-active-color: @primary-color; +@tabs-title-font-size: @font-size-base; +@tabs-title-font-size-lg: @font-size-lg; +@tabs-title-font-size-sm: @font-size-base; +@tabs-ink-bar-color: @primary-color; +@tabs-bar-margin: 0 0 16px 0; +@tabs-horizontal-margin: 0 32px 0 0; +@tabs-horizontal-padding: 12px 16px; +@tabs-horizontal-padding-lg: 16px; +@tabs-horizontal-padding-sm: 8px 16px; +@tabs-vertical-padding: 8px 24px; +@tabs-vertical-margin: 0 0 16px 0; +@tabs-scrolling-size: 32px; +@tabs-highlight-color: @primary-color; +@tabs-hover-color: @primary-5; +@tabs-active-color: @primary-7; + +// BackTop +// --- +@back-top-color: #fff; +@back-top-bg: @text-color-secondary; +@back-top-hover-bg: @text-color; + +// Avatar +// --- +@avatar-size-base: 32px; +@avatar-size-lg: 40px; +@avatar-size-sm: 24px; +@avatar-font-size-base: 18px; +@avatar-font-size-lg: 24px; +@avatar-font-size-sm: 14px; +@avatar-bg: #ccc; +@avatar-color: #fff; +@avatar-border-radius: @border-radius-base; + +// Switch +// --- +@switch-height: 22px; +@switch-sm-height: 16px; +@switch-sm-checked-margin-left: -(@switch-sm-height - 3px); +@switch-disabled-opacity: 0.4; +@switch-color: @primary-color; +@switch-shadow-color: fade(#00230b, 20%); + +// Pagination +// --- +@pagination-item-size: 32px; +@pagination-item-size-sm: 24px; +@pagination-font-family: Arial; +@pagination-font-weight-active: 500; +@pagination-item-bg-active: transparent; + +// PageHeader +// --- +@page-header-padding-horizontal: 24px; +@page-header-padding-vertical: 16px; + +// Breadcrumb +// --- +@breadcrumb-base-color: @text-color-secondary; +@breadcrumb-last-item-color: @text-color; +@breadcrumb-font-size: @font-size-base; +@breadcrumb-icon-font-size: @font-size-base; +@breadcrumb-link-color: @text-color-secondary; +@breadcrumb-link-color-hover: @primary-5; +@breadcrumb-separator-color: @text-color-secondary; +@breadcrumb-separator-margin: 0 @padding-xs; + +// Slider +// --- +@slider-margin: 14px 6px 10px; +@slider-rail-background-color: @background-color-base; +@slider-rail-background-color-hover: #e1e1e1; +@slider-track-background-color: @primary-3; +@slider-track-background-color-hover: @primary-4; +@slider-handle-color: @primary-3; +@slider-handle-color-hover: @primary-4; +@slider-handle-color-focus: tint(@primary-color, 20%); +@slider-handle-color-focus-shadow: fade(@primary-color, 20%); +@slider-handle-color-tooltip-open: @primary-color; +@slider-dot-border-color: @border-color-split; +@slider-dot-border-color-active: tint(@primary-color, 50%); +@slider-disabled-color: @disabled-color; +@slider-disabled-background-color: @component-background; + +// Tree +// --- +@tree-title-height: 24px; +@tree-child-padding: 18px; +@tree-directory-selected-color: #fff; +@tree-directory-selected-bg: @primary-color; + +// Collapse +// --- +@collapse-header-padding: 12px 16px 12px 40px; +@collapse-header-bg: @background-color-light; +@collapse-content-padding: @padding-md; +@collapse-content-bg: @component-background; + +// Skeleton +// --- +@skeleton-color: #f2f2f2; + +// Transfer +// --- +@transfer-disabled-bg: @disabled-bg; + +// Message +// --- +@message-notice-content-padding: 10px 16px; + +// Motion +// --- +@wave-animation-width: 6px; + +// Alert +// --- +@alert-success-border-color: ~`colorPalette('@{success-color}', 3) `; +@alert-success-bg-color: ~`colorPalette('@{success-color}', 1) `; +@alert-success-icon-color: @success-color; +@alert-info-border-color: ~`colorPalette('@{info-color}', 3) `; +@alert-info-bg-color: ~`colorPalette('@{info-color}', 1) `; +@alert-info-icon-color: @info-color; +@alert-warning-border-color: ~`colorPalette('@{warning-color}', 3) `; +@alert-warning-bg-color: ~`colorPalette('@{warning-color}', 1) `; +@alert-warning-icon-color: @warning-color; +@alert-error-border-color: ~`colorPalette('@{error-color}', 3) `; +@alert-error-bg-color: ~`colorPalette('@{error-color}', 1) `; +@alert-error-icon-color: @error-color; + +// List +// --- +@list-header-background: transparent; +@list-footer-background: transparent; +@list-empty-text-padding: @padding-md; +@list-item-padding: @padding-sm 0; +@list-item-meta-margin-bottom: @padding-md; +@list-item-meta-avatar-margin-right: @padding-md; +@list-item-meta-title-margin-bottom: @padding-sm; + +// Statistic +// --- +@statistic-title-font-size: @font-size-base; +@statistic-content-font-size: 24px; +@statistic-unit-font-size: 16px; +@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family; + +// Drawer +// --- +@drawer-header-padding: 16px 24px; +@drawer-body-padding: 24px; + + + +@layout-body-background: #171717; +@background-color-base: #262626; +@body-background: #404041; +@layout-sider-background: #001529; +@component-background: #262626; +@input-bg: #313133; +@btn-default-bg: #262626; +@border-color-base: #1e1e1e; +@border-color-split: #363636; +@heading-color: #E3E3E3; +@text-color: #E3E3E3; +@text-color-secondary: fade(#fff, 65%); +@table-selected-row-bg: #3a3a3a; +@table-expanded-row-bg: #3b3b3b; +@table-header-bg: #3a3a3b; +@table-row-hover-bg: #3a3a3b; +@layout-trigger-color: fade(#fff, 80%); +@layout-trigger-background: #313232; +@alert-message-color: fade(#000, 67%); +@item-hover-bg: fade(@blue-5, 20%); +@item-active-bg: fade(@blue-5, 40%); +@disabled-color: rgba(255, 255, 255, 0.25); +@tag-default-bg: #262628; +@popover-bg: #262629; +@wait-icon-color: fade(#fff, 64%); +@background-color-light: fade(@blue-5, 40%); +@collapse-header-bg: #262629; +@info-color: #313133; +@primary-color: @blue-7; +@highlight-color: @red-7; +@warning-color: @gold-9; diff --git a/zeppelin-frontend/src/styles/theme/light/antd-light.less b/zeppelin-frontend/src/styles/theme/light/antd-light.less new file mode 100644 index 00000000000..5cbd3996e77 --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/light/antd-light.less @@ -0,0 +1,3 @@ +@import '../../../../node_modules/ng-zorro-antd/ng-zorro-antd.less'; +@import 'theme-light'; +@import '../markdown'; diff --git a/zeppelin-frontend/src/styles/theme/light/theme-light.less b/zeppelin-frontend/src/styles/theme/light/theme-light.less new file mode 100644 index 00000000000..2dc2b48ce06 --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/light/theme-light.less @@ -0,0 +1,603 @@ +// The prefix to use on all css classes from ant. +@ant-prefix: ant; + +// An override for the html selector for theme prefixes +@html-selector: html; + +// -------- Colors ----------- +@primary-color: #3071a9; +@info-color: @blue-6; +@success-color: @green-6; +@processing-color: @blue-6; +@error-color: @red-6; +@highlight-color: @red-6; +@warning-color: @gold-6; +@normal-color: #d9d9d9; +@white: #fff; +@black: #000; + +// Color used by default to control hover and active backgrounds and for +// alert info backgrounds. +@primary-1: color(~`colorPalette('@{primary-color}', 1) `); // replace tint(@primary-color, 90%) +@primary-2: color(~`colorPalette('@{primary-color}', 2) `); // replace tint(@primary-color, 80%) +@primary-3: color(~`colorPalette('@{primary-color}', 3) `); // unused +@primary-4: color(~`colorPalette('@{primary-color}', 4) `); // unused +@primary-5: color( + ~`colorPalette('@{primary-color}', 5) ` +); // color used to control the text color in many active and hover states, replace tint(@primary-color, 20%) +@primary-6: @primary-color; // color used to control the text color of active buttons, don't use, use @primary-color +@primary-7: color(~`colorPalette('@{primary-color}', 7) `); // replace shade(@primary-color, 5%) +@primary-8: color(~`colorPalette('@{primary-color}', 8) `); // unused +@primary-9: color(~`colorPalette('@{primary-color}', 9) `); // unused +@primary-10: color(~`colorPalette('@{primary-color}', 10) `); // unused + +// Base Scaffolding Variables +// --- + +// Background color for `` +@body-background: #fff; +// Base background color for most components +@component-background: #fff; +@font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', + 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +@code-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; +@text-color: fade(@black, 65%); +@text-color-secondary: fade(@black, 45%); +@text-color-warning: @gold-7; +@text-color-danger: @red-7; +@text-color-inverse: @white; +@icon-color: inherit; +@icon-color-hover: fade(@black, 75%); +@heading-color: fade(#000, 85%); +@heading-color-dark: fade(@white, 100%); +@text-color-dark: fade(@white, 85%); +@text-color-secondary-dark: fade(@white, 65%); +@font-variant-base: tabular-nums; +@font-feature-settings-base: 'tnum'; +@font-size-base: 14px; +@font-size-lg: @font-size-base + 2px; +@font-size-sm: 12px; +@heading-1-size: ceil(@font-size-base * 2.71); +@heading-2-size: ceil(@font-size-base * 2.14); +@heading-3-size: ceil(@font-size-base * 1.71); +@heading-4-size: ceil(@font-size-base * 1.42); +@line-height-base: 1.5; +@border-radius-base: 0px; +@border-radius-sm: 0px; + +// vertical paddings +@padding-lg: 24px; // containers +@padding-md: 16px; // small containers and buttons +@padding-sm: 12px; // Form controls and items +@padding-xs: 8px; // small items + +// vertical padding for all form controls +@control-padding-horizontal: @padding-sm; +@control-padding-horizontal-sm: @padding-xs; + +// The background colors for active and hover states for things like +// list items or table cells. +@item-active-bg: @primary-1; +@item-hover-bg: @primary-1; + +// ICONFONT +@iconfont-css-prefix: anticon; + +// LINK +@link-color: @primary-color; +@link-hover-color: color(~`colorPalette('@{link-color}', 5) `); +@link-active-color: color(~`colorPalette('@{link-color}', 7) `); +@link-decoration: none; +@link-hover-decoration: none; + +// Animation +@ease-base-out: cubic-bezier(0.7, 0.3, 0.1, 1); +@ease-base-in: cubic-bezier(0.9, 0, 0.3, 0.7); +@ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); +@ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); +@ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); +@ease-out-back: cubic-bezier(0.12, 0.4, 0.29, 1.46); +@ease-in-back: cubic-bezier(0.71, -0.46, 0.88, 0.6); +@ease-in-out-back: cubic-bezier(0.71, -0.46, 0.29, 1.46); +@ease-out-circ: cubic-bezier(0.08, 0.82, 0.17, 1); +@ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.34); +@ease-in-out-circ: cubic-bezier(0.78, 0.14, 0.15, 0.86); +@ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1); +@ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06); +@ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1); + +// Border color +@border-color-base: hsv(0, 0, 85%); // base border outline a component +@border-color-split: hsv(0, 0, 91%); // split border inside a component +@border-color-inverse: @white; +@border-width-base: 1px; // width of the border for a component +@border-style-base: solid; // style of a components border + +// Outline +@outline-blur-size: 0; +@outline-width: 2px; +@outline-color: @primary-color; + +@background-color-light: hsv(0, 0, 98%); // background of header and selected item +@background-color-base: hsv(0, 0, 96%); // Default grey background color + +// Disabled states +@disabled-color: fade(#000, 25%); +@disabled-bg: @background-color-base; +@disabled-color-dark: fade(#fff, 35%); + +// Shadow +@shadow-color: rgba(0, 0, 0, 0.15); +@shadow-color-inverse: @component-background; +@box-shadow-base: @shadow-1-down; +@shadow-1-up: 0 -2px 8px @shadow-color; +@shadow-1-down: 0 2px 8px @shadow-color; +@shadow-1-left: -2px 0 8px @shadow-color; +@shadow-1-right: 2px 0 8px @shadow-color; +@shadow-2: 0 4px 12px @shadow-color; + +// Buttons +@btn-font-weight: 400; +@btn-border-radius-base: @border-radius-base; +@btn-border-radius-sm: @border-radius-base; +@btn-border-width: @border-width-base; +@btn-border-style: @border-style-base; +@btn-shadow: 0 2px 0 rgba(0, 0, 0, 0.015); +@btn-primary-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); +@btn-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + +@btn-primary-color: #fff; +@btn-primary-bg: @primary-color; + +@btn-default-color: @text-color; +@btn-default-bg: #fff; +@btn-default-border: @border-color-base; + +@btn-danger-color: @error-color; +@btn-danger-bg: @background-color-base; +@btn-danger-border: @border-color-base; + +@btn-disable-color: @disabled-color; +@btn-disable-bg: @disabled-bg; +@btn-disable-border: @border-color-base; + +@btn-padding-base: 0 @padding-md - 1px; +@btn-font-size-lg: @font-size-lg; +@btn-font-size-sm: @font-size-base; +@btn-padding-lg: @btn-padding-base; +@btn-padding-sm: 0 @padding-xs - 1px; + +@btn-height-base: 32px; +@btn-height-lg: 40px; +@btn-height-sm: 24px; + +@btn-circle-size: @btn-height-base; +@btn-circle-size-lg: @btn-height-lg; +@btn-circle-size-sm: @btn-height-sm; + +@btn-group-border: @primary-5; + +// Checkbox +@checkbox-size: 16px; +@checkbox-color: @primary-color; +@checkbox-check-color: #fff; +@checkbox-border-width: @border-width-base; + +// Empty +@empty-font-size: @font-size-base; + +// Radio +@radio-size: 16px; +@radio-dot-color: @primary-color; + +// Radio buttons +@radio-button-bg: @btn-default-bg; +@radio-button-checked-bg: @btn-default-bg; +@radio-button-color: @btn-default-color; +@radio-button-hover-color: @primary-5; +@radio-button-active-color: @primary-7; + +// Media queries breakpoints +// Extra small screen / phone +@screen-xs: 480px; +@screen-xs-min: @screen-xs; + +// Small screen / tablet +@screen-sm: 576px; +@screen-sm-min: @screen-sm; + +// Medium screen / desktop +@screen-md: 768px; +@screen-md-min: @screen-md; + +// Large screen / wide desktop +@screen-lg: 992px; +@screen-lg-min: @screen-lg; + +// Extra large screen / full hd +@screen-xl: 1200px; +@screen-xl-min: @screen-xl; + +// Extra extra large screen / large desktop +@screen-xxl: 1600px; +@screen-xxl-min: @screen-xxl; + +// provide a maximum +@screen-xs-max: (@screen-sm-min - 1px); +@screen-sm-max: (@screen-md-min - 1px); +@screen-md-max: (@screen-lg-min - 1px); +@screen-lg-max: (@screen-xl-min - 1px); +@screen-xl-max: (@screen-xxl-min - 1px); + +// Grid system +@grid-columns: 24; +@grid-gutter-width: 0; + +// Layout +@layout-body-background: #f0f2f5; +@layout-header-background: #001529; +@layout-footer-background: @layout-body-background; +@layout-header-height: 64px; +@layout-header-padding: 0 50px; +@layout-footer-padding: 24px 50px; +@layout-sider-background: @layout-header-background; +@layout-trigger-height: 48px; +@layout-trigger-background: #002140; +@layout-trigger-color: #fff; +@layout-zero-trigger-width: 36px; +@layout-zero-trigger-height: 42px; +// Layout light theme +@layout-sider-background-light: #fff; +@layout-trigger-background-light: #fff; +@layout-trigger-color-light: @text-color; + +// z-index list, order by `z-index` +@zindex-table-fixed: auto; +@zindex-affix: 10; +@zindex-back-top: 10; +@zindex-badge: 10; +@zindex-picker-panel: 10; +@zindex-popup-close: 10; +@zindex-modal: 1000; +@zindex-modal-mask: 1000; +@zindex-message: 1010; +@zindex-notification: 1010; +@zindex-popover: 1030; +@zindex-dropdown: 1050; +@zindex-picker: 1050; +@zindex-tooltip: 1060; + +// Animation +@animation-duration-slow: 0.3s; // Modal +@animation-duration-base: 0.2s; +@animation-duration-fast: 0.1s; // Tooltip + +// Form +// --- +@label-required-color: @highlight-color; +@label-color: @heading-color; +@form-item-margin-bottom: 24px; +@form-item-trailing-colon: true; +@form-vertical-label-padding: 0 0 8px; +@form-vertical-label-margin: 0; + +// Input +// --- +@input-height-base: 32px; +@input-height-lg: 40px; +@input-height-sm: 24px; +@input-padding-horizontal: @control-padding-horizontal - 1px; +@input-padding-horizontal-base: @input-padding-horizontal; +@input-padding-horizontal-sm: @control-padding-horizontal-sm - 1px; +@input-padding-horizontal-lg: @input-padding-horizontal; +@input-padding-vertical-base: 4px; +@input-padding-vertical-sm: 1px; +@input-padding-vertical-lg: 6px; +@input-placeholder-color: hsv(0, 0, 75%); +@input-color: @text-color; +@input-border-color: @border-color-base; +@input-bg: #fff; +@input-number-handler-active-bg: #f4f4f4; +@input-addon-bg: @background-color-light; +@input-hover-border-color: @primary-color; +@input-disabled-bg: @disabled-bg; +@input-outline-offset: 0 0; + +// Select +// --- +@select-border-color: @border-color-base; +@select-item-selected-font-weight: 600; + +// Tooltip +// --- +// Tooltip max width +@tooltip-max-width: 250px; +// Tooltip text color +@tooltip-color: #fff; +// Tooltip background color +@tooltip-bg: rgba(0, 0, 0, 0.75); +// Tooltip arrow width +@tooltip-arrow-width: 5px; +// Tooltip distance with trigger +@tooltip-distance: @tooltip-arrow-width - 1px + 4px; +// Tooltip arrow color +@tooltip-arrow-color: @tooltip-bg; + +// Popover +// --- +// Popover body background color +@popover-bg: #fff; +// Popover text color +@popover-color: @text-color; +// Popover maximum width +@popover-min-width: 177px; +// Popover arrow width +@popover-arrow-width: 6px; +// Popover arrow color +@popover-arrow-color: @popover-bg; +// Popover outer arrow width +// Popover outer arrow color +@popover-arrow-outer-color: @popover-bg; +// Popover distance with trigger +@popover-distance: @popover-arrow-width + 4px; + +// Modal +// -- +@modal-body-padding: 24px; +@modal-header-bg: @component-background; +@modal-footer-bg: tranparent; +@modal-mask-bg: fade(@black, 65%); + +// Progress +// -- +@progress-default-color: @processing-color; +@progress-remaining-color: @background-color-base; +@progress-text-color: @text-color; + +// Menu +// --- +@menu-inline-toplevel-item-height: 40px; +@menu-item-height: 40px; +@menu-collapsed-width: 80px; +@menu-bg: @component-background; +@menu-popup-bg: @component-background; +@menu-item-color: @text-color; +@menu-highlight-color: @primary-color; +@menu-item-active-bg: @item-active-bg; +@menu-item-active-border-width: 3px; +@menu-item-group-title-color: @text-color-secondary; +// dark theme +@menu-dark-color: @text-color-secondary-dark; +@menu-dark-bg: @layout-header-background; +@menu-dark-arrow-color: #fff; +@menu-dark-submenu-bg: #000c17; +@menu-dark-highlight-color: #fff; +@menu-dark-item-active-bg: @primary-color; + +// Spin +// --- +@spin-dot-size-sm: 14px; +@spin-dot-size: 20px; +@spin-dot-size-lg: 32px; + +// Table +// -- +@table-header-bg: @background-color-light; +@table-header-color: @heading-color; +@table-header-sort-bg: @background-color-base; +@table-body-sort-bg: rgba(0, 0, 0, 0.01); +@table-row-hover-bg: @primary-1; +@table-selected-row-bg: #fafafa; +@table-expanded-row-bg: #fbfbfb; +@table-padding-vertical: 16px; +@table-padding-horizontal: 16px; +@table-border-radius-base: @border-radius-base; + +// Tag +// -- +@tag-default-bg: @background-color-light; +@tag-default-color: @text-color; +@tag-font-size: @font-size-sm; + +// TimePicker +// --- +@time-picker-panel-column-width: 56px; +@time-picker-panel-width: @time-picker-panel-column-width * 3; +@time-picker-selected-bg: @background-color-base; + +// Carousel +// --- +@carousel-dot-width: 16px; +@carousel-dot-height: 3px; +@carousel-dot-active-width: 24px; + +// Badge +// --- +@badge-height: 20px; +@badge-dot-size: 6px; +@badge-font-size: @font-size-sm; +@badge-font-weight: normal; +@badge-status-size: 6px; +@badge-text-color: @component-background; + +// Rate +// --- +@rate-star-color: @yellow-6; +@rate-star-bg: @border-color-split; + +// Card +// --- +@card-head-color: @heading-color; +@card-head-background: transparent; +@card-head-padding: 16px; +@card-inner-head-padding: 12px; +@card-padding-base: 24px; +@card-actions-background: @background-color-light; +@card-background: #cfd8dc; +@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); +@card-radius: @border-radius-sm; + +// Comment +// --- +@comment-padding-base: 16px 0; +@comment-nest-indent: 44px; +@comment-author-name-color: @text-color-secondary; +@comment-author-time-color: #ccc; +@comment-action-color: @text-color-secondary; +@comment-action-hover-color: #595959; + +// Tabs +// --- +@tabs-card-head-background: @background-color-light; +@tabs-card-height: 40px; +@tabs-card-active-color: @primary-color; +@tabs-title-font-size: @font-size-base; +@tabs-title-font-size-lg: @font-size-lg; +@tabs-title-font-size-sm: @font-size-base; +@tabs-ink-bar-color: @primary-color; +@tabs-bar-margin: 0 0 16px 0; +@tabs-horizontal-margin: 0 32px 0 0; +@tabs-horizontal-padding: 12px 16px; +@tabs-horizontal-padding-lg: 16px; +@tabs-horizontal-padding-sm: 8px 16px; +@tabs-vertical-padding: 8px 24px; +@tabs-vertical-margin: 0 0 16px 0; +@tabs-scrolling-size: 32px; +@tabs-highlight-color: @primary-color; +@tabs-hover-color: @primary-5; +@tabs-active-color: @primary-7; + +// BackTop +// --- +@back-top-color: #fff; +@back-top-bg: @text-color-secondary; +@back-top-hover-bg: @text-color; + +// Avatar +// --- +@avatar-size-base: 32px; +@avatar-size-lg: 40px; +@avatar-size-sm: 24px; +@avatar-font-size-base: 18px; +@avatar-font-size-lg: 24px; +@avatar-font-size-sm: 14px; +@avatar-bg: #ccc; +@avatar-color: #fff; +@avatar-border-radius: @border-radius-base; + +// Switch +// --- +@switch-height: 22px; +@switch-sm-height: 16px; +@switch-sm-checked-margin-left: -(@switch-sm-height - 3px); +@switch-disabled-opacity: 0.4; +@switch-color: @primary-color; +@switch-shadow-color: fade(#00230b, 20%); + +// Pagination +// --- +@pagination-item-size: 32px; +@pagination-item-size-sm: 24px; +@pagination-font-family: Arial; +@pagination-font-weight-active: 500; +@pagination-item-bg-active: transparent; + +// PageHeader +// --- +@page-header-padding-horizontal: 24px; +@page-header-padding-vertical: 16px; + +// Breadcrumb +// --- +@breadcrumb-base-color: @text-color-secondary; +@breadcrumb-last-item-color: @text-color; +@breadcrumb-font-size: @font-size-base; +@breadcrumb-icon-font-size: @font-size-base; +@breadcrumb-link-color: @text-color-secondary; +@breadcrumb-link-color-hover: @primary-5; +@breadcrumb-separator-color: @text-color-secondary; +@breadcrumb-separator-margin: 0 @padding-xs; + +// Slider +// --- +@slider-margin: 14px 6px 10px; +@slider-rail-background-color: @background-color-base; +@slider-rail-background-color-hover: #e1e1e1; +@slider-track-background-color: @primary-3; +@slider-track-background-color-hover: @primary-4; +@slider-handle-color: @primary-3; +@slider-handle-color-hover: @primary-4; +@slider-handle-color-focus: tint(@primary-color, 20%); +@slider-handle-color-focus-shadow: fade(@primary-color, 20%); +@slider-handle-color-tooltip-open: @primary-color; +@slider-dot-border-color: @border-color-split; +@slider-dot-border-color-active: tint(@primary-color, 50%); +@slider-disabled-color: @disabled-color; +@slider-disabled-background-color: @component-background; + +// Tree +// --- +@tree-title-height: 24px; +@tree-child-padding: 18px; +@tree-directory-selected-color: #fff; +@tree-directory-selected-bg: @primary-color; + +// Collapse +// --- +@collapse-header-padding: 12px 16px 12px 40px; +@collapse-header-bg: @background-color-light; +@collapse-content-padding: @padding-md; +@collapse-content-bg: @component-background; + +// Skeleton +// --- +@skeleton-color: #f2f2f2; + +// Transfer +// --- +@transfer-disabled-bg: @disabled-bg; + +// Message +// --- +@message-notice-content-padding: 10px 16px; + +// Motion +// --- +@wave-animation-width: 6px; + +// Alert +// --- +@alert-success-border-color: ~`colorPalette('@{success-color}', 3) `; +@alert-success-bg-color: ~`colorPalette('@{success-color}', 1) `; +@alert-success-icon-color: @success-color; +@alert-info-border-color: ~`colorPalette('@{info-color}', 3) `; +@alert-info-bg-color: ~`colorPalette('@{info-color}', 1) `; +@alert-info-icon-color: @info-color; +@alert-warning-border-color: ~`colorPalette('@{warning-color}', 3) `; +@alert-warning-bg-color: ~`colorPalette('@{warning-color}', 1) `; +@alert-warning-icon-color: @warning-color; +@alert-error-border-color: ~`colorPalette('@{error-color}', 3) `; +@alert-error-bg-color: ~`colorPalette('@{error-color}', 1) `; +@alert-error-icon-color: @error-color; + +// List +// --- +@list-header-background: transparent; +@list-footer-background: transparent; +@list-empty-text-padding: @padding-md; +@list-item-padding: @padding-sm 0; +@list-item-meta-margin-bottom: @padding-md; +@list-item-meta-avatar-margin-right: @padding-md; +@list-item-meta-title-margin-bottom: @padding-sm; + +// Statistic +// --- +@statistic-title-font-size: @font-size-base; +@statistic-content-font-size: 24px; +@statistic-unit-font-size: 16px; +@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family; + +// Drawer +// --- +@drawer-header-padding: 16px 24px; +@drawer-body-padding: 24px; diff --git a/zeppelin-frontend/src/styles/theme/markdown.less b/zeppelin-frontend/src/styles/theme/markdown.less new file mode 100644 index 00000000000..25d28276816 --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/markdown.less @@ -0,0 +1,154 @@ +.markdown-body { + color: @text-color; + font-size: 14px; + line-height: 1.5; + + h1 { + color: @heading-color; + font-weight: 500; + margin: 0.6em 0 0.6em; + font-family: Avenir, @font-family; + font-size: 30px; + font-variant: tabular-nums; + line-height: 38px; + } + + h2 { + font-size: 24px; + line-height: 32px; + } + + h2, + h3, + h4, + h5, + h6 { + color: @heading-color; + font-family: Avenir, @font-family; + font-variant: tabular-nums; + margin: 0.6em 0 0.6em; + font-weight: 500; + clear: both; + } + + h3 { + font-size: 18px; + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + } + + h6 { + font-size: 12px; + } + + hr { + height: 1px; + border: 0; + background: @border-color-split; + margin: 56px 0; + clear: both; + } + + p, + pre { + margin: 1em 0; + } + + ul > li { + list-style-type: circle; + margin-left: 20px; + padding-left: 4px; + + &:empty { + display: none; + } + } + + ol > li { + list-style-type: decimal; + margin-left: 20px; + padding-left: 4px; + } + + ul > li > p, + ol > li > p { + margin: 0.2em 0; + } + + code { + margin: 0 1px; + background: #f2f4f5; + padding: .2em .4em; + border-radius: 3px; + font-size: .9em; + border: 1px solid #eee; + } + + pre { + border-radius: @border-radius-sm; + background: #f2f4f5; + font-family: "Lucida Console", Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + } + + pre code { + border: none; + background: #f2f4f5; + margin: 0; + padding: 0; + font-size: @font-size-base - 1px; + color: @text-color; + overflow: auto; + } + + strong, + b { + font-weight: 500; + } + + > table { + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + border: 1px solid @border-color-split; + width: 100%; + margin: 8px 0 16px; + } + + > table th { + white-space: nowrap; + color: #5c6b77; + font-weight: 500; + background: rgba(0, 0, 0, 0.02); + } + + & > table th, + & > table td { + border: 1px solid @border-color-split; + padding: 16px 24px; + text-align: left; + } + + blockquote { + font-size: 90%; + color: @text-color-secondary; + border-left: 4px solid @border-color-split; + padding-left: 0.8em; + margin: 1em 0; + } + + blockquote p { + margin: 0; + } + + & > br, + & > p > br { + clear: both; + } + +} diff --git a/zeppelin-frontend/src/styles/theme/theme-mixin.less b/zeppelin-frontend/src/styles/theme/theme-mixin.less new file mode 100644 index 00000000000..e6dda53441a --- /dev/null +++ b/zeppelin-frontend/src/styles/theme/theme-mixin.less @@ -0,0 +1,12 @@ +@import '../../../node_modules/ng-zorro-antd/src/style/color/colors'; + +.themeMixin(@rules) { + :host-context(.light) { + @import 'theme-light'; + @rules(); + } + :host-context(.dark) { + @import 'theme-dark'; + @rules(); + } +} diff --git a/zeppelin-frontend/src/test.ts b/zeppelin-frontend/src/test.ts index 16317897b1c..7602ed1c329 100644 --- a/zeppelin-frontend/src/test.ts +++ b/zeppelin-frontend/src/test.ts @@ -1,19 +1,15 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/dist/zone-testing'; import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; +import { platformBrowserDynamicTesting, BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +// tslint:disable-next-line:no-import-side-effect +import 'zone.js/dist/zone-testing'; +// tslint:disable-next-line no-any declare const require: any; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); // And load the modules. diff --git a/zeppelin-frontend/src/tsconfig.app.json b/zeppelin-frontend/src/tsconfig.app.json new file mode 100644 index 00000000000..50958688dbe --- /dev/null +++ b/zeppelin-frontend/src/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": ["mathjax"] + }, + "exclude": ["test.ts", "**/*.spec.ts"] +} diff --git a/zeppelin-frontend/src/tsconfig.spec.json b/zeppelin-frontend/src/tsconfig.spec.json new file mode 100644 index 00000000000..70add2d529d --- /dev/null +++ b/zeppelin-frontend/src/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "files": ["test.ts", "polyfills.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/zeppelin-frontend/src/tslint.json b/zeppelin-frontend/src/tslint.json new file mode 100644 index 00000000000..bb3e34a7b6e --- /dev/null +++ b/zeppelin-frontend/src/tslint.json @@ -0,0 +1,7 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "zeppelin", "camelCase"], + "component-selector": [true, "element", "zeppelin", "kebab-case"] + } +} From 3d02d0e9e9e183b78bc651006da0421c56c4f45f Mon Sep 17 00:00:00 2001 From: vthinkxie Date: Tue, 22 Oct 2019 10:20:47 +0800 Subject: [PATCH 04/19] chore: support basic features of zeppelin --- .../src/app/app-http.interceptor.ts | 41 ++ .../src/app/app-message.interceptor.ts | 56 ++ .../src/app/app-routing.module.ts | 23 +- .../src/app/app-runtime-compiler.providers.ts | 29 + zeppelin-frontend/src/app/app.component.html | 541 +-------------- zeppelin-frontend/src/app/app.component.less | 14 + zeppelin-frontend/src/app/app.component.ts | 21 +- zeppelin-frontend/src/app/app.module.ts | 72 +- .../core/copy-text/copy-text-to-clipboard.ts | 53 ++ .../src/app/core/copy-text/index.ts | 1 + .../src/app/core/copy-text/public-api.ts | 1 + .../destroy-hook/destroy-hook.component.ts | 11 + .../src/app/core/destroy-hook/index.ts | 1 + .../src/app/core/destroy-hook/public-api.ts | 1 + zeppelin-frontend/src/app/core/public-api.ts | 2 + zeppelin-frontend/src/app/interfaces/index.ts | 1 + .../src/app/interfaces/interpreter.ts | 92 +++ .../src/app/interfaces/message-interceptor.ts | 8 + .../src/app/interfaces/node-list.ts | 29 + .../src/app/interfaces/public-api.ts | 5 + .../src/app/interfaces/security.ts | 11 + .../src/app/interfaces/ticket.ts | 17 + .../src/app/interfaces/trash-folder-id.ts | 3 + zeppelin-frontend/src/app/languages/index.ts | 1 + zeppelin-frontend/src/app/languages/load.ts | 7 + .../src/app/languages/public-api.ts | 2 + zeppelin-frontend/src/app/languages/scala.ts | 229 +++++++ .../app/pages/login/login-routing.module.ts | 17 + .../src/app/pages/login/login.component.html | 34 + .../src/app/pages/login/login.component.less | 57 ++ .../src/app/pages/login/login.component.ts | 35 + .../src/app/pages/login/login.module.ts | 14 + .../workspace/home/home-routing.module.ts | 17 + .../pages/workspace/home/home.component.html | 29 + .../pages/workspace/home/home.component.less | 25 + .../pages/workspace/home/home.component.ts | 36 + .../app/pages/workspace/home/home.module.ts | 15 + .../action-bar/action-bar.component.html | 227 +++++++ .../action-bar/action-bar.component.less | 93 +++ .../action-bar/action-bar.component.ts | 281 ++++++++ .../add-paragraph.component.html | 5 + .../add-paragraph.component.less | 38 ++ .../add-paragraph/add-paragraph.component.ts | 22 + .../interpreter-binding.component.html | 43 ++ .../interpreter-binding.component.less | 18 + .../interpreter-binding.component.ts | 69 ++ .../notebook/notebook-routing.module.ts | 25 + .../notebook/notebook.component.html | 42 ++ .../notebook/notebook.component.less | 24 + .../workspace/notebook/notebook.component.ts | 303 +++++++++ .../workspace/notebook/notebook.module.ts | 86 +++ .../paragraph/control/control.component.html | 73 ++ .../paragraph/control/control.component.less | 61 ++ .../paragraph/control/control.component.ts | 292 ++++++++ .../paragraph/footer/footer.component.html | 4 + .../paragraph/footer/footer.component.less | 9 + .../paragraph/footer/footer.component.ts | 62 ++ .../paragraph/paragraph.component.html | 87 +++ .../paragraph/paragraph.component.less | 60 ++ .../notebook/paragraph/paragraph.component.ts | 625 ++++++++++++++++++ .../progress/progress.component.html | 4 + .../progress/progress.component.less | 0 .../paragraph/progress/progress.component.ts | 20 + .../permissions/permissions.component.html | 76 +++ .../permissions/permissions.component.less | 21 + .../permissions/permissions.component.ts | 123 ++++ .../revisions-comparator.component.html | 8 + .../revisions-comparator.component.less | 0 .../revisions-comparator.component.ts | 13 + .../elastic-input.component.html | 11 + .../elastic-input.component.less | 44 ++ .../elastic-input/elastic-input.component.ts | 76 +++ .../workspace/notebook/share/share.module.ts | 14 + .../workspace/workspace-routing.module.ts | 39 ++ .../pages/workspace/workspace.component.html | 5 + .../pages/workspace/workspace.component.less | 14 + .../pages/workspace/workspace.component.ts | 36 + .../app/pages/workspace/workspace.guard.ts | 24 + .../app/pages/workspace/workspace.module.ts | 25 + .../app/services/array-ordering.service.ts | 50 ++ .../src/app/services/base-rest.ts | 33 + .../src/app/services/completion.service.ts | 85 +++ .../src/app/services/helium.service.ts | 12 + .../src/app/services/ng-z.service.ts | 73 ++ .../src/app/services/note-action.service.ts | 60 ++ .../src/app/services/note-list.service.ts | 85 +++ .../src/app/services/note-status.service.ts | 53 ++ .../app/services/note-var-share.service.ts | 26 + .../src/app/services/public-api.ts | 12 + .../app/services/runtime-compiler.service.ts | 62 ++ .../src/app/services/save-as.service.ts | 25 + .../src/app/services/security.service.ts | 28 + .../src/app/services/ticket.service.ts | 104 +++ .../about-zeppelin.component.html | 16 + .../about-zeppelin.component.less | 26 + .../about-zeppelin.component.ts | 14 + .../folder-rename.component.html | 20 + .../folder-rename.component.less | 0 .../folder-rename/folder-rename.component.ts | 68 ++ .../app/share/header/header.component.html | 65 ++ .../app/share/header/header.component.less | 98 +++ .../src/app/share/header/header.component.ts | 74 +++ zeppelin-frontend/src/app/share/index.ts | 1 + .../share/node-list/node-list.component.html | 127 ++++ .../share/node-list/node-list.component.less | 70 ++ .../share/node-list/node-list.component.ts | 105 +++ .../note-create/note-create.component.html | 32 + .../note-create/note-create.component.less | 0 .../note-create/note-create.component.ts | 93 +++ .../note-import/note-import.component.html | 38 ++ .../note-import/note-import.component.less | 7 + .../note-import/note-import.component.ts | 98 +++ .../note-rename/note-rename.component.html | 11 + .../note-rename/note-rename.component.less | 0 .../note-rename/note-rename.component.ts | 25 + .../page-header/page-header.component.html | 6 + .../page-header/page-header.component.less | 5 + .../page-header/page-header.component.ts | 19 + .../app/share/pipes/humanize-bytes.pipe.ts | 46 ++ .../src/app/share/pipes/index.ts | 1 + .../src/app/share/pipes/public-api.ts | 1 + zeppelin-frontend/src/app/share/public-api.ts | 3 + .../run-scripts/run-scripts.directive.ts | 68 ++ .../src/app/share/share.module.ts | 84 +++ .../src/app/share/spin/spin.component.html | 9 + .../src/app/share/spin/spin.component.less | 0 .../src/app/share/spin/spin.component.ts | 14 + .../src/app/spell/spell-result.ts | 15 + 128 files changed, 6068 insertions(+), 552 deletions(-) create mode 100644 zeppelin-frontend/src/app/app-http.interceptor.ts create mode 100644 zeppelin-frontend/src/app/app-message.interceptor.ts create mode 100644 zeppelin-frontend/src/app/app-runtime-compiler.providers.ts create mode 100644 zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts create mode 100644 zeppelin-frontend/src/app/core/copy-text/index.ts create mode 100644 zeppelin-frontend/src/app/core/copy-text/public-api.ts create mode 100644 zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts create mode 100644 zeppelin-frontend/src/app/core/destroy-hook/index.ts create mode 100644 zeppelin-frontend/src/app/core/destroy-hook/public-api.ts create mode 100644 zeppelin-frontend/src/app/interfaces/index.ts create mode 100644 zeppelin-frontend/src/app/interfaces/interpreter.ts create mode 100644 zeppelin-frontend/src/app/interfaces/message-interceptor.ts create mode 100644 zeppelin-frontend/src/app/interfaces/node-list.ts create mode 100644 zeppelin-frontend/src/app/interfaces/public-api.ts create mode 100644 zeppelin-frontend/src/app/interfaces/security.ts create mode 100644 zeppelin-frontend/src/app/interfaces/ticket.ts create mode 100644 zeppelin-frontend/src/app/interfaces/trash-folder-id.ts create mode 100644 zeppelin-frontend/src/app/languages/index.ts create mode 100644 zeppelin-frontend/src/app/languages/load.ts create mode 100644 zeppelin-frontend/src/app/languages/public-api.ts create mode 100644 zeppelin-frontend/src/app/languages/scala.ts create mode 100644 zeppelin-frontend/src/app/pages/login/login-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/login/login.component.html create mode 100644 zeppelin-frontend/src/app/pages/login/login.component.less create mode 100644 zeppelin-frontend/src/app/pages/login/login.component.ts create mode 100644 zeppelin-frontend/src/app/pages/login/login.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/home/home.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/home/home.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/home/home.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/home/home.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/share/share.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace.guard.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/workspace.module.ts create mode 100644 zeppelin-frontend/src/app/services/array-ordering.service.ts create mode 100644 zeppelin-frontend/src/app/services/base-rest.ts create mode 100644 zeppelin-frontend/src/app/services/completion.service.ts create mode 100644 zeppelin-frontend/src/app/services/helium.service.ts create mode 100644 zeppelin-frontend/src/app/services/ng-z.service.ts create mode 100644 zeppelin-frontend/src/app/services/note-action.service.ts create mode 100644 zeppelin-frontend/src/app/services/note-list.service.ts create mode 100644 zeppelin-frontend/src/app/services/note-status.service.ts create mode 100644 zeppelin-frontend/src/app/services/note-var-share.service.ts create mode 100644 zeppelin-frontend/src/app/services/runtime-compiler.service.ts create mode 100644 zeppelin-frontend/src/app/services/save-as.service.ts create mode 100644 zeppelin-frontend/src/app/services/security.service.ts create mode 100644 zeppelin-frontend/src/app/services/ticket.service.ts create mode 100644 zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.html create mode 100644 zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.less create mode 100644 zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.ts create mode 100644 zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.html create mode 100644 zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.less create mode 100644 zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.ts create mode 100644 zeppelin-frontend/src/app/share/header/header.component.html create mode 100644 zeppelin-frontend/src/app/share/header/header.component.less create mode 100644 zeppelin-frontend/src/app/share/header/header.component.ts create mode 100644 zeppelin-frontend/src/app/share/index.ts create mode 100644 zeppelin-frontend/src/app/share/node-list/node-list.component.html create mode 100644 zeppelin-frontend/src/app/share/node-list/node-list.component.less create mode 100644 zeppelin-frontend/src/app/share/node-list/node-list.component.ts create mode 100644 zeppelin-frontend/src/app/share/note-create/note-create.component.html create mode 100644 zeppelin-frontend/src/app/share/note-create/note-create.component.less create mode 100644 zeppelin-frontend/src/app/share/note-create/note-create.component.ts create mode 100644 zeppelin-frontend/src/app/share/note-import/note-import.component.html create mode 100644 zeppelin-frontend/src/app/share/note-import/note-import.component.less create mode 100644 zeppelin-frontend/src/app/share/note-import/note-import.component.ts create mode 100644 zeppelin-frontend/src/app/share/note-rename/note-rename.component.html create mode 100644 zeppelin-frontend/src/app/share/note-rename/note-rename.component.less create mode 100644 zeppelin-frontend/src/app/share/note-rename/note-rename.component.ts create mode 100644 zeppelin-frontend/src/app/share/page-header/page-header.component.html create mode 100644 zeppelin-frontend/src/app/share/page-header/page-header.component.less create mode 100644 zeppelin-frontend/src/app/share/page-header/page-header.component.ts create mode 100644 zeppelin-frontend/src/app/share/pipes/humanize-bytes.pipe.ts create mode 100644 zeppelin-frontend/src/app/share/pipes/index.ts create mode 100644 zeppelin-frontend/src/app/share/pipes/public-api.ts create mode 100644 zeppelin-frontend/src/app/share/public-api.ts create mode 100644 zeppelin-frontend/src/app/share/run-scripts/run-scripts.directive.ts create mode 100644 zeppelin-frontend/src/app/share/share.module.ts create mode 100644 zeppelin-frontend/src/app/share/spin/spin.component.html create mode 100644 zeppelin-frontend/src/app/share/spin/spin.component.less create mode 100644 zeppelin-frontend/src/app/share/spin/spin.component.ts create mode 100644 zeppelin-frontend/src/app/spell/spell-result.ts diff --git a/zeppelin-frontend/src/app/app-http.interceptor.ts b/zeppelin-frontend/src/app/app-http.interceptor.ts new file mode 100644 index 00000000000..46762a4fa71 --- /dev/null +++ b/zeppelin-frontend/src/app/app-http.interceptor.ts @@ -0,0 +1,41 @@ +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { throwError, Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +import { isNil } from 'lodash'; + +import { environment } from '@zeppelin/environment'; +import { TicketService } from '@zeppelin/services'; + +@Injectable() +export class AppHttpInterceptor implements HttpInterceptor { + constructor(private ticketService: TicketService) {} + + // tslint:disable-next-line:no-any + intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { + let httpRequestUpdated = httpRequest.clone({ withCredentials: true }); + if (environment.production) { + httpRequestUpdated = httpRequest.clone({ setHeaders: { 'X-Requested-With': 'XMLHttpRequest' } }); + } + return next.handle(httpRequestUpdated).pipe( + map(event => { + if (event instanceof HttpResponse) { + return event.clone({ body: event.body.body }); + } else { + return event; + } + }), + catchError(event => { + const redirect = event.headers.get('Location'); + if (event.status === 401 && !isNil(redirect)) { + // Handle page redirect + window.location.href = redirect; + } else if (event.status === 405 && !event.url.contains('logout')) { + this.ticketService.logout().subscribe(); + } + return throwError(event); + }) + ); + } +} diff --git a/zeppelin-frontend/src/app/app-message.interceptor.ts b/zeppelin-frontend/src/app/app-message.interceptor.ts new file mode 100644 index 00000000000..c7183d7ed87 --- /dev/null +++ b/zeppelin-frontend/src/app/app-message.interceptor.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { NzModalService, NzNotificationService } from 'ng-zorro-antd'; + +import { MessageInterceptor } from '@zeppelin/interfaces'; +import { MessageReceiveDataTypeMap, OP, WebSocketMessage } from '@zeppelin/sdk'; +import { TicketService } from '@zeppelin/services'; + +@Injectable() +export class AppMessageInterceptor implements MessageInterceptor { + constructor( + private router: Router, + private nzNotificationService: NzNotificationService, + private ticketService: TicketService, + private nzModalService: NzModalService + ) {} + + received(data: WebSocketMessage): WebSocketMessage { + if (data.op === OP.NEW_NOTE) { + const rData = data.data as MessageReceiveDataTypeMap[OP.NEW_NOTE]; + this.router.navigate(['/notebook', rData.note.id]).then(); + } else if (data.op === OP.AUTH_INFO) { + const rData = data.data as MessageReceiveDataTypeMap[OP.AUTH_INFO]; + if (this.ticketService.ticket.roles === '[]') { + this.nzModalService.confirm({ + nzClosable: false, + nzMaskClosable: false, + nzTitle: 'Insufficient privileges', + nzContent: rData.info + }); + } else { + this.nzModalService.create({ + nzClosable: false, + nzMaskClosable: false, + nzTitle: 'Insufficient privileges', + nzContent: rData.info, + nzOkText: 'Login', + nzOnOk: () => { + this.router.navigate(['/login']).then(); + }, + nzOnCancel: () => { + this.router.navigate(['/']).then(); + } + }); + } + } else if (data.op === OP.ERROR_INFO) { + // tslint:disable-next-line:no-any + const rData = (data.data as any) as MessageReceiveDataTypeMap[OP.ERROR_INFO]; + if (rData.info) { + this.nzNotificationService.warning('ERROR', rData.info); + } + } + return data; + } +} diff --git a/zeppelin-frontend/src/app/app-routing.module.ts b/zeppelin-frontend/src/app/app-routing.module.ts index 06c734263e1..805cb239437 100644 --- a/zeppelin-frontend/src/app/app-routing.module.ts +++ b/zeppelin-frontend/src/app/app-routing.module.ts @@ -1,11 +1,24 @@ import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; +import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; - -const routes: Routes = []; +const routes: Routes = [ + { + path: '', + loadChildren: () => import('./pages/workspace/workspace.module').then(m => m.WorkspaceModule) + }, + { + path: 'login', + loadChildren: () => import('./pages/login/login.module').then(m => m.LoginModule) + } +]; @NgModule({ - imports: [RouterModule.forRoot(routes)], + imports: [ + RouterModule.forRoot(routes, { + useHash: true, + preloadingStrategy: PreloadAllModules + }) + ], exports: [RouterModule] }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts b/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts new file mode 100644 index 00000000000..96cfd186c2e --- /dev/null +++ b/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts @@ -0,0 +1,29 @@ +import { + Compiler, + CompilerFactory, + CompilerOptions, + COMPILER_OPTIONS, + StaticProvider, + ViewEncapsulation +} from '@angular/core'; +import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; + +const compilerOptions: CompilerOptions = { + useJit: true, + defaultEncapsulation: ViewEncapsulation.None +}; + +export function createCompiler(compilerFactory: CompilerFactory) { + return compilerFactory.createCompiler([compilerOptions]); +} + +export const RUNTIME_COMPILER_PROVIDERS: StaticProvider[] = [ + { provide: COMPILER_OPTIONS, useValue: compilerOptions, multi: true }, + { provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] }, + { provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] } +]; + +// TODO +// buildOptimizer false +// import 'core-js/es7/reflect'; +// https://github.com/angular/angular/issues/27584#issuecomment-446462051 diff --git a/zeppelin-frontend/src/app/app.component.html b/zeppelin-frontend/src/app/app.component.html index 7feacadc636..8e2fd10575a 100644 --- a/zeppelin-frontend/src/app/app.component.html +++ b/zeppelin-frontend/src/app/app.component.html @@ -1,538 +1,3 @@ - - - - - - - - - - - - - - -
- - -
- - - - - - - - - - - - {{ title }} app is running! - - - - - -
- - -

Resources

-

Here are some links to help you get started:

- - - - -

Next Steps

-

What do you want to do next with your app?

- - - -
-
- - - New Component -
- -
- - - Angular Material -
- -
- - - Add Dependency -
- -
- - - Run and Watch Tests -
- -
- - - Build for Production -
-
- - -
-
ng generate component xyz
-
ng add @angular/material
-
ng add _____
-
ng test
-
ng build --prod
-
- - - - - - - - - - - -
- - - - - - - - - - - - \ No newline at end of file + +Getting Ticket Data ... +Logging out ... diff --git a/zeppelin-frontend/src/app/app.component.less b/zeppelin-frontend/src/app/app.component.less index e69de29bb2d..89b27550ca0 100644 --- a/zeppelin-frontend/src/app/app.component.less +++ b/zeppelin-frontend/src/app/app.component.less @@ -0,0 +1,14 @@ +@import 'theme-mixin'; + +.themeMixin({ + .content { + background: @layout-body-background; + min-height: 100vh; + display: block; + position: relative; + + &.blur { + filter: blur(6px); + } + } +}); diff --git a/zeppelin-frontend/src/app/app.component.ts b/zeppelin-frontend/src/app/app.component.ts index d5056928fc1..4273469019a 100644 --- a/zeppelin-frontend/src/app/app.component.ts +++ b/zeppelin-frontend/src/app/app.component.ts @@ -1,10 +1,27 @@ import { Component } from '@angular/core'; +import { NavigationEnd, NavigationStart, Router } from '@angular/router'; +import { filter, map } from 'rxjs/operators'; + +import { TicketService } from '@zeppelin/services'; @Component({ - selector: 'app-root', + selector: 'zeppelin-root', templateUrl: './app.component.html', styleUrls: ['./app.component.less'] }) export class AppComponent { - title = 'zeppelin'; + logout$ = this.ticketService.logout$; + loading$ = this.router.events.pipe( + filter(data => data instanceof NavigationEnd || data instanceof NavigationStart), + map(data => { + if (data instanceof NavigationStart) { + // load ticket when redirect to workspace + return data.url === '/'; + } else if (data instanceof NavigationEnd) { + return false; + } + }) + ); + + constructor(private router: Router, private ticketService: TicketService) {} } diff --git a/zeppelin-frontend/src/app/app.module.ts b/zeppelin-frontend/src/app/app.module.ts index 2c3ba2995c8..8f7300453be 100644 --- a/zeppelin-frontend/src/app/app.module.ts +++ b/zeppelin-frontend/src/app/app.module.ts @@ -1,18 +1,76 @@ -import { BrowserModule } from '@angular/platform-browser'; +import { registerLocaleData } from '@angular/common'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import en from '@angular/common/locales/en'; import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Router, RouterModule } from '@angular/router'; + +import { ZeppelinHeliumModule } from '@zeppelin/helium'; +import { en_US, NzModalService, NzNotificationService, NZ_I18N } from 'ng-zorro-antd'; +import { NZ_CODE_EDITOR_CONFIG } from 'ng-zorro-antd/code-editor'; + +import { MESSAGE_INTERCEPTOR, TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; +import { loadMonacoLanguage } from '@zeppelin/languages'; +import { TicketService } from '@zeppelin/services'; +import { ShareModule } from '@zeppelin/share'; +import { AppHttpInterceptor } from './app-http.interceptor'; +import { AppMessageInterceptor } from './app-message.interceptor'; import { AppRoutingModule } from './app-routing.module'; +import { RUNTIME_COMPILER_PROVIDERS } from './app-runtime-compiler.providers'; import { AppComponent } from './app.component'; +export const loadMonaco = () => { + loadMonacoLanguage(); +}; + +registerLocaleData(en); + @NgModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent], imports: [ BrowserModule, - AppRoutingModule + FormsModule, + HttpClientModule, + BrowserAnimationsModule, + ShareModule, + AppRoutingModule, + RouterModule, + ZeppelinHeliumModule + ], + providers: [ + ...RUNTIME_COMPILER_PROVIDERS, + { + provide: NZ_I18N, + useValue: en_US + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AppHttpInterceptor, + multi: true, + deps: [TicketService] + }, + { + provide: NZ_CODE_EDITOR_CONFIG, + useValue: { + defaultEditorOption: { + scrollBeyondLastLine: false + }, + onLoad: loadMonaco + } + }, + { + provide: MESSAGE_INTERCEPTOR, + useClass: AppMessageInterceptor, + deps: [Router, NzNotificationService, TicketService, NzModalService] + }, + { + provide: TRASH_FOLDER_ID_TOKEN, + useValue: '~Trash' + } ], - providers: [], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule {} diff --git a/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts b/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts new file mode 100644 index 00000000000..3eded4ee484 --- /dev/null +++ b/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts @@ -0,0 +1,53 @@ +export function copyTextToClipboard(text: string): void { + const textArea: HTMLTextAreaElement = document.createElement('textarea'); + + // + // *** This styling is an extra step which is likely not required. *** + // + // Why is it here? To ensure: + // 1. the element is able to have focus and selection. + // 2. if element was to flash render it has minimal visual impact. + // 3. less flakyness with selection and copying which **might** occur if + // the textarea element is not visible. + // + // The likelihood is the element won't even render, not even a flash, + // so some of these are just precautions. However in IE the element + // is visible whilst the popup box asking the user for permission for + // the web page to copy to the clipboard. + // + + // Place in top-left corner of screen regardless of scroll position. + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = '0'; + + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; + + textArea.value = text; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + window.prompt('Copy to clipboard: Ctrl+C, Enter', text); + } + + document.body.removeChild(textArea); +} diff --git a/zeppelin-frontend/src/app/core/copy-text/index.ts b/zeppelin-frontend/src/app/core/copy-text/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/core/copy-text/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/copy-text/public-api.ts b/zeppelin-frontend/src/app/core/copy-text/public-api.ts new file mode 100644 index 00000000000..1335378243e --- /dev/null +++ b/zeppelin-frontend/src/app/core/copy-text/public-api.ts @@ -0,0 +1 @@ +export * from './copy-text-to-clipboard'; diff --git a/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts b/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts new file mode 100644 index 00000000000..d4cd6fea1d2 --- /dev/null +++ b/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts @@ -0,0 +1,11 @@ +import { OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +export class DestroyHookComponent implements OnDestroy { + readonly destroy$ = new Subject(); + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/core/destroy-hook/index.ts b/zeppelin-frontend/src/app/core/destroy-hook/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/core/destroy-hook/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts b/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts new file mode 100644 index 00000000000..851b581557a --- /dev/null +++ b/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts @@ -0,0 +1 @@ +export * from './destroy-hook.component'; diff --git a/zeppelin-frontend/src/app/core/public-api.ts b/zeppelin-frontend/src/app/core/public-api.ts index ef611e2851a..3eeb4271901 100644 --- a/zeppelin-frontend/src/app/core/public-api.ts +++ b/zeppelin-frontend/src/app/core/public-api.ts @@ -1 +1,3 @@ export * from './message-listener'; +export * from './destroy-hook'; +export * from './copy-text'; diff --git a/zeppelin-frontend/src/app/interfaces/index.ts b/zeppelin-frontend/src/app/interfaces/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/interfaces/interpreter.ts b/zeppelin-frontend/src/app/interfaces/interpreter.ts new file mode 100644 index 00000000000..1e3b5a7cc35 --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/interpreter.ts @@ -0,0 +1,92 @@ +export type InterpreterPropertyTypes = 'textarea' | 'string' | 'number' | 'url' | 'password' | 'checkbox'; + +export interface Interpreter { + id: string; + name: string; + group: string; + properties: Properties; + status: string; + errorReason?: string; + interpreterGroup: InterpreterGroupItem[]; + dependencies: DependenciesItem[]; + option: Option; +} + +export interface InterpreterMap { + [key: string]: Interpreter; +} + +export interface CreateInterpreterRepositoryForm { + id: string; + url: string; + snapshot: boolean; + username: string; + password: string; + proxyProtocol: string; + proxyHost: string; + proxyPort: string; + proxyLogin: string; + proxyPassword: string; +} + +export interface InterpreterRepository { + id: string; + type: string; + url: string; + releasePolicy: ReleasePolicy; + snapshotPolicy: SnapshotPolicy; + // tslint:disable-next-line + mirroredRepositories: any[]; + repositoryManager: boolean; +} +interface ReleasePolicy { + enabled: boolean; + updatePolicy: string; + checksumPolicy: string; +} +interface SnapshotPolicy { + enabled: boolean; + updatePolicy: string; + checksumPolicy: string; +} + +interface Properties { + [key: string]: { + name: string; + value: boolean; + type: string; + defaultValue?: string; + description?: string; + }; +} + +interface InterpreterGroupItem { + name: string; + class: string; + defaultInterpreter: boolean; + editor: Editor; +} +interface Editor { + language: string; + editOnDblClick: boolean; + completionKey?: string; + completionSupport?: boolean; +} + +interface DependenciesItem { + groupArtifactVersion: string; + local: boolean; + exclusions: string[]; +} + +interface Option { + remote: boolean; + port: number; + isExistingProcess: boolean; + setPermission: boolean; + // tslint:disable-next-line:no-any + owners: any[]; + isUserImpersonate: boolean; + perNote?: string; + perUser?: string; +} diff --git a/zeppelin-frontend/src/app/interfaces/message-interceptor.ts b/zeppelin-frontend/src/app/interfaces/message-interceptor.ts new file mode 100644 index 00000000000..ca126b5e874 --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/message-interceptor.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; +import { MessageReceiveDataTypeMap, WebSocketMessage } from '@zeppelin/sdk'; + +export interface MessageInterceptor { + received(data: WebSocketMessage): WebSocketMessage; +} + +export const MESSAGE_INTERCEPTOR = new InjectionToken('MESSAGE_INTERCEPTOR'); diff --git a/zeppelin-frontend/src/app/interfaces/node-list.ts b/zeppelin-frontend/src/app/interfaces/node-list.ts new file mode 100644 index 00000000000..da4854e5e3a --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/node-list.ts @@ -0,0 +1,29 @@ +export interface NodeList { + root: RootNode; + flatList: FlatListNodeItem[]; + flatFolderMap: FlatFolderNodeMap; +} + +export interface RootNode { + children: NodeItem[]; +} + +export interface NodeItem { + id: string; + title: string; + isLeaf?: boolean; + expanded?: boolean; + children?: NodeItem[]; + isTrash: boolean; + path?: string; +} + +interface FlatListNodeItem { + id: string; + path: string; + isTrash: boolean; +} + +interface FlatFolderNodeMap { + [title: string]: NodeItem; +} diff --git a/zeppelin-frontend/src/app/interfaces/public-api.ts b/zeppelin-frontend/src/app/interfaces/public-api.ts new file mode 100644 index 00000000000..11d21360bea --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/public-api.ts @@ -0,0 +1,5 @@ +export * from './ticket'; +export * from './trash-folder-id'; +export * from './interpreter'; +export * from './message-interceptor'; +export * from './security'; diff --git a/zeppelin-frontend/src/app/interfaces/security.ts b/zeppelin-frontend/src/app/interfaces/security.ts new file mode 100644 index 00000000000..b7d769c75a3 --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/security.ts @@ -0,0 +1,11 @@ +export interface SecurityUserList { + roles: string[]; + users: string[]; +} + +export interface Permissions { + readers: string[]; + owners: string[]; + writers: string[]; + runners: string[]; +} diff --git a/zeppelin-frontend/src/app/interfaces/ticket.ts b/zeppelin-frontend/src/app/interfaces/ticket.ts new file mode 100644 index 00000000000..542d8617586 --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/ticket.ts @@ -0,0 +1,17 @@ +export class ITicket { + principal = ''; + ticket = ''; + redirectURL = ''; + roles = ''; +} + +export class IZeppelinVersion { + 'git-commit-id': string; + 'git-timestamp': string; + 'version': string; +} + +export class ITicketWrapped extends ITicket { + init = false; + screenUsername = ''; +} diff --git a/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts b/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts new file mode 100644 index 00000000000..11ca0045165 --- /dev/null +++ b/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const TRASH_FOLDER_ID_TOKEN = new InjectionToken('TRASH_FOLDER_ID'); diff --git a/zeppelin-frontend/src/app/languages/index.ts b/zeppelin-frontend/src/app/languages/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/languages/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/languages/load.ts b/zeppelin-frontend/src/app/languages/load.ts new file mode 100644 index 00000000000..8d1d7c25206 --- /dev/null +++ b/zeppelin-frontend/src/app/languages/load.ts @@ -0,0 +1,7 @@ +import { conf as ScalaConf, language as ScalaLanguage } from './scala'; + +export const loadMonacoLanguage = () => { + monaco.languages.register({ id: 'scala' }); + monaco.languages.setMonarchTokensProvider('scala', ScalaLanguage); + monaco.languages.setLanguageConfiguration('scala', ScalaConf); +}; diff --git a/zeppelin-frontend/src/app/languages/public-api.ts b/zeppelin-frontend/src/app/languages/public-api.ts new file mode 100644 index 00000000000..d3eca94a978 --- /dev/null +++ b/zeppelin-frontend/src/app/languages/public-api.ts @@ -0,0 +1,2 @@ +export * from './scala'; +export * from './load'; diff --git a/zeppelin-frontend/src/app/languages/scala.ts b/zeppelin-frontend/src/app/languages/scala.ts new file mode 100644 index 00000000000..bee7f545199 --- /dev/null +++ b/zeppelin-frontend/src/app/languages/scala.ts @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import IRichLanguageConfiguration = monaco.languages.LanguageConfiguration; +import ILanguage = monaco.languages.IMonarchLanguage; + +export const conf: IRichLanguageConfiguration = { + // the default separators except `@$` + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g, + comments: { + lineComment: '//', + blockComment: ['/*', '*/'] + }, + brackets: [['{', '}'], ['[', ']'], ['(', ')']], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + { open: '<', close: '>' } + ] +}; + +export const language = { + defaultToken: '', + tokenPostfix: '.scala', + + keywords: [ + // From https://www.scala-lang.org/files/archive/spec/2.11/01-lexical-syntax.html + 'abstract', + 'case', + 'catch', + 'class', + 'def', + 'do', + 'else', + 'extends', + 'false', + 'final', + 'finally', + 'for', + 'forSome', + 'if', + 'implicit', + 'import', + 'lazy', + 'macro', + 'match', + 'new', + 'null', + 'object', + 'override', + 'package', + 'private', + 'protected', + 'return', + 'sealed', + 'super', + 'this', + 'throw', + 'trait', + 'try', + 'true', + 'type', + 'val', + 'var', + 'while', + 'with', + 'yield' + ], + + operators: [ + '_', + ':', + '=', + '=>', + '<-', + '<:', + '<%', + '>:', + '#', + '@', + + // Copied from java.ts, to be validated + '=', + '>', + '<', + '!', + '~', + '?', + ':', + '==', + '<=', + '>=', + '!=', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '<<', + '>>', + '>>>', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '%=', + '<<=', + '>>=', + '>>>=' + ], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '' + } + } + ], + + // @ annotations. + [/@\s*[a-zA-Z_\$][\w\$]*/, 'annotation'], + + // numbers + [/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/, 'number.float'], + [/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/, 'number.float'], + [/0[xX](@hexdigits)[Ll]?/, 'number.hex'], + [/0(@octaldigits)[Ll]?/, 'number.octal'], + [/0[bB](@binarydigits)[Ll]?/, 'number.binary'], + [/(@digits)[fFdD]/, 'number.float'], + [/(@digits)[lL]?/, 'number'], + + // delimiter: after number because of .\d floats + [/[;,.]/, 'delimiter'], + + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [/"/, 'string', '@string'], + + // characters + [/'[^\\']'/, 'string'], + [/(')(@escapes)(')/, ['string', 'string.escape', 'string']], + [/'/, 'string.invalid'] + ], + + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*\*(?!\/)/, 'comment.doc', '@scaladoc'], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'] + ], + + comment: [ + [/[^\/*]+/, 'comment'], + // [/\/\*/, 'comment', '@push' ], // nested comment not allowed :-( + // [/\/\*/, 'comment.invalid' ], // this breaks block comments in the shape of /* //*/ + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'] + ], + // Identical copy of comment above, except for the addition of .doc + scaladoc: [ + [/[^\/*]+/, 'comment.doc'], + // [/\/\*/, 'comment.doc', '@push' ], // nested comment not allowed :-( + [/\/\*/, 'comment.doc.invalid'], + [/\*\//, 'comment.doc', '@pop'], + [/[\/*]/, 'comment.doc'] + ], + + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'] + ] + } +} as ILanguage; diff --git a/zeppelin-frontend/src/app/pages/login/login-routing.module.ts b/zeppelin-frontend/src/app/pages/login/login-routing.module.ts new file mode 100644 index 00000000000..32f29152ebe --- /dev/null +++ b/zeppelin-frontend/src/app/pages/login/login-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { LoginComponent } from './login.component'; + +const routes: Routes = [ + { + path: '', + component: LoginComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class LoginRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/login/login.component.html b/zeppelin-frontend/src/app/pages/login/login.component.html new file mode 100644 index 00000000000..6ef9b161e04 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/login/login.component.html @@ -0,0 +1,34 @@ +
+
+
+
+ + User Name + + + + + + + + Password + + + + + + + + + + + +
+
+ +
+
diff --git a/zeppelin-frontend/src/app/pages/login/login.component.less b/zeppelin-frontend/src/app/pages/login/login.component.less new file mode 100644 index 00000000000..902c59e1f5c --- /dev/null +++ b/zeppelin-frontend/src/app/pages/login/login.component.less @@ -0,0 +1,57 @@ +@import "theme-mixin"; + +.themeMixin({ + .content { + height: 100vh; + width: 100vw; + + &:after { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("../../../assets/images/bg.jpg"); + background-size: cover; + filter: blur(4px); + background-repeat: no-repeat; + background-position: center; + } + + .inner { + width: 800px; + height: 300px; + position: absolute; + z-index: 1; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: @box-shadow-base; + + .form { + width: 500px; + position: absolute; + left: 0; + height: 100%; + background: @component-background; + padding: 36px; + } + + .sidebar { + width: 300px; + position: absolute; + right: 0; + height: 100%; + background: @primary-color; + padding: 24px 36px; + color: @text-color-dark; + + h1 { + color: @heading-color-dark; + } + } + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/login/login.component.ts b/zeppelin-frontend/src/app/pages/login/login.component.ts new file mode 100644 index 00000000000..acd687f2480 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/login/login.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { TicketService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LoginComponent implements OnInit { + userName: string; + password: string; + loading = false; + + login() { + this.loading = true; + this.ticketService.login(this.userName, this.password).subscribe( + () => { + this.loading = false; + this.cdr.markForCheck(); + this.router.navigate(['/']).then(); + }, + () => { + this.loading = false; + this.cdr.markForCheck(); + } + ); + } + + constructor(private ticketService: TicketService, private cdr: ChangeDetectorRef, private router: Router) {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/pages/login/login.module.ts b/zeppelin-frontend/src/app/pages/login/login.module.ts new file mode 100644 index 00000000000..bb1f7a3465b --- /dev/null +++ b/zeppelin-frontend/src/app/pages/login/login.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzButtonModule, NzFormModule, NzIconModule, NzInputModule } from 'ng-zorro-antd'; + +import { LoginRoutingModule } from './login-routing.module'; +import { LoginComponent } from './login.component'; + +@NgModule({ + declarations: [LoginComponent], + imports: [CommonModule, LoginRoutingModule, FormsModule, NzFormModule, NzInputModule, NzButtonModule, NzIconModule] +}) +export class LoginModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts new file mode 100644 index 00000000000..9c008c0a7ec --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { HomeComponent } from './home.component'; + +const routes: Routes = [ + { + path: '', + component: HomeComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HomeRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.html b/zeppelin-frontend/src/app/pages/workspace/home/home.component.html new file mode 100644 index 00000000000..922ceaf3dba --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.html @@ -0,0 +1,29 @@ +
+
+

+ Welcome to Zeppelin! +

+ Zeppelin is web-based notebook that enables interactive data analytics.
+ You can make beautiful data-driven, interactive, collaborative document with SQL, code and even more!
+
+
+
+

Notebook

+ +
+
+

Help

+ Get started with Zeppelin + documentation
+ +

Community

+ Please feel free to help us to improve Zeppelin,
+ Any contribution are welcome!

+ Mailing list
+ Issues + tracking
+ Github +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.less b/zeppelin-frontend/src/app/pages/workspace/home/home.component.less new file mode 100644 index 00000000000..2dd9d3ad1d0 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.less @@ -0,0 +1,25 @@ +@import 'theme-mixin'; + +.themeMixin({ + .content { + margin: 12px; + border: 1px solid @border-color-base; + padding: 24px; + background-color: @component-background; + position: relative; + background-image: url(../../../../assets/images/zeppelin_svg_logo_bg.svg); + background-repeat: no-repeat; + background-position: right bottom; + } + .welcome { + margin-bottom: 12px; + } + .more-info { + h3 { + margin-top: 0.5em; + } + } + .refresh-note { + margin-left: 6px + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts b/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts new file mode 100644 index 00000000000..c2899555dc9 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { OP } from '@zeppelin/sdk'; +import { MessageService, TicketService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HomeComponent extends MessageListenersManager implements OnInit { + loading = false; + + reloadNoteList() { + this.messageService.reloadAllNotesFromRepo(); + this.loading = true; + } + + @MessageListener(OP.NOTES_INFO) + getNotes() { + this.loading = false; + this.cdr.markForCheck(); + } + + constructor( + public ticketService: TicketService, + public messageService: MessageService, + private cdr: ChangeDetectorRef + ) { + super(messageService); + } + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts b/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts new file mode 100644 index 00000000000..6fbf65c2aff --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzGridModule, NzIconModule, NzToolTipModule } from 'ng-zorro-antd'; + +import { ShareModule } from '@zeppelin/share'; + +import { HomeRoutingModule } from './home-routing.module'; +import { HomeComponent } from './home.component'; + +@NgModule({ + declarations: [HomeComponent], + imports: [CommonModule, HomeRoutingModule, NzGridModule, NzIconModule, NzToolTipModule, ShareModule] +}) +export class HomeModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.html new file mode 100644 index 00000000000..3e761a3d51e --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.html @@ -0,0 +1,227 @@ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
  • + {{r.message}} + + {{(r.time * 1000 | date: 'MMMM dd yyyy, h:mm:ss a') || 'Current'}} +
  • +
+
+
+ + + + + + + + + + + + + +
+ Run note with cron scheduler. + Either choose from preset or write your own + + cron expression + + . +
+ - Preset + {{cr.name}} +
+
+ - Preset + +

+ {{note.info.cron}} +

+
+
+ +
+
+
+
+
+
+ + + + + + +
    +
  • {{lf}}
  • +
+
+
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.less new file mode 100644 index 00000000000..fbec0f871d9 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.less @@ -0,0 +1,93 @@ +@import "theme-mixin"; + +.scheduler { + width: 300px; + + .cron-preset { + margin-right: 6px; + + &.selected { + font-weight: bold; + } + } + + > div { + line-height: 24px; + + span { + margin-right: 6px; + } + + input { + display: inline-block; + width: 160px; + } + } +} + +.themeMixin({ + nz-dropdown-button { + height: 24px; + line-height: 22px; + margin-left: -1px; + } + .bar { + height: 50px; + background: @component-background; + box-shadow: -2px 4px 2px 0 rgba(0, 0, 0, 0.06); + padding: 0 15px; + position: absolute; + width: 100%; + top: 0; + line-height: 50px; + + &.simple { + box-shadow: none; + + .control { + display: none; + } + + .setting { + display: none; + } + + &:hover { + .control { + display: block; + } + + .setting { + display: block; + } + } + } + + .title { + float: left; + width: auto; + max-width: 40%; + } + + .control { + float: left; + + nz-button-group { + margin-right: 24px; + + &:last-child { + margin-right: 0; + } + } + } + + .setting { + float: right; + + button { + box-shadow: none; + border: none; + } + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts new file mode 100644 index 00000000000..2643244adb1 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts @@ -0,0 +1,281 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NzMessageService, NzModalService } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; +import { Note, OP, RevisionListItem } from '@zeppelin/sdk'; +import { MessageService, NoteActionService, NoteStatusService, SaveAsService, TicketService } from '@zeppelin/services'; + +import { NoteCreateComponent } from '@zeppelin/share/note-create/note-create.component'; + +@Component({ + selector: 'zeppelin-notebook-action-bar', + templateUrl: './action-bar.component.html', + styleUrls: ['./action-bar.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookActionBarComponent extends MessageListenersManager implements OnInit { + @Input() note: Note['note']; + @Input() isOwner = true; + @Input() looknfeel: string; + @Input() noteRevisions: RevisionListItem[] = []; + @Input() currentRevision: string; + @Input() collaborativeMode = false; + @Input() collaborativeModeUsers = []; + @Input() revisionView = false; + @Input() activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide'; + @Output() readonly activatedExtensionChange = new EventEmitter< + 'interpreter' | 'permissions' | 'revisions' | 'hide' + >(); + @Output() readonly editorHideChange = new EventEmitter(); + @Output() readonly tableHideChange = new EventEmitter(); + lfOption: Array<'report' | 'default' | 'simple'> = ['default', 'simple', 'report']; + isNoteParagraphRunning = false; + principal = this.ticketService.ticket.principal; + editorHide = false; + commitVisible = false; + tableHide = false; + isRevisionSupported = JSON.parse(this.ticketService.configuration.isRevisionSupported); + cronOption = [ + { name: 'None', value: undefined }, + { name: '1m', value: '0 0/1 * * * ?' }, + { name: '5m', value: '0 0/5 * * * ?' }, + { name: '1h', value: '0 0 0/1 * * ?' }, + { name: '3h', value: '0 0 0/3 * * ?' }, + { name: '6h', value: '0 0 0/6 * * ?' }, + { name: '12h', value: '0 0 0/12 * * ?' }, + { name: '1d', value: '0 0 0 * * ?' } + ]; + + updateNoteName(name: string) { + const trimmedNewName = name.trim(); + if (trimmedNewName.length > 0 && this.note.name !== trimmedNewName) { + this.note.name = trimmedNewName; + this.messageService.noteRename(this.note.id, this.note.name, true); + } + } + + visitRevision(revision: RevisionListItem) { + if (revision.id) { + if (revision.id === 'Head') { + this.router.navigate(['/notebook', this.activatedRoute.snapshot.params.noteId]).then(); + } else { + this.router.navigate(['/notebook', this.activatedRoute.snapshot.params.noteId, 'revision', revision.id]).then(); + } + } else { + this.nzMessageService.warning('There is a problem with this Revision'); + } + } + + checkpointNote(value: string, e: MouseEvent) { + e.preventDefault(); + this.commitVisible = false; + this.messageService.checkpointNote(this.note.id, value); + } + + setNoteRevision() { + const { revisionId } = this.activatedRoute.snapshot.params; + if (revisionId) { + this.nzModalService.confirm({ + nzTitle: 'Set revision', + nzContent: 'Set notebook head to current revision?', + nzOnOk: () => { + this.messageService.setNoteRevision(this.note.id, revisionId); + } + }); + } + } + + toggleExtension(extension: 'interpreter' | 'permissions' | 'revisions' | 'hide') { + if (this.activatedExtension === extension) { + this.activatedExtension = 'hide'; + } else { + this.activatedExtension = extension; + } + this.activatedExtensionChange.emit(this.activatedExtension); + } + + @MessageListener(OP.PARAGRAPH) + paragraphUpdate() { + this.updateIsNoteParagraphRunning(); + this.cdr.markForCheck(); + } + + runAllParagraphs() { + this.messageService.runAllParagraphs( + this.note.id, + this.note.paragraphs.map(p => { + return { + id: p.id, + title: p.title, + paragraph: p.text, + config: p.config, + params: p.settings.params + }; + }) + ); + } + + clearAllParagraphOutput() { + this.messageService.paragraphClearAllOutput(this.note.id); + } + + setCronScheduler(cronExpr: string) { + if (cronExpr) { + if (!this.note.config.cronExecutingUser) { + this.note.config.cronExecutingUser = this.ticketService.ticket.principal; + } + if (!this.note.config.cronExecutingRoles) { + this.note.config.cronExecutingRoles = this.ticketService.ticket.roles; + } + } else { + this.note.config.cronExecutingUser = ''; + this.note.config.cronExecutingRoles = ''; + } + this.note.config.cron = cronExpr; + this.setConfig(); + } + + setReleaseResource(releaseresource: boolean) { + this.note.config.releaseresource = releaseresource; + this.setConfig(); + } + + setConfig() { + // TODO + } + + cloneNote() { + this.nzModalService.create({ + nzTitle: 'Clone Note', + nzContent: NoteCreateComponent, + nzComponentParams: { + cloneNote: this.note + }, + nzFooter: null + }); + } + + exportNote() { + const sizeLimit = +this.ticketService.configuration['zeppelin.websocket.max.text.message.size']; + const jsonContent = JSON.stringify(this.note); + if (jsonContent.length > sizeLimit) { + this.nzModalService.confirm({ + nzTitle: `Note size exceeds importable limit (${sizeLimit})`, + nzContent: 'Do you still want to export this note?', + nzOnOk: () => { + this.saveAsService.saveAs(jsonContent, this.note.name, 'zpln'); + } + }); + } else { + this.saveAsService.saveAs(jsonContent, this.note.name, 'zpln'); + } + } + + toggleAllEditor() { + this.editorHide = !this.editorHide; + this.editorHideChange.emit(this.editorHide); + } + + toggleAllTable() { + this.tableHide = !this.tableHide; + this.tableHideChange.emit(this.tableHide); + } + + searchCode() { + // TODO + } + + deleteNote() { + this.messageService.deleteNote(this.note.id); + } + + moveNoteToTrash() { + this.messageService.moveNoteToTrash(this.note.id); + } + + get isTrash() { + return this.noteStatusService.isTrash(this.note); + } + + get viewOnly(): boolean { + return this.noteStatusService.viewOnly(this.note); + } + + updateIsNoteParagraphRunning() { + this.isNoteParagraphRunning = this.noteStatusService.isNoteParagraphRunning(this.note); + } + + showShortCut() { + // TODO + } + + togglePermissions() { + this.toggleExtension('permissions'); + } + + setLookAndFeel(lf: 'report' | 'default' | 'simple') { + this.note.config.looknfeel = lf; + if (!this.activatedRoute.snapshot.params.revisionId) { + this.messageService.updateNote(this.note.id, this.note.name, this.note.config); + } + } + + toggleNotePersonalizedMode() { + const modeText = this.note.config.personalizedMode === 'true' ? 'collaborate' : 'personalize'; + if (this.isOwner) { + this.nzModalService.confirm({ + nzTitle: 'Setting the result display', + nzContent: `Do you want to ${modeText} your analysis?`, + nzOnOk: () => { + if (this.note.config.personalizedMode === undefined || this.note.config.personalizedMode === 'true') { + this.note.config.personalizedMode = 'false'; + } else { + this.note.config.personalizedMode = 'true'; + } + this.messageService.updatePersonalizedMode(this.note.id, this.note.config.personalizedMode); + } + }); + } + } + + get getCronOptionNameFromValue() { + if (!this.note.config.cron) { + return ''; + } else if (this.cronOption.find(cron => cron.value === this.note.config.cron)) { + return this.cronOption.find(cron => cron.value === this.note.config.cron).name; + } else { + return this.note.config.cron; + } + } + + constructor( + public messageService: MessageService, + private nzModalService: NzModalService, + private ticketService: TicketService, + private nzMessageService: NzMessageService, + private router: Router, + private cdr: ChangeDetectorRef, + private noteActionService: NoteActionService, + private noteStatusService: NoteStatusService, + @Inject(TRASH_FOLDER_ID_TOKEN) public TRASH_FOLDER_ID: string, + private activatedRoute: ActivatedRoute, + private saveAsService: SaveAsService + ) { + super(messageService); + } + + ngOnInit(): void { + this.updateIsNoteParagraphRunning(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.html new file mode 100644 index 00000000000..7b7af7b04df --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.html @@ -0,0 +1,5 @@ + diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less new file mode 100644 index 00000000000..4b63645644f --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less @@ -0,0 +1,38 @@ +@import "theme-mixin"; + +.themeMixin({ + .add-paragraph { + height: 32px; + text-align: center; + margin: -12px 0; + color: @primary-color; + font-weight: 500; + position: relative;; + + .inner { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 10; + display: none; + line-height: 30px; + background: @blue-1; + border: 1px solid @border-color-split; + box-shadow: @btn-shadow; + padding: 0 12px; + + &.disabled { + cursor: default; + color: @disabled-color; + } + } + + &:hover { + .inner { + display: block; + } + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts new file mode 100644 index 00000000000..230af235258 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'zeppelin-notebook-add-paragraph', + templateUrl: './add-paragraph.component.html', + styleUrls: ['./add-paragraph.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookAddParagraphComponent implements OnInit { + @Output() readonly addParagraph = new EventEmitter(); + @Input() disabled = false; + + clickAdd() { + if (!this.disabled) { + this.addParagraph.emit(); + } + } + + constructor() {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html new file mode 100644 index 00000000000..245a050e4fc --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html @@ -0,0 +1,43 @@ +
+
+

Settings

+
+ +
+

Interpreter binding

+

+ Bind interpreter for this note. + Click to Bind/Unbind interpreter. + Drag and drop to reorder interpreters.
+ The first interpreter on the list becomes default. To create/remove interpreters, go to + Interpreter + menu. +

+
+
+
+
+ + + +
+ +
+
+
+
+ + +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less new file mode 100644 index 00000000000..7774ba7f9c1 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less @@ -0,0 +1,18 @@ +@import "theme-mixin"; + +.themeMixin({ + .interpreter-binding { + .interpreter-list { + background: @layout-body-background; + padding: 12px; + } + + .submit-interpreter { + margin-top: 12px; + + button { + margin-right: 12px; + } + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts new file mode 100644 index 00000000000..8d7e7b1d8be --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts @@ -0,0 +1,69 @@ +import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; + +import { NzModalService } from 'ng-zorro-antd'; + +import { InterpreterBindingItem } from '@zeppelin/sdk'; +import { InterpreterService, MessageService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-notebook-interpreter-binding', + templateUrl: './interpreter-binding.component.html', + styleUrls: ['./interpreter-binding.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookInterpreterBindingComponent { + private restarting = false; + @Input() noteId: string; + @Input() interpreterBindings: InterpreterBindingItem[] = []; + @Input() activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide'; + @Output() readonly activatedExtensionChange = new EventEmitter< + 'interpreter' | 'permissions' | 'revisions' | 'hide' + >(); + + restartInterpreter(interpreter: InterpreterBindingItem) { + this.nzModalService.create({ + nzTitle: 'Restart interpreter', + nzContent: `Do you want to restart ${interpreter.name} interpreter?`, + nzOkLoading: this.restarting, + nzOnOk: () => + new Promise(resolve => { + this.restarting = true; + this.interpreterService.restartInterpreter(interpreter.id, this.noteId).subscribe( + data => { + this.restarting = false; + this.cdr.markForCheck(); + resolve(); + }, + () => { + this.restarting = false; + resolve(); + } + ); + }) + }); + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.interpreterBindings, event.previousIndex, event.currentIndex); + } + + saveSetting() { + const selectedSettingIds = this.interpreterBindings.filter(item => item.selected).map(item => item.id); + this.messageService.saveInterpreterBindings(this.noteId, selectedSettingIds); + this.messageService.getInterpreterBindings(this.noteId); + this.closeSetting(); + } + + closeSetting() { + this.activatedExtension = 'hide'; + this.activatedExtensionChange.emit('hide'); + } + + constructor( + private nzModalService: NzModalService, + private interpreterService: InterpreterService, + private cdr: ChangeDetectorRef, + private messageService: MessageService + ) {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts new file mode 100644 index 00000000000..8d4bb780e37 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { NotebookComponent } from './notebook.component'; + +const routes: Routes = [ + { + path: ':noteId', + component: NotebookComponent + }, + { + path: ':noteId/paragraph/:paragraphId', + component: NotebookComponent + }, + { + path: ':noteId/revision/:revisionId', + component: NotebookComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class NotebookRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html new file mode 100644 index 00000000000..40e318011ae --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html @@ -0,0 +1,42 @@ +
+ +
+ + + +
+
+
+ +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.less new file mode 100644 index 00000000000..a7164645427 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.less @@ -0,0 +1,24 @@ +@import "theme-mixin"; + + +.themeMixin({ + .notebook-container { + background: @layout-body-background; + display: block; + position: relative; + padding-top: 50px; + min-height: calc(~"100vh - 50px"); + + &.simple { + background: @component-background; + } + } + .extension-area { + padding: 24px; + background: @component-background; + box-shadow: -2px 4px 2px 0 rgba(0, 0, 0, 0.06); + } + .paragraph-area { + margin: 6px 0 0 0; + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.ts new file mode 100644 index 00000000000..6a9c83415a0 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.ts @@ -0,0 +1,303 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + QueryList, + ViewChildren +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { isNil } from 'lodash'; +import { Subject } from 'rxjs'; +import { distinctUntilKeyChanged, takeUntil } from 'rxjs/operators'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { Permissions } from '@zeppelin/interfaces'; +import { InterpreterBindingItem, MessageReceiveDataTypeMap, Note, OP, RevisionListItem } from '@zeppelin/sdk'; +import { + MessageService, + NoteStatusService, + NoteVarShareService, + SecurityService, + TicketService +} from '@zeppelin/services'; + +import { NotebookParagraphComponent } from './paragraph/paragraph.component'; + +@Component({ + selector: 'zeppelin-notebook', + templateUrl: './notebook.component.html', + styleUrls: ['./notebook.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookComponent extends MessageListenersManager implements OnInit, OnDestroy { + @ViewChildren(NotebookParagraphComponent) listOfNotebookParagraphComponent: QueryList; + private destroy$ = new Subject(); + note: Note['note']; + permissions: Permissions; + isOwner = true; + noteRevisions: RevisionListItem[] = []; + currentRevision: string; + collaborativeMode = false; + revisionView = false; + collaborativeModeUsers = []; + isNoteDirty = false; + saveTimer = null; + interpreterBindings: InterpreterBindingItem[] = []; + activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide'; + + @MessageListener(OP.NOTE) + getNote(data: MessageReceiveDataTypeMap[OP.NOTE]) { + const note = data.note; + if (isNil(note)) { + this.router.navigate(['/']).then(); + } else { + this.note = note; + const { paragraphId } = this.activatedRoute.snapshot.params; + if (paragraphId) { + this.note = this.cleanParagraphExcept(paragraphId); + this.initializeLookAndFeel(); + } else { + this.initializeLookAndFeel(); + this.getInterpreterBindings(); + this.getPermissions(); + this.note.config.personalizedMode = + this.note.config.personalizedMode === undefined ? 'false' : this.note.config.personalizedMode; + } + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.INTERPRETER_BINDINGS) + loadInterpreterBindings(data: MessageReceiveDataTypeMap[OP.INTERPRETER_BINDINGS]) { + this.interpreterBindings = data.interpreterBindings; + if (!this.interpreterBindings.some(item => item.selected)) { + this.activatedExtension = 'interpreter'; + } + this.cdr.markForCheck(); + } + + @MessageListener(OP.PARAGRAPH_REMOVED) + removeParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_REMOVED]) { + const { paragraphId } = this.activatedRoute.snapshot.params; + if (paragraphId || this.revisionView) { + return; + } + this.note.paragraphs = this.note.paragraphs.filter(p => p.id !== data.id); + this.cdr.markForCheck(); + } + + @MessageListener(OP.PARAGRAPH_ADDED) + addParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_ADDED]) { + const { paragraphId } = this.activatedRoute.snapshot.params; + if (paragraphId || this.revisionView) { + return; + } + this.note.paragraphs.splice(data.index, 0, data.paragraph).map(p => { + return { + ...p, + focus: p.id === data.paragraph.id + }; + }); + this.note.paragraphs = [...this.note.paragraphs]; + this.cdr.markForCheck(); + // TODO focus on paragraph + } + + @MessageListener(OP.SAVE_NOTE_FORMS) + saveNoteForms(data: MessageReceiveDataTypeMap[OP.SAVE_NOTE_FORMS]) { + this.note.noteForms = data.formsData.forms; + this.note.noteParams = data.formsData.params; + } + + @MessageListener(OP.NOTE_REVISION) + getNoteRevision(data: MessageReceiveDataTypeMap[OP.NOTE_REVISION]) { + const note = data.note; + if (isNil(note)) { + this.router.navigate(['/']).then(); + } else { + this.note = data.note; + this.initializeLookAndFeel(); + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.SET_NOTE_REVISION) + setNoteRevision() { + const { noteId } = this.activatedRoute.snapshot.params; + this.router.navigate(['/notebook', noteId]).then(); + } + + @MessageListener(OP.PARAGRAPH_MOVED) + moveParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_MOVED]) { + if (!this.revisionView) { + const movedPara = this.note.paragraphs.find(p => p.id === data.id); + if (movedPara) { + const listOfRestPara = this.note.paragraphs.filter(p => p.id !== data.id); + this.note.paragraphs = [...listOfRestPara.slice(0, data.index), movedPara, ...listOfRestPara.slice(data.index)]; + this.cdr.markForCheck(); + } + } + } + + @MessageListener(OP.COLLABORATIVE_MODE_STATUS) + getCollaborativeModeStatus(data: MessageReceiveDataTypeMap[OP.COLLABORATIVE_MODE_STATUS]) { + this.collaborativeMode = Boolean(data.status); + this.collaborativeModeUsers = data.users; + this.cdr.markForCheck(); + } + + @MessageListener(OP.PATCH_PARAGRAPH) + patchParagraph() { + this.collaborativeMode = true; + this.cdr.markForCheck(); + } + + @MessageListener(OP.NOTE_UPDATED) + noteUpdated(data: MessageReceiveDataTypeMap[OP.NOTE_UPDATED]) { + if (data.name !== this.note.name) { + this.note.name = data.name; + } + this.note.config = data.config; + this.note.info = data.info; + this.initializeLookAndFeel(); + this.cdr.markForCheck(); + } + + @MessageListener(OP.LIST_REVISION_HISTORY) + listRevisionHistory(data: MessageReceiveDataTypeMap[OP.LIST_REVISION_HISTORY]) { + this.noteRevisions = data.revisionList; + if (this.noteRevisions) { + if (this.noteRevisions.length === 0 || this.noteRevisions[0].id !== 'Head') { + this.noteRevisions.splice(0, 0, { id: 'Head', message: 'Head' }); + } + const { revisionId } = this.activatedRoute.snapshot.params; + if (revisionId) { + this.currentRevision = this.noteRevisions.find(r => r.id === revisionId).message; + } else { + this.currentRevision = 'Head'; + } + } + this.cdr.markForCheck(); + } + + saveParagraph(id: string) { + this.listOfNotebookParagraphComponent + .toArray() + .find(p => p.paragraph.id === id) + .saveParagraph(); + } + + killSaveTimer() { + if (this.saveTimer) { + clearTimeout(this.saveTimer); + this.saveTimer = null; + } + } + + startSaveTimer() { + this.killSaveTimer(); + this.isNoteDirty = true; + this.saveTimer = setTimeout(() => { + this.saveNote(); + }, 10000); + } + + saveNote() { + if (this.note && this.note.paragraphs && this.listOfNotebookParagraphComponent) { + this.listOfNotebookParagraphComponent.toArray().forEach(p => { + p.saveParagraph(); + }); + this.isNoteDirty = null; + this.cdr.markForCheck(); + } + } + + getInterpreterBindings() { + this.messageService.getInterpreterBindings(this.note.id); + } + + getPermissions() { + this.securityService.getPermissions(this.note.id).subscribe(data => { + this.permissions = data; + this.isOwner = !( + this.permissions.owners.length && this.permissions.owners.indexOf(this.ticketService.ticket.principal) < 0 + ); + this.cdr.markForCheck(); + }); + } + + get viewOnly(): boolean { + return this.noteStatusService.viewOnly(this.note); + } + + initializeLookAndFeel() { + this.note.config.looknfeel = this.note.config.looknfeel || 'default'; + if (this.note.paragraphs && this.note.paragraphs[0]) { + this.note.paragraphs[0].focus = true; + } + } + + cleanParagraphExcept(paragraphId) { + const targetParagraph = this.note.paragraphs.find(p => p.id === paragraphId); + const config = targetParagraph.config || {}; + config.editorHide = true; + config.tableHide = false; + const paragraphs = [{ ...targetParagraph, config }]; + return { ...this.note, paragraphs }; + } + + setAllParagraphTableHide(tableHide: boolean) { + this.listOfNotebookParagraphComponent.forEach(p => p.setTableHide(tableHide)); + } + + setAllParagraphEditorHide(editorHide: boolean) { + this.listOfNotebookParagraphComponent.forEach(p => p.setEditorHide(editorHide)); + } + + constructor( + private activatedRoute: ActivatedRoute, + public messageService: MessageService, + private cdr: ChangeDetectorRef, + private noteStatusService: NoteStatusService, + private noteVarShareService: NoteVarShareService, + private ticketService: TicketService, + private securityService: SecurityService, + private router: Router + ) { + super(messageService); + } + + ngOnInit() { + this.activatedRoute.params + .pipe( + takeUntil(this.destroy$), + distinctUntilKeyChanged('noteId') + ) + .subscribe(() => { + this.noteVarShareService.clear(); + }); + this.activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe(param => { + const { noteId, revisionId } = param; + if (revisionId) { + this.messageService.noteRevision(noteId, revisionId); + } else { + this.messageService.getNote(noteId); + } + this.revisionView = !!revisionId; + this.cdr.markForCheck(); + this.messageService.listRevisionHistory(noteId); + // TODO scroll to current paragraph + }); + this.revisionView = !!this.activatedRoute.snapshot.params.revisionId; + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.killSaveTimer(); + this.saveNote(); + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts new file mode 100644 index 00000000000..20a80638cf8 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts @@ -0,0 +1,86 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { PortalModule } from '@angular/cdk/portal'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { + NzButtonModule, + NzCheckboxModule, + NzDividerModule, + NzDropDownModule, + NzFormModule, + NzGridModule, + NzIconModule, + NzInputModule, + NzNoAnimationModule, + NzPopconfirmModule, + NzPopoverModule, + NzProgressModule, + NzRadioModule, + NzSelectModule, + NzSwitchModule, + NzToolTipModule +} from 'ng-zorro-antd'; +import { NzResizableModule } from 'ng-zorro-antd/resizable'; + +import { ShareModule } from '@zeppelin/share'; + +import { VisualizationModule } from 'src/app/visualizations/visualization.module'; +import { NotebookAddParagraphComponent } from './add-paragraph/add-paragraph.component'; +import { NotebookInterpreterBindingComponent } from './interpreter-binding/interpreter-binding.component'; +import { NotebookParagraphControlComponent } from './paragraph/control/control.component'; +import { NotebookParagraphFooterComponent } from './paragraph/footer/footer.component'; +import { NotebookParagraphComponent } from './paragraph/paragraph.component'; +import { NotebookParagraphProgressComponent } from './paragraph/progress/progress.component'; +import { NotebookPermissionsComponent } from './permissions/permissions.component'; +import { NotebookRevisionsComparatorComponent } from './revisions-comparator/revisions-comparator.component'; + +import { NotebookActionBarComponent } from './action-bar/action-bar.component'; +import { NotebookRoutingModule } from './notebook-routing.module'; +import { NotebookComponent } from './notebook.component'; +import { NotebookShareModule } from './share/share.module'; + +@NgModule({ + declarations: [ + NotebookComponent, + NotebookActionBarComponent, + NotebookInterpreterBindingComponent, + NotebookPermissionsComponent, + NotebookRevisionsComparatorComponent, + NotebookParagraphComponent, + NotebookAddParagraphComponent, + NotebookParagraphProgressComponent, + NotebookParagraphFooterComponent, + NotebookParagraphControlComponent, + ], + imports: [ + CommonModule, + PortalModule, + NotebookRoutingModule, + ShareModule, + VisualizationModule, + NotebookShareModule, + NzButtonModule, + NzIconModule, + NzDropDownModule, + NzNoAnimationModule, + NzToolTipModule, + NzPopconfirmModule, + NzFormModule, + NzPopoverModule, + NzInputModule, + FormsModule, + ReactiveFormsModule, + NzDividerModule, + NzCheckboxModule, + NzProgressModule, + NzSwitchModule, + NzSelectModule, + NzGridModule, + NzRadioModule, + DragDropModule, + NzResizableModule + ] +}) +export class NotebookModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html new file mode 100644 index 00000000000..c4f3d313cd4 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html @@ -0,0 +1,73 @@ + +
{{status}}
+
{{progress}}%
+ + + + + + + + + + + + + + + +
    +
  • + {{ pid }} +
  • +
  • +
  • + Run on selection change + +
  • +
  • + Width + + + +
  • +
  • + Font size + + + +
  • + +
  • + + {{menu.label}} + {{menu.shortCut}} +
  • +
    +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.less new file mode 100644 index 00000000000..d14f85fe4cb --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.less @@ -0,0 +1,61 @@ +@import "theme-mixin"; + +:host { + display: flex; +} + +.list-item { + display: flex; + justify-content: space-between; +} + +.setting-menu { + width: 270px; + + li { + font-size: 12px; + + i { + margin-right: 6px; + } + } +} + +.short-cut { + opacity: 0.7; +} + +.paragraph-id { + text-align: center; +} + +.themeMixin({ + .job-link { + margin-right: 12px; + + a { + color: @link-color; + } + } + .status { + color: @text-color-secondary; + margin-right: 12px; + } + .progress { + color: @text-color-secondary; + margin-right: 12px; + } + + a { + margin-left: 8px; + color: @text-color-secondary; + + .run-para { + color: @primary-color; + } + + .cancel-para { + color: @warning-color; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.ts new file mode 100644 index 00000000000..2063f3ffd27 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.ts @@ -0,0 +1,292 @@ +/// +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output +} from '@angular/core'; +import { copyTextToClipboard } from '@zeppelin/core'; + +import { NzMessageService, NzModalService } from 'ng-zorro-antd'; + +import { ActivatedRoute } from '@angular/router'; +import { RuntimeInfos } from '@zeppelin/sdk'; +import { MessageService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-control', + exportAs: 'paragraphControl', + templateUrl: './control.component.html', + styleUrls: ['./control.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphControlComponent implements OnInit, OnChanges { + @Input() status: string; + @Input() progress = 0; + @Input() revisionView = false; + @Input() enabled = true; + @Input() pid: string; + @Input() tableHide = false; + @Input() editorHide = false; + @Input() colWidth: number; + @Input() fontSize: number; + @Input() runOnSelectionChange = true; + @Input() isEntireNoteRunning = true; + @Input() runtimeInfos: RuntimeInfos; + @Input() colWidthOption = []; + @Input() first = false; + @Input() last = false; + @Input() title = false; + @Input() lineNumbers = false; + @Input() paragraphLength: number; + @Output() readonly colWidthChange = new EventEmitter(); + @Output() readonly titleChange = new EventEmitter(); + @Output() readonly enabledChange = new EventEmitter(); + @Output() readonly fontSizeChange = new EventEmitter(); + @Output() readonly tableHideChange = new EventEmitter(); + @Output() readonly runParagraph = new EventEmitter(); + @Output() readonly lineNumbersChange = new EventEmitter(); + @Output() readonly cancelParagraph = new EventEmitter(); + @Output() readonly editorHideChange = new EventEmitter(); + @Output() readonly runOnSelectionChangeChange = new EventEmitter(); + @Output() readonly moveUp = new EventEmitter(); + @Output() readonly moveDown = new EventEmitter(); + @Output() readonly insertNew = new EventEmitter(); + @Output() readonly runAllAbove = new EventEmitter(); + @Output() readonly runAllBelowAndCurrent = new EventEmitter(); + @Output() readonly cloneParagraph = new EventEmitter(); + fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + dropdownVisible = false; + isMac = navigator.appVersion.indexOf('Mac') !== -1; + listOfMenu: Array<{ + label: string; + show: boolean; + disabled: boolean; + icon: string; + shortCut: string; + keyBindings: number[]; + trigger(): void; + }> = []; + + updateListOfMenu(monaco?) { + this.listOfMenu = [ + { + label: 'Move up', + show: !this.first, + disabled: this.isEntireNoteRunning, + icon: 'up', + trigger: () => this.trigger(this.moveUp), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+K`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_K] : [] + }, + { + label: 'Move down', + show: !this.last, + disabled: this.isEntireNoteRunning, + icon: 'down', + trigger: () => this.trigger(this.moveDown), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+J`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_J] : [] + }, + { + label: 'Insert new', + show: true, + disabled: this.isEntireNoteRunning, + icon: 'plus', + trigger: () => this.trigger(this.insertNew), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+B`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_B] : [] + }, + { + label: 'Run all above', + show: !this.first, + disabled: this.isEntireNoteRunning, + icon: 'up-square', + trigger: () => this.trigger(this.runAllAbove), + shortCut: `Ctrl+Shift+Enter`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Shift | monaco.KeyCode.Enter] : [] + }, + { + label: 'Run all below', + show: !this.last, + disabled: this.isEntireNoteRunning, + icon: 'down-square', + trigger: () => this.trigger(this.runAllBelowAndCurrent), + shortCut: `Ctrl+Shift+Enter`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Shift | monaco.KeyCode.Enter] : [] + }, + { + label: 'Clone paragraph', + show: true, + disabled: this.isEntireNoteRunning, + icon: 'copy', + trigger: () => this.trigger(this.cloneParagraph), + shortCut: `Ctrl+Shift+C`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Shift | monaco.KeyCode.KEY_C] : [] + }, + { + label: this.title ? 'Hide Title' : 'Show Title', + show: true, + disabled: false, + icon: 'font-colors', + trigger: () => this.toggleTitle(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+T`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_T] : [] + }, + { + label: this.lineNumbers ? 'Hide line numbers' : 'Show line numbers', + show: true, + disabled: false, + icon: 'ordered-list', + trigger: () => this.toggleLineNumbers(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+M`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_M] : [] + }, + { + label: this.enabled ? 'Disable run' : 'Enable run', + show: true, + disabled: this.isEntireNoteRunning, + icon: 'api', + trigger: () => this.toggleEnabled(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+R`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_R] : [] + }, + { + label: 'Link this paragraph', + show: true, + disabled: false, + icon: 'export', + trigger: () => this.goToSingleParagraph(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+W`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_W] : [] + }, + { + label: 'Clear output', + show: true, + disabled: this.isEntireNoteRunning, + icon: 'fire', + trigger: () => this.clearParagraphOutput(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+L`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_L] : [] + }, + { + label: 'Remove', + show: this.paragraphLength > 1, + disabled: this.isEntireNoteRunning, + icon: 'delete', + trigger: () => this.removeParagraph(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+D`, + keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_D] : [] + } + ]; + } + + toggleEditor() { + this.editorHide = !this.editorHide; + this.editorHideChange.emit(this.editorHide); + } + + toggleOutput() { + this.tableHide = !this.tableHide; + this.tableHideChange.emit(this.tableHide); + } + + toggleRunOnSelectionChange() { + this.runOnSelectionChange = !this.runOnSelectionChange; + this.runOnSelectionChangeChange.emit(this.runOnSelectionChange); + } + + toggleTitle() { + this.title = !this.title; + this.titleChange.emit(this.title); + } + + toggleLineNumbers() { + this.lineNumbers = !this.lineNumbers; + this.lineNumbersChange.emit(this.lineNumbers); + } + + toggleEnabled() { + if (!this.isEntireNoteRunning) { + this.enabled = !this.enabled; + this.enabledChange.emit(this.enabled); + } + } + + goToSingleParagraph() { + // TODO asIframe + const { noteId } = this.activatedRoute.snapshot.params; + const redirectToUrl = `${location.protocol}//${location.host}${location.pathname}#/notebook/${noteId}/paragraph/${this.pid}`; + window.open(redirectToUrl); + } + + changeColWidth(colWidth: number) { + this.colWidth = +colWidth; + this.colWidthChange.emit(this.colWidth); + this.dropdownVisible = false; + } + + changeFontSize(fontSize: number) { + this.fontSize = +fontSize; + this.fontSizeChange.emit(this.fontSize); + } + + copyClipboard(id: string) { + copyTextToClipboard(id); + this.nzMessageService.info('Paragraph id copied'); + } + + clearParagraphOutput() { + if (!this.isEntireNoteRunning) { + this.messageService.paragraphClearOutput(this.pid); + } + } + + removeParagraph() { + if (!this.isEntireNoteRunning) { + if (this.paragraphLength === 1) { + this.nzModalService.warning({ + nzTitle: `Warning`, + nzContent: `All the paragraphs can't be deleted` + }); + } else { + this.nzModalService.confirm({ + nzTitle: 'Delete Paragraph', + nzContent: 'Do you want to delete this paragraph?', + nzOnOk: () => { + this.messageService.paragraphRemove(this.pid); + this.cdr.markForCheck(); + // TODO moveFocusToNextParagraph + } + }); + } + } + } + + trigger(event: EventEmitter) { + if (!this.isEntireNoteRunning) { + this.dropdownVisible = false; + event.emit(); + } + } + + constructor( + private cdr: ChangeDetectorRef, + private nzMessageService: NzMessageService, + private activatedRoute: ActivatedRoute, + private messageService: MessageService, + private nzModalService: NzModalService + ) {} + + ngOnInit() { + this.updateListOfMenu(); + } + + ngOnChanges(): void { + this.updateListOfMenu(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.html new file mode 100644 index 00000000000..d5a082504ef --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.html @@ -0,0 +1,4 @@ + diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.less new file mode 100644 index 00000000000..d7d44577624 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.less @@ -0,0 +1,9 @@ +@import "theme-mixin"; + +.themeMixin({ + .footer { + color: @text-color-secondary; + font-size: 12px; + margin-top: 12px; + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.ts new file mode 100644 index 00000000000..c4e16b51ab9 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/footer/footer.component.ts @@ -0,0 +1,62 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; + +import * as distanceInWordsStrict from 'date-fns/distance_in_words_strict'; +import * as distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; +import * as format from 'date-fns/format'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphFooterComponent implements OnChanges { + @Input() dateStarted: string; + @Input() dateFinished: string; + @Input() dateUpdated: string; + @Input() showExecutionTime = false; + @Input() showElapsedTime = false; + @Input() user: string; + executionTime: string; + elapsedTime: string; + + isResultOutdated() { + return this.dateUpdated !== undefined && Date.parse(this.dateUpdated) > Date.parse(this.dateStarted); + } + + getExecutionTime() { + const end = this.dateFinished; + const start = this.dateStarted; + const timeMs = Date.parse(end) - Date.parse(start); + if (isNaN(timeMs) || timeMs < 0) { + if (this.isResultOutdated()) { + return 'outdated'; + } + return ''; + } + + const durationFormat = distanceInWordsStrict(start, end); + const endFormat = format(this.dateFinished, 'MMMM DD YYYY, h:mm:ss A'); + + const user = this.user === undefined || this.user === null ? 'anonymous' : this.user; + let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.`; + + if (this.isResultOutdated()) { + desc += ' (outdated)'; + } + + return desc; + } + + getElapsedTime() { + // TODO dateStarted undefined after start + return `Started ${distanceInWordsToNow(this.dateStarted || new Date())} ago.`; + } + + constructor() {} + + ngOnChanges() { + this.executionTime = this.getExecutionTime(); + this.elapsedTime = this.getElapsedTime(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html new file mode 100644 index 00000000000..df38ddeb04a --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html @@ -0,0 +1,87 @@ +
+ +
+ + + + + + + + + + + +
+ +
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.less new file mode 100644 index 00000000000..712bb97bc60 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.less @@ -0,0 +1,60 @@ +@import "theme-mixin"; + +:host { + display: block; + padding: 0 4px; +} + +.themeMixin({ + .paragraph { + background: @component-background; + border: 1px solid @border-color-split; + box-shadow: @card-shadow; + padding: 32px 12px 12px 12px; + position: relative; + + zeppelin-notebook-paragraph-code-editor + zeppelin-notebook-paragraph-dynamic-forms { + margin-top: 24px; + } + + zeppelin-notebook-paragraph-progress + zeppelin-notebook-paragraph-dynamic-forms { + margin-top: 24px; + } + + &.simple { + box-shadow: none; + border-color: transparent; + + zeppelin-notebook-paragraph-control, zeppelin-notebook-paragraph-footer { + visibility: hidden; + } + + &:hover { + border: 1px solid @border-color-split; + box-shadow: @card-shadow; + + zeppelin-notebook-paragraph-control, zeppelin-notebook-paragraph-footer { + visibility: visible; + } + } + } + + &.report { + &:hover { + box-shadow: none; + border-color: transparent; + + zeppelin-notebook-paragraph-control, zeppelin-notebook-paragraph-footer { + visibility: hidden; + } + } + } + + zeppelin-notebook-paragraph-control { + position: absolute; + right: 12px; + top: 8px; + z-index: 10; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts new file mode 100644 index 00000000000..08350b06095 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts @@ -0,0 +1,625 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChild, + ViewChildren +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import DiffMatchPatch from 'diff-match-patch'; +import { isEmpty, isEqual } from 'lodash'; +import { NzModalService } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { + AngularObjectRemove, + AngularObjectUpdate, + GraphConfig, + InterpreterBindingItem, + MessageReceiveDataTypeMap, + Note, + OP, + ParagraphConfig, + ParagraphConfigResult, + ParagraphEditorSetting, + ParagraphItem, + ParagraphIResultsMsgItem +} from '@zeppelin/sdk'; +import { + HeliumService, + MessageService, + NgZService, + NoteStatusService, + NoteVarShareService, + ParagraphStatus +} from '@zeppelin/services'; +import { SpellResult } from '@zeppelin/spell/spell-result'; + +import { NzResizeEvent } from 'ng-zorro-antd/resizable'; + +@Component({ + selector: 'zeppelin-notebook-paragraph', + templateUrl: './paragraph.component.html', + styleUrls: ['./paragraph.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy { + @Input() paragraph: ParagraphItem; + @Input() note: Note['note']; + @Input() looknfeel: string; + @Input() revisionView: boolean; + @Input() viewOnly: boolean; + @Input() last: boolean; + @Input() collaborativeMode = false; + @Input() first: boolean; + @Input() interpreterBindings: InterpreterBindingItem[] = []; + @Output() readonly saveNoteTimer = new EventEmitter(); + @Output() readonly triggerSaveParagraph = new EventEmitter(); + + private destroy$ = new Subject(); + dirtyText: string; + originalText: string; + isEntireNoteRunning = false; + diffMatchPatch = new DiffMatchPatch(); + isParagraphRunning = false; + results = []; + configs = {}; + progress = 0; + colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + editorSetting: ParagraphEditorSetting = {}; + + @MessageListener(OP.PROGRESS) + onProgress(data: MessageReceiveDataTypeMap[OP.PROGRESS]) { + if (data.id === this.paragraph.id) { + this.progress = data.progress; + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.NOTE_RUNNING_STATUS) + noteRunningStatusChange(data: MessageReceiveDataTypeMap[OP.NOTE_RUNNING_STATUS]) { + this.isEntireNoteRunning = data.status; + this.cdr.markForCheck(); + } + + @MessageListener(OP.PARAS_INFO) + updateParaInfos(data: MessageReceiveDataTypeMap[OP.PARAS_INFO]) { + if (this.paragraph.id === data.id) { + this.paragraph.runtimeInfos = data.infos; + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.EDITOR_SETTING) + getEditorSetting(data: MessageReceiveDataTypeMap[OP.EDITOR_SETTING]) { + if (this.paragraph.id === data.paragraphId) { + this.paragraph.config.editorSetting = { ...this.paragraph.config.editorSetting, ...data.editor }; + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.PARAGRAPH) + paragraphData(data: MessageReceiveDataTypeMap[OP.PARAGRAPH]) { + const oldPara = this.paragraph; + const newPara = data.paragraph; + if (this.isUpdateRequired(oldPara, newPara)) { + this.updateParagraph(oldPara, newPara, () => { + if (newPara.results && newPara.results.msg) { + // tslint:disable-next-line:no-for-in-array + for (const i in newPara.results.msg) { + if (newPara.results.msg[i]) { + const newResult = newPara.results.msg ? newPara.results.msg[i] : new ParagraphIResultsMsgItem(); + const oldResult = + oldPara.results && oldPara.results.msg ? oldPara.results.msg[i] : new ParagraphIResultsMsgItem(); + const newConfig = newPara.config.results ? newPara.config.results[i] : { graph: new GraphConfig() }; + const oldConfig = oldPara.config.results ? oldPara.config.results[i] : { graph: new GraphConfig() }; + if (!isEqual(newResult, oldResult) || !isEqual(newConfig, oldConfig)) { + const resultComponent = this.notebookParagraphResultComponents.toArray()[i]; + if (resultComponent) { + resultComponent.updateResult(newConfig, newResult); + } + } + } + } + } + this.cdr.markForCheck(); + }); + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.PATCH_PARAGRAPH) + patchParagraph(data: MessageReceiveDataTypeMap[OP.PATCH_PARAGRAPH]) { + if (data.paragraphId === this.paragraph.id) { + let patch = data.patch; + patch = this.diffMatchPatch.patch_fromText(patch); + if (!this.paragraph.text) { + this.paragraph.text = ''; + } + this.paragraph.text = this.diffMatchPatch.patch_apply(patch, this.paragraph.text)[0]; + this.originalText = this.paragraph.text; + this.cdr.markForCheck(); + } + } + + @MessageListener(OP.ANGULAR_OBJECT_UPDATE) + angularObjectUpdate(data: AngularObjectUpdate) { + if (data.paragraphId === this.paragraph.id) { + const { name, object } = data.angularObject; + this.ngZService.setContextValue(name, object, data.paragraphId, false); + } + } + + @MessageListener(OP.ANGULAR_OBJECT_REMOVE) + angularObjectRemove(data: AngularObjectRemove) { + if (data.paragraphId === this.paragraph.id) { + this.ngZService.unsetContextValue(data.name, data.paragraphId, false); + } + } + + updateParagraph(oldPara: ParagraphItem, newPara: ParagraphItem, updateCallback: () => void) { + // 1. can't update on revision view + if (!this.revisionView) { + // 2. get status, refreshed + const statusChanged = newPara.status !== oldPara.status; + const resultRefreshed = + newPara.dateFinished !== oldPara.dateFinished || + isEmpty(newPara.results) !== isEmpty(oldPara.results) || + newPara.status === ParagraphStatus.ERROR || + (newPara.status === ParagraphStatus.FINISHED && statusChanged); + + // 3. update texts managed by paragraph + this.updateAllScopeTexts(oldPara, newPara); + // 4. execute callback to update result + updateCallback(); + + // 5. update remaining paragraph objects + this.updateParagraphObjectWhenUpdated(newPara); + + // 6. 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 + // TODO + } + this.cdr.markForCheck(); + } + } + + textChanged(text: string) { + this.dirtyText = text; + this.paragraph.text = text; + if (this.dirtyText !== this.originalText) { + if (this.collaborativeMode) { + this.sendPatch(); + } else { + this.startSaveTimer(); + } + } + } + + sendPatch() { + this.originalText = this.originalText ? this.originalText : ''; + const patch = this.diffMatchPatch.patch_make(this.originalText, this.dirtyText).toString(); + this.originalText = this.dirtyText; + this.messageService.patchParagraph(this.paragraph.id, this.note.id, patch); + } + + startSaveTimer() { + this.saveNoteTimer.emit(); + } + + saveParagraph() { + const dirtyText = this.paragraph.text; + if (dirtyText === undefined || dirtyText === this.originalText) { + return; + } + this.commitParagraph(); + this.originalText = dirtyText; + this.dirtyText = undefined; + this.cdr.markForCheck(); + } + + runAllAbove() { + const index = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id); + const toRunParagraphs = this.note.paragraphs.filter((p, i) => i < index); + + const paragraphs = toRunParagraphs.map(p => { + return { + id: p.id, + title: p.title, + paragraph: p.text, + config: p.config, + params: p.settings.params + }; + }); + this.nzModalService.confirm({ + nzTitle: 'Run all above?', + nzContent: 'Are you sure to run all above paragraphs?', + nzOnOk: () => { + this.messageService.runAllParagraphs(this.note.id, paragraphs); + } + }); + // TODO: save cursor + } + + doubleClickParagraph() { + if (this.paragraph.config.editorSetting.editOnDblClick && this.revisionView !== true) { + this.paragraph.config.editorHide = false; + this.paragraph.config.tableHide = true; + // TODO: focus editor + } + } + + runAllBelowAndCurrent() { + const index = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id); + const toRunParagraphs = this.note.paragraphs.filter((p, i) => i >= index); + + const paragraphs = toRunParagraphs.map(p => { + return { + id: p.id, + title: p.title, + paragraph: p.text, + config: p.config, + params: p.settings.params + }; + }); + this.nzModalService.confirm({ + nzTitle: 'Run current and all below?', + nzContent: 'Are you sure to run current and all below?', + nzOnOk: () => { + this.messageService.runAllParagraphs(this.note.id, paragraphs); + } + }); + // TODO: save cursor + } + + cloneParagraph(position: string = 'below') { + let newIndex = -1; + for (let i = 0; i < this.note.paragraphs.length; i++) { + if (this.note.paragraphs[i].id === this.paragraph.id) { + // determine position of where to add new paragraph; default is below + if (position === 'above') { + newIndex = i; + } else { + newIndex = i + 1; + } + break; + } + } + + if (newIndex < 0 || newIndex > this.note.paragraphs.length) { + return; + } + + const config = this.paragraph.config; + config.editorHide = false; + + this.messageService.copyParagraph( + newIndex, + this.paragraph.title, + this.paragraph.text, + config, + this.paragraph.settings.params + ); + } + + runParagraph(paragraphText?: string, propagated: boolean = false) { + const text = paragraphText || this.paragraph.text; + if (text && !this.isParagraphRunning) { + const magic = SpellResult.extractMagic(text); + + if (this.heliumService.getSpellByMagic(magic)) { + this.runParagraphUsingSpell(text, magic, propagated); + } else { + this.runParagraphUsingBackendInterpreter(text); + } + + this.originalText = text; + this.dirtyText = undefined; + + if (this.paragraph.config.editorSetting.editOnDblClick) { + this.paragraph.config.editorHide = true; + this.paragraph.config.tableHide = false; + this.commitParagraph(); + } else if (this.editorSetting.isOutputHidden && !this.paragraph.config.editorSetting.editOnDblClick) { + // %md/%angular repl make output to be hidden by default after running + // so should open output if repl changed from %md/%angular to another + this.paragraph.config.editorHide = false; + this.paragraph.config.tableHide = false; + this.commitParagraph(); + } + this.editorSetting.isOutputHidden = this.paragraph.config.editorSetting.editOnDblClick; + } + } + + runParagraphUsingSpell(paragraphText: string, magic: string, propagated: boolean) { + // TODO + } + + runParagraphUsingBackendInterpreter(paragraphText: string) { + this.messageService.runParagraph( + this.paragraph.id, + this.paragraph.title, + paragraphText, + this.paragraph.config, + this.paragraph.settings.params + ); + } + + cancelParagraph() { + if (!this.isEntireNoteRunning) { + this.messageService.cancelParagraph(this.paragraph.id); + } + } + + updateAllScopeTexts(oldPara: ParagraphItem, newPara: ParagraphItem) { + if (oldPara.text !== newPara.text) { + if (this.dirtyText) { + // check if editor has local update + if (this.dirtyText === newPara.text) { + // when local update is the same from remote, clear local update + this.paragraph.text = newPara.text; + this.dirtyText = undefined; + this.originalText = newPara.text; + } else { + // if there're local update, keep it. + this.paragraph.text = newPara.text; + } + } else { + this.paragraph.text = newPara.text; + this.originalText = newPara.text; + } + } + this.cdr.markForCheck(); + } + + updateParagraphObjectWhenUpdated(newPara: ParagraphItem) { + if (this.paragraph.config.colWidth !== newPara.config.colWidth) { + this.changeColWidth(false); + } + this.paragraph.aborted = newPara.aborted; + this.paragraph.user = newPara.user; + this.paragraph.dateUpdated = newPara.dateUpdated; + this.paragraph.dateCreated = newPara.dateCreated; + this.paragraph.dateFinished = newPara.dateFinished; + this.paragraph.dateStarted = newPara.dateStarted; + this.paragraph.errorMessage = newPara.errorMessage; + this.paragraph.jobName = newPara.jobName; + this.paragraph.title = newPara.title; + this.paragraph.lineNumbers = newPara.lineNumbers; + this.paragraph.status = newPara.status; + this.paragraph.fontSize = newPara.fontSize; + if (newPara.status !== ParagraphStatus.RUNNING) { + this.paragraph.results = newPara.results; + } + this.paragraph.settings = newPara.settings; + this.paragraph.runtimeInfos = newPara.runtimeInfos; + this.isParagraphRunning = this.noteStatusService.isParagraphRunning(newPara); + this.paragraph.config = newPara.config; + this.initializeDefault(this.paragraph.config); + this.setResults(); + this.cdr.markForCheck(); + } + + isUpdateRequired(oldPara: ParagraphItem, newPara: ParagraphItem): boolean { + return ( + newPara.id === oldPara.id && + (newPara.dateCreated !== oldPara.dateCreated || + newPara.text !== oldPara.text || + 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 || + !isEqual(newPara.settings, oldPara.settings) || + !isEqual(newPara.config, oldPara.config) || + !isEqual(newPara.runtimeInfos, oldPara.runtimeInfos)) + ); + } + + insertParagraph(position: string) { + if (this.revisionView === true) { + return; + } + let newIndex = -1; + for (let i = 0; i < this.note.paragraphs.length; i++) { + if (this.note.paragraphs[i].id === this.paragraph.id) { + // determine position of where to add new paragraph; default is below + if (position === 'above') { + newIndex = i; + } else { + newIndex = i + 1; + } + break; + } + } + + if (newIndex < 0 || newIndex > this.note.paragraphs.length) { + return; + } + this.messageService.insertParagraph(newIndex); + this.cdr.markForCheck(); + } + + setResults() { + if (this.paragraph.results) { + this.results = this.paragraph.results.msg; + this.configs = this.paragraph.config.results; + } + if (!this.paragraph.config) { + this.paragraph.config = {}; + } + } + + setTitle(title: string) { + this.paragraph.title = title; + this.commitParagraph(); + this.cdr.markForCheck(); + } + + commitParagraph() { + const { + id, + title, + text, + config, + settings: { params } + } = this.paragraph; + this.messageService.commitParagraph(id, title, text, config, params, this.note.id); + } + + initializeDefault(config: ParagraphConfig) { + const forms = this.paragraph.settings.forms; + + if (!config.colWidth) { + config.colWidth = 12; + } + + if (!config.fontSize) { + config.fontSize = 9; + } + + if (config.enabled === undefined) { + config.enabled = true; + } + + for (const idx in forms) { + if (forms[idx]) { + if (forms[idx].options) { + if (config.runOnSelectionChange === undefined) { + config.runOnSelectionChange = true; + } + } + } + } + + if (!config.results) { + config.results = {}; + } + + if (!config.editorSetting) { + config.editorSetting = {}; + } else if (config.editorSetting.editOnDblClick) { + this.editorSetting.isOutputHidden = config.editorSetting.editOnDblClick; + } + } + + moveUpParagraph() { + const newIndex = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id) - 1; + if (newIndex < 0 || newIndex >= this.note.paragraphs.length) { + return; + } + // save dirtyText of moving paragraphs. + const prevParagraph = this.note.paragraphs[newIndex]; + // TODO: save pre paragraph? + this.saveParagraph(); + this.triggerSaveParagraph.emit(prevParagraph.id); + this.messageService.moveParagraph(this.paragraph.id, newIndex); + } + + moveDownParagraph() { + const newIndex = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id) + 1; + if (newIndex < 0 || newIndex >= this.note.paragraphs.length) { + return; + } + // save dirtyText of moving paragraphs. + const nextParagraph = this.note.paragraphs[newIndex]; + // TODO: save pre paragraph? + this.saveParagraph(); + this.triggerSaveParagraph.emit(nextParagraph.id); + this.messageService.moveParagraph(this.paragraph.id, newIndex); + } + + changeColWidth(needCommit: boolean, updateResult = true) { + if (needCommit) { + this.commitParagraph(); + } + } + + onSizeChange(resize: NzResizeEvent) { + this.paragraph.config.colWidth = resize.col; + this.changeColWidth(true, false); + this.cdr.markForCheck(); + } + + onConfigChange(configResult: ParagraphConfigResult, index: number) { + this.paragraph.config.results[index] = configResult; + this.commitParagraph(); + this.cdr.markForCheck(); + } + + setEditorHide(editorHide: boolean) { + this.paragraph.config.editorHide = editorHide; + this.cdr.markForCheck(); + } + + setTableHide(tableHide: boolean) { + this.paragraph.config.tableHide = tableHide; + this.cdr.markForCheck(); + } + + trackByIndexFn(index: number) { + return index; + } + + constructor( + private heliumService: HeliumService, + private noteStatusService: NoteStatusService, + public messageService: MessageService, + private nzModalService: NzModalService, + private noteVarShareService: NoteVarShareService, + private cdr: ChangeDetectorRef, + private ngZService: NgZService + ) { + super(messageService); + } + + ngOnInit() { + this.setResults(); + this.originalText = this.paragraph.text; + this.isEntireNoteRunning = this.noteStatusService.isEntireNoteRunning(this.note); + this.isParagraphRunning = this.noteStatusService.isParagraphRunning(this.paragraph); + this.noteVarShareService.set(this.paragraph.id + '_paragraphScope', this); + this.initializeDefault(this.paragraph.config); + this.ngZService + .runParagraphAction() + .pipe(takeUntil(this.destroy$)) + .subscribe(id => { + if (id === this.paragraph.id) { + this.runParagraph(); + } + }); + this.ngZService + .contextChanged() + .pipe(takeUntil(this.destroy$)) + .subscribe(change => { + if (change.paragraphId === this.paragraph.id && change.emit) { + if (change.set) { + this.messageService.angularObjectClientBind(this.note.id, change.key, change.value, change.paragraphId); + } else { + this.messageService.angularObjectClientUnbind(this.note.id, change.key, change.paragraphId); + } + } + }); + } + + ngOnChanges(): void {} + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.ngZService.removeParagraph(this.paragraph.id); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.html new file mode 100644 index 00000000000..db0896910d5 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.html @@ -0,0 +1,4 @@ + diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.ts new file mode 100644 index 00000000000..c580f90cc6f --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/progress/progress.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-progress', + templateUrl: './progress.component.html', + styleUrls: ['./progress.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphProgressComponent implements OnChanges { + @Input() progress = 0; + displayProgress = 0; + + ngOnChanges(): void { + if (this.progress > 0 && this.progress < 100) { + this.displayProgress = this.progress; + } else { + this.displayProgress = 100; + } + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.html new file mode 100644 index 00000000000..ad19dcc5e2b --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.html @@ -0,0 +1,76 @@ +
+
+

Note Permissions (Only note owners can change)

+
+ +
+

+ Enter comma separated users and groups in the fields.
+ Empty field (*) implies anyone can do the operation. +

+
+
+
+ + Owners + + + + + + + + + + Owners can change permissions,read, run and write the note. + + + + Writers + + + + + + + + + + Writers can read, run and write the note. + + + + Runners + + + + + + + + + + Runners can read and run the note. + + + + Readers + + + + + + + + + + Readers can only read the note. + + +
+
+ + +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.less new file mode 100644 index 00000000000..b0415be38e5 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.less @@ -0,0 +1,21 @@ +@import "theme-mixin"; + +.themeMixin({ + .permissions-form { + background: @layout-body-background; + padding: 12px; + margin-bottom: 12px; + } + nz-form-item { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } + } + .submit-permissions { + button { + margin-right: 12px; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.ts new file mode 100644 index 00000000000..1bb578a6343 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/permissions/permissions.component.ts @@ -0,0 +1,123 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output +} from '@angular/core'; + +import { NzMessageService, NzModalService } from 'ng-zorro-antd'; + +import { Permissions } from '@zeppelin/interfaces'; +import { SecurityService, TicketService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-notebook-permissions', + templateUrl: './permissions.component.html', + styleUrls: ['./permissions.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookPermissionsComponent implements OnInit, OnChanges { + @Input() permissions: Permissions; + @Input() noteId: string; + @Input() activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide'; + @Output() readonly activatedExtensionChange = new EventEmitter< + 'interpreter' | 'permissions' | 'revisions' | 'hide' + >(); + permissionsBack: Permissions; + listOfUserAndRole = []; + + savePermissions() { + const principal = this.ticketService.ticket.principal; + const isAnonymous = principal === 'anonymous'; + if (isAnonymous || this.ticketService.ticket.principal.trim().length === 0) { + this.blockAnonUsers(); + } + if (this.isOwnerEmpty()) { + this.nzModalService.create({ + nzTitle: 'Setting Owners Permissions', + nzContent: `Please fill the [Owners] field. If not, it will set as current user. Current user : [ ${this.ticketService.ticket.principal.trim()} ]`, + nzOnOk: () => { + this.permissions.owners = [this.ticketService.ticket.principal]; + this.setPermissions(); + }, + nzOnCancel: () => { + this.resetPermissions(); + } + }); + } else { + this.setPermissions(); + } + } + + closePermissions() { + this.activatedExtension = 'hide'; + this.activatedExtensionChange.emit('hide'); + } + + blockAnonUsers() { + this.nzModalService.create({ + nzTitle: 'No permission', + nzContent: 'Only authenticated user can set the permission.', + nzOkText: 'Read Doc', + nzOnOk: () => { + const url = `https://zeppelin.apache.org/docs/${this.ticketService.version}/security/notebook_authorization.html`; + window.open(url); + } + }); + } + + setPermissions() { + this.securityService.setPermissions(this.noteId, this.permissions).subscribe(() => { + this.nzMessageService.success('Permissions Saved Successfully'); + this.closePermissions(); + }); + } + + resetPermissions() { + this.permissions = { ...this.permissionsBack }; + } + + isOwnerEmpty() { + return !this.permissions.owners.some(o => o.trim().length > 0); + } + + searchUser(search: string) { + this.securityService.searchUsers(search).subscribe(data => { + const results = []; + if (data.users.length) { + results.push({ + text: 'Users :', + children: data.users + }); + } + if (data.roles.length) { + results.push({ + text: 'Roles :', + children: data.roles + }); + } + this.listOfUserAndRole = results; + this.cdr.markForCheck(); + }); + } + + constructor( + private securityService: SecurityService, + private cdr: ChangeDetectorRef, + private nzMessageService: NzMessageService, + private ticketService: TicketService, + private nzModalService: NzModalService + ) {} + + ngOnInit() { + this.permissionsBack = { ...this.permissions }; + } + + ngOnChanges(): void { + this.permissionsBack = { ...this.permissions }; + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html new file mode 100644 index 00000000000..a605bd4da24 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.html @@ -0,0 +1,8 @@ +
+
+

Revisions comparator

+
+ +
+
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts new file mode 100644 index 00000000000..975bc2dfe39 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/revisions-comparator/revisions-comparator.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'zeppelin-notebook-revisions-comparator', + templateUrl: './revisions-comparator.component.html', + styleUrls: ['./revisions-comparator.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookRevisionsComparatorComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.html new file mode 100644 index 00000000000..0ba81ac9ec0 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.html @@ -0,0 +1,11 @@ +
+ +

{{value || 'Untitled'}}

+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.less new file mode 100644 index 00000000000..a8c0c59287b --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.less @@ -0,0 +1,44 @@ +@import "theme-mixin"; + +.themeMixin({ + .elastic { + margin-right: 24px; + overflow: hidden; + max-width: 100%; + + &.min { + margin-bottom: 6px; + margin-right: 0; + p, input { + font-size: 16px; + padding: 0 1px; + } + + input { + height: 24px; + margin: 0; + padding: 0; + } + } + + p, input { + font-size: 28px; + width: 100%; + font-weight: 700; + } + + p { + margin: 0 1px; + padding: 0 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + input { + height: 36px; + margin: 7px 0; + padding: 0 10px; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.ts new file mode 100644 index 00000000000..d79ca00311e --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/share/elastic-input/elastic-input.component.ts @@ -0,0 +1,76 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output, + Renderer2, + SimpleChanges, + ViewChild +} from '@angular/core'; + +@Component({ + selector: 'zeppelin-elastic-input', + templateUrl: './elastic-input.component.html', + styleUrls: ['./elastic-input.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ElasticInputComponent implements OnChanges { + @Input() value: string; + @Input() readonly = false; + @Input() min = false; + @Output() readonly valueUpdate = new EventEmitter(); + @ViewChild('inputElement', { read: ElementRef, static: false }) inputElement: ElementRef; + @ViewChild('pElement', { read: ElementRef, static: false }) pElement: ElementRef; + @ViewChild('elasticElement', { read: ElementRef, static: true }) elasticElement: ElementRef; + showEditor = false; + editValue: string; + + cancelEdit() { + this.editValue = this.value; + this.showEditor = false; + } + + updateValue(value: string) { + const trimmedNewName = value.trim(); + if (trimmedNewName.length > 0 && this.value !== trimmedNewName) { + this.editValue = trimmedNewName; + } + } + + setEditorState(showEditor: boolean) { + if (!this.readonly) { + this.showEditor = showEditor; + if (!this.showEditor) { + this.valueUpdate.emit(this.editValue); + } else { + const width = this.pElement.nativeElement.getBoundingClientRect().width; + this.renderer.setStyle(this.elasticElement.nativeElement, 'width', `${width}px`); + setTimeout(() => { + this.inputElement.nativeElement.focus(); + this.renderer.setStyle(this.inputElement.nativeElement, 'width', `${width}px`); + }); + } + } + } + + updateInputWidth() { + const width = this.inputElement.nativeElement.scrollWidth; + if (width > this.inputElement.nativeElement.getBoundingClientRect().width) { + this.renderer.removeStyle(this.elasticElement.nativeElement, 'width'); + this.renderer.setStyle(this.inputElement.nativeElement, 'width', `${width}px`); + } + } + + constructor(private renderer: Renderer2) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes.value) { + this.showEditor = false; + this.editValue = this.value; + this.renderer.removeStyle(this.elasticElement.nativeElement, 'width'); + } + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/share/share.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/share/share.module.ts new file mode 100644 index 00000000000..5ec039e9ab3 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/share/share.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzInputModule } from 'ng-zorro-antd'; + +import { ElasticInputComponent } from './elastic-input/elastic-input.component'; + +@NgModule({ + declarations: [ElasticInputComponent], + exports: [ElasticInputComponent], + imports: [CommonModule, NzInputModule, FormsModule] +}) +export class NotebookShareModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/workspace-routing.module.ts new file mode 100644 index 00000000000..8eb4030ba85 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace-routing.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { WorkspaceComponent } from './workspace.component'; +import { WorkspaceGuard } from './workspace.guard'; + +const routes: Routes = [ + { + path: '', + component: WorkspaceComponent, + canActivate: [WorkspaceGuard], + children: [ + { + path: '', + loadChildren: () => import('@zeppelin/pages/workspace/home/home.module').then(m => m.HomeModule) + }, + { + path: 'notebook', + loadChildren: () => import('@zeppelin/pages/workspace/notebook/notebook.module').then(m => m.NotebookModule) + }, + { + path: 'jobmanager', + loadChildren: () => + import('@zeppelin/pages/workspace/job-manager/job-manager.module').then(m => m.JobManagerModule) + }, + { + path: 'interpreter', + loadChildren: () => + import('@zeppelin/pages/workspace/interpreter/interpreter.module').then(m => m.InterpreterModule) + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class WorkspaceRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace.component.html b/zeppelin-frontend/src/app/pages/workspace/workspace.component.html new file mode 100644 index 00000000000..d3c406f3bb7 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace.component.html @@ -0,0 +1,5 @@ +
+ + +
+Connecting WebSocket ... diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace.component.less b/zeppelin-frontend/src/app/pages/workspace/workspace.component.less new file mode 100644 index 00000000000..cf6b13d6df4 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace.component.less @@ -0,0 +1,14 @@ +@import 'theme-mixin'; + +.themeMixin({ + .content { + background: @layout-body-background; + min-height: 100vh; + display: block; + position: relative; + + &.blur { + filter: blur(4px); + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace.component.ts b/zeppelin-frontend/src/app/pages/workspace/workspace.component.ts new file mode 100644 index 00000000000..8cd456414ff --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { HeliumManagerService } from '@zeppelin/helium-manager'; +import { MessageService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-workspace', + templateUrl: './workspace.component.html', + styleUrls: ['./workspace.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WorkspaceComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + websocketConnected = false; + + constructor( + public messageService: MessageService, + private cdr: ChangeDetectorRef, + private heliumManagerService: HeliumManagerService + ) {} + + ngOnInit() { + this.messageService.connectedStatus$.pipe(takeUntil(this.destroy$)).subscribe(data => { + this.websocketConnected = data; + this.cdr.markForCheck(); + }); + this.heliumManagerService.initPackages(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace.guard.ts b/zeppelin-frontend/src/app/pages/workspace/workspace.guard.ts new file mode 100644 index 00000000000..1657cce8b9e --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { of, Observable } from 'rxjs'; +import { catchError, mapTo, tap } from 'rxjs/operators'; + +import { MessageService, TicketService } from '@zeppelin/services'; + +@Injectable({ + providedIn: 'root' +}) +export class WorkspaceGuard implements CanActivate { + constructor(private ticketService: TicketService, private router: Router, private messageService: MessageService) {} + + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean | UrlTree { + return this.ticketService.getTicket().pipe( + mapTo(true), + tap(() => this.messageService.bootstrap()), + catchError(() => of(this.router.createUrlTree(['/login']))) + ); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/workspace.module.ts b/zeppelin-frontend/src/app/pages/workspace/workspace.module.ts new file mode 100644 index 00000000000..7ea5d1c87a7 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/workspace.module.ts @@ -0,0 +1,25 @@ +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { HeliumManagerModule } from '@zeppelin/helium-manager'; +import { ShareModule } from '@zeppelin/share'; + +import { WorkspaceRoutingModule } from './workspace-routing.module'; +import { WorkspaceComponent } from './workspace.component'; + +@NgModule({ + declarations: [WorkspaceComponent], + imports: [ + CommonModule, + WorkspaceRoutingModule, + FormsModule, + HttpClientModule, + ShareModule, + RouterModule, + HeliumManagerModule + ] +}) +export class WorkspaceModule {} diff --git a/zeppelin-frontend/src/app/services/array-ordering.service.ts b/zeppelin-frontend/src/app/services/array-ordering.service.ts new file mode 100644 index 00000000000..51c1ce2cd30 --- /dev/null +++ b/zeppelin-frontend/src/app/services/array-ordering.service.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable } from '@angular/core'; +import { TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; + +@Injectable({ + providedIn: 'root' +}) +export class ArrayOrderingService { + noteListOrdering(note) { + if (note.id === this.TRASH_FOLDER_ID) { + return '\uFFFF'; + } + return this.getNoteName(note); + } + + getNoteName(note) { + if (note.name === undefined || note.name.trim() === '') { + return 'Note ' + note.id; + } else { + return note.name; + } + } + + noteComparator = (v1, v2) => { + const note1 = v1.value || v1; + const note2 = v2.value || v2; + + if (note1.id === this.TRASH_FOLDER_ID) { + return 1; + } + + if (note2.id === this.TRASH_FOLDER_ID) { + return -1; + } + + if (note1.children === undefined && note2.children !== undefined) { + return 1; + } + + if (note1.children !== undefined && note2.children === undefined) { + return -1; + } + + const noteName1 = this.getNoteName(note1); + const noteName2 = this.getNoteName(note2); + + return noteName1.localeCompare(noteName2); + }; + + constructor(@Inject(TRASH_FOLDER_ID_TOKEN) private TRASH_FOLDER_ID: string) {} +} diff --git a/zeppelin-frontend/src/app/services/base-rest.ts b/zeppelin-frontend/src/app/services/base-rest.ts new file mode 100644 index 00000000000..412ae723669 --- /dev/null +++ b/zeppelin-frontend/src/app/services/base-rest.ts @@ -0,0 +1,33 @@ +import { BaseUrlService } from './base-url.service'; + +/** + * @private + */ +export class BaseRest { + constructor(public baseUrlService: BaseUrlService) {} + + /** + * ```ts + * this.restUrl`/user/${username}` + * this.restUrl(['/user/'], username) + * this.restUrl(`/user/${username}`) + * ``` + * @param str` + * @param values + */ + restUrl(str: TemplateStringsArray | string, ...values): string { + let output = this.baseUrlService.getRestApiBase(); + + if (typeof str === 'string') { + return `${output}${str}`; + } + + let index; + for (index = 0; index < values.length; index++) { + output += str[index] + values[index]; + } + + output += str[index]; + return output; + } +} diff --git a/zeppelin-frontend/src/app/services/completion.service.ts b/zeppelin-frontend/src/app/services/completion.service.ts new file mode 100644 index 00000000000..7bbc89f1699 --- /dev/null +++ b/zeppelin-frontend/src/app/services/completion.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { filter, map, take } from 'rxjs/operators'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { CompletionReceived, OP } from '@zeppelin/sdk'; + +import { MessageService } from './message.service'; + +@Injectable({ + providedIn: 'root' +}) +export class CompletionService extends MessageListenersManager { + private completionLanguages = ['python', 'scala']; + private completionItem$ = new Subject(); + private receivers = new WeakMap(); + private bound = false; + + constructor(messageService: MessageService) { + super(messageService); + } + + @MessageListener(OP.COMPLETION_LIST) + onCompletion(data?: CompletionReceived): void { + console.log('on receive!', data.id); + this.completionItem$.next(data); + } + + registerAsCompletionReceiver(model: monaco.editor.ITextModel, pid: string): void { + if (this.receivers.has(model)) { + return; + } + + if (!this.bound) { + this.bindMonacoCompletion(); + this.bound = true; + } + + this.receivers.set(model, pid); + } + + unregister(model: monaco.editor.ITextModel): void { + this.receivers.delete(model); + } + + private bindMonacoCompletion(): void { + // tslint:disable-next-line:no-this-assignment + const that = this; + + this.completionLanguages.forEach(l => { + monaco.languages.registerCompletionItemProvider(l, { + provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) { + const id = that.getIdForModel(model); + + if (!id) { + return { suggestions: null }; + } + + that.messageService.completion(id, model.getValue(), model.getOffsetAt(position)); + + return that.completionItem$ + .pipe( + filter(d => d.id === id), + take(1), + map(d => { + return { + suggestions: d.completions.map(i => ({ + kind: monaco.languages.CompletionItemKind.Keyword, + label: i.name, + insertText: i.name, + range: undefined + })) + }; + }) + ) + .toPromise(); + } + }); + }); + } + + private getIdForModel(model?: monaco.editor.ITextModel): string | null { + return this.receivers.get(model); + } +} diff --git a/zeppelin-frontend/src/app/services/helium.service.ts b/zeppelin-frontend/src/app/services/helium.service.ts new file mode 100644 index 00000000000..cfd45321702 --- /dev/null +++ b/zeppelin-frontend/src/app/services/helium.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class HeliumService { + getSpellByMagic(magic: string): string { + return null; + } + + constructor() {} +} diff --git a/zeppelin-frontend/src/app/services/ng-z.service.ts b/zeppelin-frontend/src/app/services/ng-z.service.ts new file mode 100644 index 00000000000..e036666b91b --- /dev/null +++ b/zeppelin-frontend/src/app/services/ng-z.service.ts @@ -0,0 +1,73 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class NgZService implements OnDestroy { + private paragraphMap = new Map(); + private contextChange$ = new Subject<{ + paragraphId: string; + key: string; + // tslint:disable-next-line:no-any + value: any; + emit: boolean; + set: boolean; + }>(); + private runParagraph$ = new Subject(); + + constructor() {} + + contextChanged() { + return this.contextChange$.asObservable(); + } + + runParagraphAction() { + return this.runParagraph$.asObservable(); + } + + removeParagraph(paragraphId: string) { + this.paragraphMap.delete(paragraphId); + } + + runParagraph(paragraphId: string) { + this.runParagraph$.next(paragraphId); + } + + bindParagraph(paragraphId: string, context: {}) { + this.paragraphMap.set(paragraphId, context); + } + + setContextValue(key: string, value, paragraphId: string, emit = true) { + const context = this.paragraphMap.get(paragraphId); + if (context) { + context[key] = value; + } + this.contextChange$.next({ + paragraphId, + key, + value, + emit, + set: true + }); + } + + unsetContextValue(key: string, paragraphId: string, emit = true) { + const context = this.paragraphMap.get(paragraphId); + if (context) { + context[key] = undefined; + } + this.contextChange$.next({ + paragraphId, + key, + emit, + value: undefined, + set: false + }); + } + + ngOnDestroy(): void { + this.paragraphMap.clear(); + this.contextChange$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/services/note-action.service.ts b/zeppelin-frontend/src/app/services/note-action.service.ts new file mode 100644 index 00000000000..04b3c077f44 --- /dev/null +++ b/zeppelin-frontend/src/app/services/note-action.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; + +import { NzModalService } from 'ng-zorro-antd'; + +import { FolderRenameComponent } from '@zeppelin/share/folder-rename/folder-rename.component'; +import { NoteCreateComponent } from '@zeppelin/share/note-create/note-create.component'; +import { NoteImportComponent } from '@zeppelin/share/note-import/note-import.component'; +import { NoteRenameComponent } from '@zeppelin/share/note-rename/note-rename.component'; + +@Injectable({ + providedIn: 'root' +}) +export class NoteActionService { + renameNote(id: string, path: string, name: string) { + this.nzModalService.create({ + nzTitle: 'Rename note', + nzContent: NoteRenameComponent, + nzComponentParams: { + id, + newName: path || name + }, + nzWidth: '800px', + nzFooter: null + }); + } + + renameFolder(path: string) { + this.nzModalService.create({ + nzTitle: 'Rename folder', + nzContent: FolderRenameComponent, + nzComponentParams: { + folderId: path, + newFolderPath: path + }, + nzWidth: '800px', + nzFooter: null + }); + } + + importNote() { + this.nzModalService.create({ + nzTitle: 'Import New Note', + nzContent: NoteImportComponent, + nzWidth: '800px', + nzFooter: null + }); + } + + createNote(path?: string) { + this.nzModalService.create({ + nzTitle: 'Create New Note', + nzContent: NoteCreateComponent, + nzComponentParams: { path }, + nzWidth: '800px', + nzFooter: null + }); + } + + constructor(private nzModalService: NzModalService) {} +} diff --git a/zeppelin-frontend/src/app/services/note-list.service.ts b/zeppelin-frontend/src/app/services/note-list.service.ts new file mode 100644 index 00000000000..dae9f178534 --- /dev/null +++ b/zeppelin-frontend/src/app/services/note-list.service.ts @@ -0,0 +1,85 @@ +import { Inject, Injectable } from '@angular/core'; + +import { TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; +import { NotesInfoItem } from '@zeppelin/sdk'; + +import { NodeList } from '../interfaces/node-list'; +import { ArrayOrderingService } from './array-ordering.service'; + +@Injectable({ + providedIn: 'root' +}) +export class NoteListService { + notes: NodeList = { + root: { children: [] }, + flatList: [], + flatFolderMap: {} + }; + + setNotes(notesList: NotesInfoItem[]) { + // a flat list to boost searching + this.notes.flatList = notesList.map(note => { + const isTrash = note.path ? note.path.split('/')[1] === this.TRASH_FOLDER_ID : false; + return { ...note, isTrash }; + }); + + // construct the folder-based tree + this.notes.root = { children: [] }; + this.notes.flatFolderMap = {}; + notesList.reduce((root, note) => { + const notePath = note.path || note.id; + const nodes = notePath.match(/([^\/][^\/]*)/g); + + // recursively add nodes + this.addNode(root, nodes, note.id); + + return root; + }, this.notes.root); + this.notes.root.children.sort(this.arrayOrderingService.noteComparator); + } + + addNode(curDir, nodes, noteId) { + if (nodes.length === 1) { + // the leaf + curDir.children.push({ + id: noteId, + title: nodes[0], + isLeaf: true, + nodeType: 'note', + path: curDir.id ? `${curDir.id}/${nodes[0]}` : nodes[0], + isTrash: curDir.id ? curDir.id.split('/')[0] === this.TRASH_FOLDER_ID : false + }); + } else { + // a folder node + const node = nodes.shift(); + const dir = curDir.children.find(c => { + return c.title === node && c.children !== undefined; + }); + if (dir !== undefined) { + // found an existing dir + this.addNode(dir, nodes, noteId); + } else { + const id = curDir.id ? `${curDir.id}/${node}` : node; + const newDir = { + id, + title: node, + expanded: false, + nodeType: id === this.TRASH_FOLDER_ID ? 'trash' : 'folder', + children: [], + isTrash: curDir.id ? curDir.id.split('/')[0] === this.TRASH_FOLDER_ID : false + }; + + // add the folder to flat folder map + this.notes.flatFolderMap[newDir.id] = newDir; + + curDir.children.push(newDir); + this.addNode(newDir, nodes, noteId); + } + } + } + + constructor( + @Inject(TRASH_FOLDER_ID_TOKEN) public TRASH_FOLDER_ID: string, + private arrayOrderingService: ArrayOrderingService + ) {} +} diff --git a/zeppelin-frontend/src/app/services/note-status.service.ts b/zeppelin-frontend/src/app/services/note-status.service.ts new file mode 100644 index 00000000000..baf4a6932e3 --- /dev/null +++ b/zeppelin-frontend/src/app/services/note-status.service.ts @@ -0,0 +1,53 @@ +import { Inject, Injectable } from '@angular/core'; + +import { TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; +import { Note, ParagraphItem } from '@zeppelin/sdk'; + +export const ParagraphStatus = { + READY: 'READY', + PENDING: 'PENDING', + RUNNING: 'RUNNING', + FINISHED: 'FINISHED', + ABORT: 'ABORT', + ERROR: 'ERROR' +}; + +@Injectable({ + providedIn: 'root' +}) +export class NoteStatusService { + isParagraphRunning(paragraph: ParagraphItem) { + if (!paragraph) { + return false; + } + const status = paragraph.status; + if (!status) { + return false; + } + + return status === ParagraphStatus.PENDING || status === ParagraphStatus.RUNNING; + } + + isTrash(note: Note['note']) { + // TODO https://github.com/apache/zeppelin/pull/3365/files + return note.name.split('/')[1] === this.TRASH_FOLDER_ID; + } + + viewOnly(note: Note['note']): boolean { + return note.config.looknfeel === 'report'; + } + + isNoteParagraphRunning(note: Note['note']): boolean { + if (!note) { + return false; + } else { + return note.paragraphs.some(p => this.isParagraphRunning(p)); + } + } + + isEntireNoteRunning(note: Note['note']): boolean { + return !!(note.info && note.info.isRunning && note.info.isRunning === true); + } + + constructor(@Inject(TRASH_FOLDER_ID_TOKEN) public TRASH_FOLDER_ID: string) {} +} diff --git a/zeppelin-frontend/src/app/services/note-var-share.service.ts b/zeppelin-frontend/src/app/services/note-var-share.service.ts new file mode 100644 index 00000000000..48196055fb0 --- /dev/null +++ b/zeppelin-frontend/src/app/services/note-var-share.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class NoteVarShareService { + private store = new Map(); + + clear() { + this.store.clear(); + } + + set(key, value) { + this.store.set(key, value); + } + + get(key) { + return this.store.get(key); + } + + del(key) { + return this.store.delete(key); + } + + constructor() {} +} diff --git a/zeppelin-frontend/src/app/services/public-api.ts b/zeppelin-frontend/src/app/services/public-api.ts index 4bcef3502db..2509562e9b3 100644 --- a/zeppelin-frontend/src/app/services/public-api.ts +++ b/zeppelin-frontend/src/app/services/public-api.ts @@ -1,4 +1,16 @@ export * from './base-url.service'; +export * from './ticket.service'; export * from './message.service'; export * from './job-manager.service'; export * from './interpreter.service'; +export * from './security.service'; +export * from './note-status.service'; +export * from './save-as.service'; +export * from './helium.service'; +export * from './note-var-share.service'; +export * from './note-action.service'; +export * from './completion.service'; +export * from './ng-z.service'; +export * from './array-ordering.service'; +export * from './note-list.service'; +export * from './runtime-compiler.service'; diff --git a/zeppelin-frontend/src/app/services/runtime-compiler.service.ts b/zeppelin-frontend/src/app/services/runtime-compiler.service.ts new file mode 100644 index 00000000000..f19ddd95d81 --- /dev/null +++ b/zeppelin-frontend/src/app/services/runtime-compiler.service.ts @@ -0,0 +1,62 @@ +import { CommonModule } from '@angular/common'; +import { + Compiler, + Component, + Injectable, + ModuleWithComponentFactories, + NgModule, + NgModuleFactory, + Type +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NgZorroAntdModule } from 'ng-zorro-antd'; + +import { NgZService } from './ng-z.service'; + +export class DynamicTemplate { + constructor( + public readonly template: string, + // tslint:disable-next-line:no-any + public readonly component: Type, + // tslint:disable-next-line:no-any + public readonly moduleFactory?: NgModuleFactory + ) {} +} + +@Injectable({ + providedIn: 'root' +}) +export class RuntimeCompilerService { + // tslint:disable-next-line:no-any + private compiledModule?: ModuleWithComponentFactories; + + public async createAndCompileTemplate(paragraphId: string, template: string): Promise { + const ngZService = this.ngZService; + const dynamicComponent = Component({ template: template })( + class DynamicTemplateComponent { + z = { + set: (key: string, value, id: string) => ngZService.setContextValue(key, value, id), + unset: (key: string, id: string) => ngZService.unsetContextValue(key, id), + run: (id: string) => ngZService.runParagraph(id) + }; + + constructor() { + ngZService.bindParagraph(paragraphId, this); + Object.freeze(this.z); + } + } + ); + const dynamicModule = NgModule({ + declarations: [dynamicComponent], + exports: [dynamicComponent], + entryComponents: [dynamicComponent], + imports: [CommonModule, NgZorroAntdModule, FormsModule] + })(class DynamicModule {}); + + this.compiledModule = await this.compiler.compileModuleAndAllComponentsAsync(dynamicModule); + return new DynamicTemplate(template, dynamicComponent, this.compiledModule.ngModuleFactory); + } + + constructor(private compiler: Compiler, private ngZService: NgZService) {} +} diff --git a/zeppelin-frontend/src/app/services/save-as.service.ts b/zeppelin-frontend/src/app/services/save-as.service.ts new file mode 100644 index 00000000000..81d20ac6b8e --- /dev/null +++ b/zeppelin-frontend/src/app/services/save-as.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class SaveAsService { + saveAs(content: string, filename: string, extension: string) { + const BOM = '\uFEFF'; + const fileName = `${filename}.${extension}`; + const binaryData = []; + binaryData.push(BOM); + binaryData.push(content); + const blob = new Blob(binaryData, { type: 'octet/stream' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + document.body.appendChild(a); + a.style.display = 'none'; + a.href = url; + a.download = fileName; + a.click(); + window.URL.revokeObjectURL(url); + } + + constructor() {} +} diff --git a/zeppelin-frontend/src/app/services/security.service.ts b/zeppelin-frontend/src/app/services/security.service.ts new file mode 100644 index 00000000000..5fd5a1cd32d --- /dev/null +++ b/zeppelin-frontend/src/app/services/security.service.ts @@ -0,0 +1,28 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { Permissions, SecurityUserList } from '@zeppelin/interfaces'; + +import { BaseRest } from './base-rest'; +import { BaseUrlService } from './base-url.service'; + +@Injectable({ + providedIn: 'root' +}) +export class SecurityService extends BaseRest { + constructor(baseUrlService: BaseUrlService, private http: HttpClient) { + super(baseUrlService); + } + + searchUsers(term: string) { + return this.http.get(this.restUrl`/security/userlist/${term}`); + } + + getPermissions(id: string) { + return this.http.get(this.restUrl`/notebook/${id}/permissions`); + } + + setPermissions(id: string, permissions: Permissions) { + return this.http.put(this.restUrl`/notebook/${id}/permissions`, permissions); + } +} diff --git a/zeppelin-frontend/src/app/services/ticket.service.ts b/zeppelin-frontend/src/app/services/ticket.service.ts new file mode 100644 index 00000000000..521f898d8c5 --- /dev/null +++ b/zeppelin-frontend/src/app/services/ticket.service.ts @@ -0,0 +1,104 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { forkJoin, BehaviorSubject, Subject } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +import { NzMessageService } from 'ng-zorro-antd'; + +import { ITicket, ITicketWrapped, IZeppelinVersion } from '@zeppelin/interfaces'; +import { ConfigurationsInfo } from '@zeppelin/sdk'; + +import { BaseUrlService } from './base-url.service'; + +@Injectable({ + providedIn: 'root' +}) +export class TicketService { + configuration: ConfigurationsInfo['configurations']; + ticket = new ITicketWrapped(); + originTicket = new ITicket(); + ticket$ = new Subject(); + logout$ = new BehaviorSubject(false); + version: string; + + setConfiguration(conf: ConfigurationsInfo) { + this.configuration = conf.configurations; + } + + getTicket() { + return forkJoin([ + this.httpClient.get(`${this.baseUrlService.getRestApiBase()}/security/ticket`), + this.getZeppelinVersion() + ]).pipe( + tap(data => { + const [ticket, version] = data; + this.version = version; + this.setTicket(ticket); + }) + ); + } + + setTicket(ticket: ITicket) { + if (ticket.redirectURL) { + window.location.href = ticket.redirectURL + window.location.href; + } + let screenUsername = ticket.principal; + if (ticket.principal.indexOf('#Pac4j') === 0) { + const re = ', name=(.*?),'; + screenUsername = ticket.principal.match(re)[1]; + } + this.originTicket = ticket; + this.ticket = { ...ticket, screenUsername, ...{ init: true } }; + this.ticket$.next(this.ticket); + } + + clearTicket() { + this.ticket = new ITicketWrapped(); + this.originTicket = new ITicket(); + } + + logout() { + this.logout$.next(true); + const nextAction = () => { + this.nzMessageService.success('Logout Success'); + this.clearTicket(); + this.logout$.next(false); + this.router.navigate(['/login']).then(); + }; + return this.httpClient + .post(`${this.baseUrlService.getRestApiBase()}/login/logout`, {}) + .pipe(tap(() => nextAction(), () => nextAction())); + } + + login(userName: string, password: string) { + return this.httpClient + .post(`${this.baseUrlService.getRestApiBase()}/login`, `password=${password}&userName=${userName}`, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }) + .pipe( + tap( + data => { + this.nzMessageService.success('Login Success'); + this.setTicket(data); + }, + () => { + this.nzMessageService.warning("The username and password that you entered don't match."); + } + ) + ); + } + + getZeppelinVersion() { + return this.httpClient + .get(`${this.baseUrlService.getRestApiBase()}/version`) + .pipe(map(data => data.version)); + } + + constructor( + private httpClient: HttpClient, + private baseUrlService: BaseUrlService, + private router: Router, + private nzMessageService: NzMessageService + ) {} +} diff --git a/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.html b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.html new file mode 100644 index 00000000000..0f3a14c35a0 --- /dev/null +++ b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.html @@ -0,0 +1,16 @@ + diff --git a/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.less b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.less new file mode 100644 index 00000000000..b4fa763c719 --- /dev/null +++ b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.less @@ -0,0 +1,26 @@ +@import 'theme-mixin'; + +.themeMixin({ + .modal { + .about-logo { + img { + width: 95%; + } + } + + .content { + text-align: center; + + h3 { + font-family: 'Patua One', cursive; + color: #3071A9; + font-size: 30px; + margin: 0 auto; + } + + .about-version { + font-weight: 500; + } + } + } +}); diff --git a/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.ts b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.ts new file mode 100644 index 00000000000..dec6d245364 --- /dev/null +++ b/zeppelin-frontend/src/app/share/about-zeppelin/about-zeppelin.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { TicketService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-about-zeppelin', + templateUrl: './about-zeppelin.component.html', + styleUrls: ['./about-zeppelin.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AboutZeppelinComponent implements OnInit { + constructor(public ticketService: TicketService) {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.html b/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.html new file mode 100644 index 00000000000..dca46e688d1 --- /dev/null +++ b/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.html @@ -0,0 +1,20 @@ +
+ + Please enter a new name + + + + + + + + + + +
+ + + The folder will be merged into {{newFolderPath}}. Are you sure? + diff --git a/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.less b/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.ts b/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.ts new file mode 100644 index 00000000000..47721b1b730 --- /dev/null +++ b/zeppelin-frontend/src/app/share/folder-rename/folder-rename.component.ts @@ -0,0 +1,68 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { NzModalRef } from 'ng-zorro-antd'; + +import { MessageService } from '@zeppelin/services/message.service'; +import { NoteListService } from '@zeppelin/services/note-list.service'; + +@Component({ + selector: 'zeppelin-folder-rename', + templateUrl: './folder-rename.component.html', + styleUrls: ['./folder-rename.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FolderRenameComponent implements OnInit { + @Input() newFolderPath: string; + @Input() folderId: string; + willMerged = false; + + checkMerged() { + const newFolderPath = this.normalizeFolderId(this.newFolderPath); + this.willMerged = this.folderId !== this.newFolderPath && !!this.noteListService.notes.flatFolderMap[newFolderPath]; + this.cdr.markForCheck(); + } + + rename() { + this.messageService.folderRename(this.folderId, this.newFolderPath); + this.nzModalRef.destroy(); + } + + normalizeFolderId(folderId) { + let normalizeFolderId = folderId.trim(); + + while (normalizeFolderId.indexOf('\\') > -1) { + normalizeFolderId = normalizeFolderId.replace('\\', '/'); + } + + while (normalizeFolderId.indexOf('///') > -1) { + normalizeFolderId = normalizeFolderId.replace('///', '/'); + } + + normalizeFolderId = normalizeFolderId.replace('//', '/'); + + if (normalizeFolderId === '/') { + return '/'; + } + + if (normalizeFolderId[0] === '/') { + normalizeFolderId = normalizeFolderId.substring(1); + } + + if (normalizeFolderId.slice(-1) === '/') { + normalizeFolderId = normalizeFolderId.slice(0, -1); + } + + return normalizeFolderId; + } + + constructor( + private noteListService: NoteListService, + private cdr: ChangeDetectorRef, + private messageService: MessageService, + private nzModalRef: NzModalRef + ) {} + + ngOnInit() { + this.checkMerged(); + } +} diff --git a/zeppelin-frontend/src/app/share/header/header.component.html b/zeppelin-frontend/src/app/share/header/header.component.html new file mode 100644 index 00000000000..a26c8224deb --- /dev/null +++ b/zeppelin-frontend/src/app/share/header/header.component.html @@ -0,0 +1,65 @@ +
+ + +
+ + + + + + + +
+ +
diff --git a/zeppelin-frontend/src/app/share/header/header.component.less b/zeppelin-frontend/src/app/share/header/header.component.less new file mode 100644 index 00000000000..5c9b303fd9b --- /dev/null +++ b/zeppelin-frontend/src/app/share/header/header.component.less @@ -0,0 +1,98 @@ +@import 'theme-mixin'; + +.themeMixin({ + .small-icon { + margin-left: 6px; + font-size: 12px; + margin-right: 0; + transform: scale(0.8); + } + .node-list-trigger { + height: 100%; + display: inline-block; + margin: 0 -20px; + padding: 0 20px; + color: @text-color; + } + .header { + position: relative; + z-index: 999; + width: 100%; + height: 50px; + background: @component-background; + padding: 0 15px; + overflow: hidden; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.06) + } + .brand { + float: left; + width: 200px; + } + .logo { + height: 50px; + width: 75px; + background-size: 50px 30px; + background-repeat: no-repeat; + background-position: center; + background-image: url("../../../assets/images/zeppelin_svg_logo.svg"); + float: left; + } + .title { + float: left; + line-height: 50px; + font-family: 'Patua One', cursive; + font-size: 25px; + color: @primary-color; + } + .nav { + float: left; + + ul { + position: relative; + top: 2px; + border-bottom: none; + } + + a { + font-weight: 500; + } + } + .search { + float: right; + padding: 9px; + width: 300px; + margin-right: 24px; + } + .user { + float: right; + + .status { + line-height: 50px; + height: 50px; + display: inline-block; + color: @text-color; + } + } + .modal { + .about-logo { + img { + width: 95%; + } + } + + .content { + text-align: center; + + h3 { + font-family: 'Patua One', cursive; + color: #3071A9; + font-size: 30px; + margin: 0 auto; + } + + .about-version { + font-weight: 500; + } + } + } +}); diff --git a/zeppelin-frontend/src/app/share/header/header.component.ts b/zeppelin-frontend/src/app/share/header/header.component.ts new file mode 100644 index 00000000000..ddc8672a94f --- /dev/null +++ b/zeppelin-frontend/src/app/share/header/header.component.ts @@ -0,0 +1,74 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { NzModalService } from 'ng-zorro-antd'; + +import { Subject } from 'rxjs'; +import { filter, takeUntil } from 'rxjs/operators'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { MessageReceiveDataTypeMap, OP } from '@zeppelin/sdk'; +import { MessageService, TicketService } from '@zeppelin/services'; +import { AboutZeppelinComponent } from '@zeppelin/share/about-zeppelin/about-zeppelin.component'; + +@Component({ + selector: 'zeppelin-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HeaderComponent extends MessageListenersManager implements OnInit, OnDestroy { + private destroy$ = new Subject(); + connectStatus = 'error'; + noteListVisible = false; + + about() { + this.nzModalService.create({ + nzTitle: 'About Zeppelin', + nzWidth: '600px', + nzContent: AboutZeppelinComponent, + nzFooter: null + }); + } + + logout() { + this.ticketService.logout().subscribe(); + } + + @MessageListener(OP.CONFIGURATIONS_INFO) + getConfiguration(data: MessageReceiveDataTypeMap[OP.CONFIGURATIONS_INFO]) { + this.ticketService.setConfiguration(data); + } + + constructor( + public ticketService: TicketService, + private nzModalService: NzModalService, + public messageService: MessageService, + private router: Router, + private cdr: ChangeDetectorRef + ) { + super(messageService); + } + + ngOnInit() { + this.messageService.listConfigurations(); + this.messageService.connectedStatus$.pipe(takeUntil(this.destroy$)).subscribe(status => { + this.connectStatus = status ? 'success' : 'error'; + this.cdr.markForCheck(); + }); + this.router.events + .pipe( + filter(e => e instanceof NavigationEnd), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.noteListVisible = false; + this.cdr.markForCheck(); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } +} diff --git a/zeppelin-frontend/src/app/share/index.ts b/zeppelin-frontend/src/app/share/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/share/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/share/node-list/node-list.component.html b/zeppelin-frontend/src/app/share/node-list/node-list.component.html new file mode 100644 index 00000000000..507da61ce47 --- /dev/null +++ b/zeppelin-frontend/src/app/share/node-list/node-list.component.html @@ -0,0 +1,127 @@ + diff --git a/zeppelin-frontend/src/app/share/node-list/node-list.component.less b/zeppelin-frontend/src/app/share/node-list/node-list.component.less new file mode 100644 index 00000000000..5dedcbcf8f6 --- /dev/null +++ b/zeppelin-frontend/src/app/share/node-list/node-list.component.less @@ -0,0 +1,70 @@ +@import 'theme-mixin'; + +:host { + display: block; +} + +.themeMixin({ + .content { + width: 100%; + position: relative; + + &.header-mode { + .operation { + display: none; + } + } + } + .rename { + width: 200px; + } + nz-tree { + display: block; + } + nz-input-group { + width: 100%; + margin-top: 12px; + margin-bottom: 6px; + } + .node { + line-height: 24px; + display: inline-block; + width: calc(~"100% - 24px"); + + &.not-matched { + opacity: 0.5; + filter: grayscale(1); + } + + i { + margin-right: 6px; + } + + .operation { + margin-left: 12px; + + i { + font-size: 12px; + display: none; + + &:hover { + color: @link-active-color; + } + } + } + + &.active { + .name { + color: @link-active-color; + } + } + + &:hover, &.active { + .operation { + i { + display: inline-block; + } + } + } + } +}); diff --git a/zeppelin-frontend/src/app/share/node-list/node-list.component.ts b/zeppelin-frontend/src/app/share/node-list/node-list.component.ts new file mode 100644 index 00000000000..22d9b196beb --- /dev/null +++ b/zeppelin-frontend/src/app/share/node-list/node-list.component.ts @@ -0,0 +1,105 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { NzModalService, NzTreeNode } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { MessageReceiveDataTypeMap, OP } from '@zeppelin/sdk'; +import { MessageService, NoteActionService, NoteListService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-node-list', + templateUrl: './node-list.component.html', + providers: [NoteListService], + styleUrls: ['./node-list.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NodeListComponent extends MessageListenersManager implements OnInit { + @Input() headerMode = false; + searchValue: string; + nodes = []; + activatedId: string; + + activeNote(id: string) { + this.activatedId = id; + } + + moveFolderToTrash(id: string) { + return this.messageService.moveFolderToTrash(id); + } + + restoreFolder(id: string) { + return this.messageService.restoreFolder(id); + } + + removeFolder(id: string) { + return this.messageService.removeFolder(id); + } + + paragraphClearAllOutput(id: string) { + return this.messageService.paragraphClearAllOutput(id); + } + + moveNoteToTrash(id: string) { + return this.messageService.moveNoteToTrash(id); + } + + restoreNote(id: string) { + return this.messageService.restoreNote(id); + } + + deleteNote(id: string) { + return this.messageService.deleteNote(id); + } + + restoreAll() { + return this.messageService.restoreAll(); + } + + emptyTrash() { + return this.messageService.emptyTrash(); + } + + toggleFolder(node: NzTreeNode) { + node.isExpanded = !node.isExpanded; + this.cdr.markForCheck(); + } + + renameNote(id: string, path: string, name: string) { + this.noteActionService.renameNote(id, path, name); + } + + renameFolder(path) { + this.noteActionService.renameFolder(path); + } + + importNote() { + this.noteActionService.importNote(); + } + + createNote(path?: string) { + this.noteActionService.createNote(path); + } + + @MessageListener(OP.NOTES_INFO) + getNotes(data: MessageReceiveDataTypeMap[OP.NOTES_INFO]) { + this.noteListService.setNotes(data.notes); + this.nodes = this.noteListService.notes.root.children.map(item => { + return { ...item, key: item.id }; + }); + this.cdr.markForCheck(); + } + + constructor( + private noteListService: NoteListService, + public messageService: MessageService, + private nzModalService: NzModalService, + private noteActionService: NoteActionService, + private cdr: ChangeDetectorRef + ) { + super(messageService); + } + + ngOnInit() { + this.messageService.listNodes(); + } +} diff --git a/zeppelin-frontend/src/app/share/note-create/note-create.component.html b/zeppelin-frontend/src/app/share/note-create/note-create.component.html new file mode 100644 index 00000000000..e748df25e83 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-create/note-create.component.html @@ -0,0 +1,32 @@ +
+ + + Clone Note + Import As + + + + + + + Default Interpreter + + + + + + + + + + + + + +
+ diff --git a/zeppelin-frontend/src/app/share/note-create/note-create.component.less b/zeppelin-frontend/src/app/share/note-create/note-create.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/share/note-create/note-create.component.ts b/zeppelin-frontend/src/app/share/note-create/note-create.component.ts new file mode 100644 index 00000000000..28b4691e7d1 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-create/note-create.component.ts @@ -0,0 +1,93 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { NzModalRef } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { InterpreterItem, MessageReceiveDataTypeMap, Note, OP } from '@zeppelin/sdk'; +import { MessageService } from '@zeppelin/services/message.service'; +import { NoteListService } from '@zeppelin/services/note-list.service'; + +@Component({ + selector: 'zeppelin-note-create', + templateUrl: './note-create.component.html', + styleUrls: ['./note-create.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NoteCreateComponent extends MessageListenersManager implements OnInit { + @Input() path: string; + @Input() cloneNote: Note['note']; + noteName: string; + defaultInterpreter: string; + listOfInterpreter: InterpreterItem[] = []; + + @MessageListener(OP.INTERPRETER_SETTINGS) + getInterpreterSettings(data: MessageReceiveDataTypeMap[OP.INTERPRETER_SETTINGS]) { + this.listOfInterpreter = data.interpreterSettings; + this.defaultInterpreter = data.interpreterSettings[0].name; + this.cdr.markForCheck(); + } + + @MessageListener(OP.NOTES_INFO) + getNotes() { + this.nzModalRef.destroy(); + } + + newNoteName(path: string) { + let newCount = 1; + this.noteListService.notes.flatList.forEach(note => { + const noteName = note.path; + if (noteName.match(/^\/Untitled Note [0-9]*$/)) { + const lastCount = +noteName.substr(15); + if (newCount <= lastCount) { + newCount = lastCount + 1; + } + } + }); + return `${path ? path + '/' : ''}Untitled Note ${newCount}`; + } + + cloneNoteName() { + let copyCount = 1; + let newCloneName = ''; + const lastIndex = this.cloneNote.name.lastIndexOf(' '); + const endsWithNumber: boolean = !!this.cloneNote.name.match('^.+?\\s\\d$'); + const noteNamePrefix = endsWithNumber ? this.cloneNote.name.substr(0, lastIndex) : this.cloneNote.name; + const regexp = new RegExp(`^${noteNamePrefix}.+`); + + this.noteListService.notes.flatList.forEach(note => { + const noteName = note.path; + if (noteName.match(regexp)) { + const lastCopyCount = parseInt(noteName.substr(lastIndex).trim(), 10); + newCloneName = noteNamePrefix; + if (copyCount <= lastCopyCount) { + copyCount = lastCopyCount + 1; + } + } + }); + + if (!newCloneName) { + newCloneName = this.cloneNote.name; + } + return `${newCloneName} ${copyCount}`; + } + + createNote() { + this.cloneNote + ? this.messageService.cloneNote(this.cloneNote.id, this.noteName) + : this.messageService.newNote(this.noteName, this.defaultInterpreter); + } + + constructor( + public messageService: MessageService, + private cdr: ChangeDetectorRef, + private noteListService: NoteListService, + private nzModalRef: NzModalRef + ) { + super(messageService); + } + + ngOnInit() { + this.messageService.getInterpreterSettings(); + this.noteName = this.cloneNote ? this.cloneNoteName() : this.newNoteName(this.path); + } +} diff --git a/zeppelin-frontend/src/app/share/note-import/note-import.component.html b/zeppelin-frontend/src/app/share/note-import/note-import.component.html new file mode 100644 index 00000000000..9835ebee2a2 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-import/note-import.component.html @@ -0,0 +1,38 @@ +
+ + Import As + + + + +
+ + + + +

+ +

+

Click or drag JSON file to this area to upload

+

+ JSON file size cannot exceed {{maxLimit | humanizeBytes}} +

+
+
+ +
+ + URL + + + + + + + + + +
+
+
+ diff --git a/zeppelin-frontend/src/app/share/note-import/note-import.component.less b/zeppelin-frontend/src/app/share/note-import/note-import.component.less new file mode 100644 index 00000000000..39bdfd39ad9 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-import/note-import.component.less @@ -0,0 +1,7 @@ +@import "theme-mixin"; + +.themeMixin({ + nz-alert { + margin-top: 12px; + } +}); diff --git a/zeppelin-frontend/src/app/share/note-import/note-import.component.ts b/zeppelin-frontend/src/app/share/note-import/note-import.component.ts new file mode 100644 index 00000000000..1f25b3ed581 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-import/note-import.component.ts @@ -0,0 +1,98 @@ +import { HttpClient } from '@angular/common/http'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; + +import { get } from 'lodash'; +import { NzModalRef, UploadFile } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { OP } from '@zeppelin/sdk'; +import { MessageService } from '@zeppelin/services/message.service'; +import { TicketService } from '@zeppelin/services/ticket.service'; + +@Component({ + selector: 'zeppelin-note-import', + templateUrl: './note-import.component.html', + styleUrls: ['./note-import.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NoteImportComponent extends MessageListenersManager implements OnInit { + noteImportName: string; + importUrl: string; + errorText: string; + importLoading = false; + maxLimit = get(this.ticketService.configuration, ['zeppelin.websocket.max.text.message.size'], null); + + @MessageListener(OP.NOTES_INFO) + getNotes() { + this.nzModalRef.destroy(); + } + + importNote() { + this.errorText = ''; + this.importLoading = true; + this.httpClient.get(this.importUrl).subscribe( + data => { + this.importLoading = false; + this.processImportJson(data); + this.cdr.markForCheck(); + }, + () => { + this.errorText = 'Unable to Fetch URL'; + this.importLoading = false; + this.cdr.markForCheck(); + }, + () => {} + ); + } + + beforeUpload = (file: UploadFile): boolean => { + this.errorText = ''; + if (file.size > this.maxLimit) { + this.errorText = 'File size limit Exceeded!'; + } else { + const reader = new FileReader(); + // tslint:disable-next-line:no-any + reader.readAsText(file as any); + reader.onloadend = () => { + this.processImportJson(reader.result); + }; + } + this.cdr.markForCheck(); + return false; + }; + + processImportJson(data) { + let result = data; + if (typeof result !== 'object') { + try { + result = JSON.parse(result); + } catch (e) { + this.errorText = 'JSON parse exception'; + return; + } + } + if (result.paragraphs && result.paragraphs.length > 0) { + if (!this.noteImportName) { + this.noteImportName = result.name; + } else { + result.name = this.noteImportName; + } + this.messageService.importNote(result); + } else { + this.errorText = 'Invalid JSON'; + } + this.cdr.markForCheck(); + } + + constructor( + private ticketService: TicketService, + public messageService: MessageService, + private cdr: ChangeDetectorRef, + private nzModalRef: NzModalRef, + private httpClient: HttpClient + ) { + super(messageService); + } + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/share/note-rename/note-rename.component.html b/zeppelin-frontend/src/app/share/note-rename/note-rename.component.html new file mode 100644 index 00000000000..58f867b3e87 --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-rename/note-rename.component.html @@ -0,0 +1,11 @@ +
+ + Please enter a new name + + + + + +
diff --git a/zeppelin-frontend/src/app/share/note-rename/note-rename.component.less b/zeppelin-frontend/src/app/share/note-rename/note-rename.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/share/note-rename/note-rename.component.ts b/zeppelin-frontend/src/app/share/note-rename/note-rename.component.ts new file mode 100644 index 00000000000..9d7139e942b --- /dev/null +++ b/zeppelin-frontend/src/app/share/note-rename/note-rename.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; + +import { NzModalRef } from 'ng-zorro-antd'; + +import { MessageService } from '@zeppelin/services/message.service'; + +@Component({ + selector: 'zeppelin-note-rename', + templateUrl: './note-rename.component.html', + styleUrls: ['./note-rename.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NoteRenameComponent implements OnInit { + @Input() newName: string; + @Input() id: string; + + rename() { + this.messageService.noteRename(this.id, this.newName); + this.nzModalRef.destroy(); + } + + constructor(private messageService: MessageService, private nzModalRef: NzModalRef) {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/share/page-header/page-header.component.html b/zeppelin-frontend/src/app/share/page-header/page-header.component.html new file mode 100644 index 00000000000..0c0e169ecb8 --- /dev/null +++ b/zeppelin-frontend/src/app/share/page-header/page-header.component.html @@ -0,0 +1,6 @@ + +

{{title}}

+

{{description}}

+ + +
diff --git a/zeppelin-frontend/src/app/share/page-header/page-header.component.less b/zeppelin-frontend/src/app/share/page-header/page-header.component.less new file mode 100644 index 00000000000..494070a70af --- /dev/null +++ b/zeppelin-frontend/src/app/share/page-header/page-header.component.less @@ -0,0 +1,5 @@ +:host { + .header-extra { + float: right; + } +} diff --git a/zeppelin-frontend/src/app/share/page-header/page-header.component.ts b/zeppelin-frontend/src/app/share/page-header/page-header.component.ts new file mode 100644 index 00000000000..1b8e7ec9183 --- /dev/null +++ b/zeppelin-frontend/src/app/share/page-header/page-header.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core'; +import { InputBoolean } from 'ng-zorro-antd'; + +@Component({ + selector: 'zeppelin-page-header', + templateUrl: './page-header.component.html', + styleUrls: ['./page-header.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PageHeaderComponent implements OnInit { + @Input() title: string; + @Input() description: string; + @Input() @InputBoolean() divider = false; + @Input() extra: TemplateRef; + + constructor() {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/share/pipes/humanize-bytes.pipe.ts b/zeppelin-frontend/src/app/share/pipes/humanize-bytes.pipe.ts new file mode 100644 index 00000000000..81495b5c9e0 --- /dev/null +++ b/zeppelin-frontend/src/app/share/pipes/humanize-bytes.pipe.ts @@ -0,0 +1,46 @@ +/* + * 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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'humanizeBytes' +}) +export class HumanizeBytesPipe implements PipeTransform { + transform(value: number): string { + if (value === null || value === undefined) { + return '-'; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; + const converter = (v: number, p: number): string => { + const base = Math.pow(1024, p); + if (v < base) { + return `${(v / base).toFixed(2)} ${units[p]}`; + } else if (v < base * 1000) { + return `${(v / base).toPrecision(3)} ${units[p]}`; + } else { + return converter(v, p + 1); + } + }; + if (value < 1000) { + return value + ' B'; + } else { + return converter(value, 1); + } + } +} diff --git a/zeppelin-frontend/src/app/share/pipes/index.ts b/zeppelin-frontend/src/app/share/pipes/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/share/pipes/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/share/pipes/public-api.ts b/zeppelin-frontend/src/app/share/pipes/public-api.ts new file mode 100644 index 00000000000..6f9b580d83b --- /dev/null +++ b/zeppelin-frontend/src/app/share/pipes/public-api.ts @@ -0,0 +1 @@ +export * from './humanize-bytes.pipe'; diff --git a/zeppelin-frontend/src/app/share/public-api.ts b/zeppelin-frontend/src/app/share/public-api.ts new file mode 100644 index 00000000000..7f9bbbe05ed --- /dev/null +++ b/zeppelin-frontend/src/app/share/public-api.ts @@ -0,0 +1,3 @@ +export * from './pipes'; +export * from './resize-handle'; +export * from './share.module'; diff --git a/zeppelin-frontend/src/app/share/run-scripts/run-scripts.directive.ts b/zeppelin-frontend/src/app/share/run-scripts/run-scripts.directive.ts new file mode 100644 index 00000000000..eec5e76d7d2 --- /dev/null +++ b/zeppelin-frontend/src/app/share/run-scripts/run-scripts.directive.ts @@ -0,0 +1,68 @@ +import { Directive, ElementRef, Input, NgZone, OnChanges, Renderer2, SimpleChanges } from '@angular/core'; +import { SafeHtml } from '@angular/platform-browser'; +import { take } from 'rxjs/operators'; + +const loadedExternalScripts = new Set(); + +@Directive({ + selector: '[zeppelinRunScripts]' +}) +export class RunScriptsDirective implements OnChanges { + @Input() scriptsContent: string | SafeHtml; + + constructor(private elementRef: ElementRef, private ngZone: NgZone, private renderer: Renderer2) {} + + runScripts(): void { + if (!this.scriptsContent.toString()) { + return; + } + this.ngZone.onStable.pipe(take(1)).subscribe(() => { + this.ngZone.runOutsideAngular(() => { + const scripts = this.elementRef.nativeElement.getElementsByTagName('script'); + const externalScripts = []; + const localScripts = []; + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + if (script.text) { + localScripts.push(script); + } else if (script.src) { + externalScripts.push(script); + } + this.renderer.removeChild(this.elementRef.nativeElement, script); + } + Promise.all(externalScripts.map(s => this.loadExternalScript(s, this.elementRef.nativeElement))).then(() => { + localScripts.forEach(s => this.loadLocalScript(s, this.elementRef.nativeElement)); + }); + }); + }); + } + + loadExternalScript(script: HTMLScriptElement, parentNode: HTMLElement): Promise { + return new Promise(resolve => { + if (loadedExternalScripts.has(script.src)) { + resolve(); + } + const scriptCopy = this.renderer.createElement('script') as HTMLScriptElement; + scriptCopy.type = script.type ? script.type : 'text/javascript'; + scriptCopy.src = script.src; + scriptCopy.onload = () => { + resolve(); + loadedExternalScripts.add(script.src); + }; + parentNode.appendChild(scriptCopy); + }); + } + + loadLocalScript(script: HTMLScriptElement, parentNode: HTMLElement): void { + const scriptCopy = this.renderer.createElement('script') as HTMLScriptElement; + scriptCopy.type = script.type ? script.type : 'text/javascript'; + scriptCopy.text = `(function() { ${script.text} })();`; + parentNode.appendChild(scriptCopy); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.scriptsContent) { + this.runScripts(); + } + } +} diff --git a/zeppelin-frontend/src/app/share/share.module.ts b/zeppelin-frontend/src/app/share/share.module.ts new file mode 100644 index 00000000000..8f1a0bf26d0 --- /dev/null +++ b/zeppelin-frontend/src/app/share/share.module.ts @@ -0,0 +1,84 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { + NzAlertModule, + NzBadgeModule, + NzButtonModule, + NzCardModule, + NzDividerModule, + NzDropDownModule, + NzFormModule, + NzGridModule, + NzIconModule, + NzInputModule, + NzMenuModule, + NzMessageModule, + NzModalModule, + NzNotificationModule, + NzPopconfirmModule, + NzProgressModule, + NzSelectModule, + NzTabsModule, + NzToolTipModule, + NzTreeModule, + NzUploadModule +} from 'ng-zorro-antd'; + +import { AboutZeppelinComponent } from '@zeppelin/share/about-zeppelin/about-zeppelin.component'; +import { FolderRenameComponent } from '@zeppelin/share/folder-rename/folder-rename.component'; +import { HeaderComponent } from '@zeppelin/share/header/header.component'; +import { NodeListComponent } from '@zeppelin/share/node-list/node-list.component'; +import { NoteCreateComponent } from '@zeppelin/share/note-create/note-create.component'; +import { NoteImportComponent } from '@zeppelin/share/note-import/note-import.component'; +import { NoteRenameComponent } from '@zeppelin/share/note-rename/note-rename.component'; +import { PageHeaderComponent } from '@zeppelin/share/page-header/page-header.component'; +import { HumanizeBytesPipe } from '@zeppelin/share/pipes'; +import { RunScriptsDirective } from '@zeppelin/share/run-scripts/run-scripts.directive'; +import { SpinComponent } from '@zeppelin/share/spin/spin.component'; + +const MODAL_LIST = [ + AboutZeppelinComponent, + NoteImportComponent, + NoteCreateComponent, + NoteRenameComponent, + FolderRenameComponent +]; +const EXPORT_LIST = [HeaderComponent, NodeListComponent, PageHeaderComponent, SpinComponent]; +const PIPES = [HumanizeBytesPipe]; + +@NgModule({ + declarations: [MODAL_LIST, EXPORT_LIST, PIPES, RunScriptsDirective], + entryComponents: [MODAL_LIST], + exports: [EXPORT_LIST, PIPES, RunScriptsDirective], + imports: [ + FormsModule, + CommonModule, + NzMenuModule, + NzIconModule, + NzInputModule, + NzDropDownModule, + NzBadgeModule, + NzGridModule, + NzModalModule, + NzTreeModule, + RouterModule, + NzButtonModule, + NzNotificationModule, + NzToolTipModule, + NzDividerModule, + NzMessageModule, + NzCardModule, + NzPopconfirmModule, + NzPopconfirmModule, + NzFormModule, + NzTabsModule, + NzUploadModule, + NzSelectModule, + NzAlertModule, + NzProgressModule + ] +}) +export class ShareModule {} diff --git a/zeppelin-frontend/src/app/share/spin/spin.component.html b/zeppelin-frontend/src/app/share/spin/spin.component.html new file mode 100644 index 00000000000..a5df45a038f --- /dev/null +++ b/zeppelin-frontend/src/app/share/spin/spin.component.html @@ -0,0 +1,9 @@ +
+
+ +
+

Zeppelin

+ +
+
+
diff --git a/zeppelin-frontend/src/app/share/spin/spin.component.less b/zeppelin-frontend/src/app/share/spin/spin.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/share/spin/spin.component.ts b/zeppelin-frontend/src/app/share/spin/spin.component.ts new file mode 100644 index 00000000000..8907e81df27 --- /dev/null +++ b/zeppelin-frontend/src/app/share/spin/spin.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; + +@Component({ + selector: 'zeppelin-spin', + templateUrl: './spin.component.html', + styleUrls: ['./spin.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpinComponent implements OnInit { + @Input() transparent = false; + constructor() {} + + ngOnInit() {} +} diff --git a/zeppelin-frontend/src/app/spell/spell-result.ts b/zeppelin-frontend/src/app/spell/spell-result.ts new file mode 100644 index 00000000000..a8f2624378f --- /dev/null +++ b/zeppelin-frontend/src/app/spell/spell-result.ts @@ -0,0 +1,15 @@ +export class SpellResult { + static extractMagic(allParagraphText) { + const pattern = /^\s*%(\S+)\s*/g; + try { + const match = pattern.exec(allParagraphText); + if (match) { + return `%${match[1].trim()}`; + } + } catch (error) { + // failed to parse, ignore + } + + return undefined; + } +} From 4ea8b5046d3a98a83c7cb2eb047b06963e183d45 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Tue, 22 Oct 2019 10:59:55 +0800 Subject: [PATCH 05/19] feat: add visualizations - area-chart - bar-chart - line-chart - pie-chart - scatter-chart - table --- .../helium-manager/helium-manager.module.ts | 15 + .../helium-manager/helium-manager.service.ts | 62 +++ .../src/app/helium-manager/index.ts | 1 + .../src/app/helium-manager/public-api.ts | 2 + .../create-repository-modal.component.html | 146 ++++++ .../create-repository-modal.component.less | 7 + .../create-repository-modal.component.ts | 66 +++ .../interpreter/interpreter-routing.module.ts | 17 + .../interpreter/interpreter.component.html | 54 +++ .../interpreter/interpreter.component.less | 39 ++ .../interpreter/interpreter.component.ts | 174 +++++++ .../interpreter/interpreter.module.ts | 61 +++ .../interpreter/item/item.component.html | 435 ++++++++++++++++++ .../interpreter/item/item.component.less | 93 ++++ .../interpreter/item/item.component.ts | 393 ++++++++++++++++ .../job-manager/job-manager-routing.module.ts | 17 + .../job-manager/job-manager.component.html | 62 +++ .../job-manager/job-manager.component.less | 38 ++ .../job-manager/job-manager.component.ts | 124 +++++ .../job-manager/job-manager.module.ts | 59 +++ .../job-status/job-status.component.html | 5 + .../job-status/job-status.component.less | 11 + .../job-status/job-status.component.ts | 25 + .../job-manager/job/job.component.html | 46 ++ .../job-manager/job/job.component.less | 58 +++ .../job-manager/job/job.component.ts | 69 +++ .../workspace/notebook/notebook.module.ts | 8 +- .../dynamic-forms.component.html | 39 ++ .../dynamic-forms.component.less | 12 + .../dynamic-forms/dynamic-forms.component.ts | 108 +++++ .../paragraph/paragraph.component.html | 14 - .../notebook/paragraph/paragraph.component.ts | 11 +- .../paragraph/result/result.component.html | 62 +++ .../paragraph/result/result.component.less | 59 +++ .../paragraph/result/result.component.ts | 348 ++++++++++++++ .../app/share/math-jax/math-jax.directive.ts | 12 + .../src/app/share/resize-handle/index.ts | 1 + .../src/app/share/resize-handle/public-api.ts | 1 + .../resize-handle.component.html | 11 + .../resize-handle.component.less | 11 + .../resize-handle/resize-handle.component.ts | 14 + .../src/app/share/share.module.ts | 8 +- .../area-chart-visualization.component.html | 23 + .../area-chart-visualization.component.less | 3 + .../area-chart-visualization.component.ts | 90 ++++ .../area-chart/area-chart-visualization.ts | 20 + .../bar-chart-visualization.component.html | 22 + .../bar-chart-visualization.component.less | 13 + .../bar-chart-visualization.component.ts | 95 ++++ .../bar-chart/bar-chart-visualization.ts | 19 + .../pivot-setting.component.html | 73 +++ .../pivot-setting.component.less | 12 + .../pivot-setting/pivot-setting.component.ts | 77 ++++ .../scatter-setting.component.html | 86 ++++ .../scatter-setting.component.less | 12 + .../scatter-setting.component.ts | 90 ++++ .../common/util/calc-tick-count.ts | 10 + .../visualizations/common/util/set-x-axis.ts | 34 ++ .../x-axis-setting.component.html | 26 ++ .../x-axis-setting.component.less | 6 + .../x-axis-setting.component.ts | 66 +++ .../line-chart-visualization.component.html | 32 ++ .../line-chart-visualization.component.less | 13 + .../line-chart-visualization.component.ts | 125 +++++ .../line-chart/line-chart-visualization.ts | 20 + .../pie-chart-visualization.component.html | 7 + .../pie-chart-visualization.component.less | 0 .../pie-chart-visualization.component.ts | 61 +++ .../pie-chart/pie-chart-visualization.ts | 20 + ...scatter-chart-visualization.component.html | 7 + ...scatter-chart-visualization.component.less | 0 .../scatter-chart-visualization.component.ts | 77 ++++ .../scatter-chart-visualization.ts | 20 + .../table/table-visualization.component.html | 111 +++++ .../table/table-visualization.component.less | 32 ++ .../table/table-visualization.component.ts | 162 +++++++ .../table/table-visualization.ts | 48 ++ .../visualizations/visualization.module.ts | 67 +++ 78 files changed, 4358 insertions(+), 19 deletions(-) create mode 100644 zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts create mode 100644 zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts create mode 100644 zeppelin-frontend/src/app/helium-manager/index.ts create mode 100644 zeppelin-frontend/src/app/helium-manager/public-api.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager-routing.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.ts create mode 100644 zeppelin-frontend/src/app/share/math-jax/math-jax.directive.ts create mode 100644 zeppelin-frontend/src/app/share/resize-handle/index.ts create mode 100644 zeppelin-frontend/src/app/share/resize-handle/public-api.ts create mode 100644 zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.html create mode 100644 zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.less create mode 100644 zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/common/util/calc-tick-count.ts create mode 100644 zeppelin-frontend/src/app/visualizations/common/util/set-x-axis.ts create mode 100644 zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/table/table-visualization.component.html create mode 100644 zeppelin-frontend/src/app/visualizations/table/table-visualization.component.less create mode 100644 zeppelin-frontend/src/app/visualizations/table/table-visualization.component.ts create mode 100644 zeppelin-frontend/src/app/visualizations/table/table-visualization.ts create mode 100644 zeppelin-frontend/src/app/visualizations/visualization.module.ts diff --git a/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts b/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts new file mode 100644 index 00000000000..1f812518dca --- /dev/null +++ b/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts @@ -0,0 +1,15 @@ +import { Compiler, CompilerFactory, COMPILER_OPTIONS, NgModule } from '@angular/core'; +import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; + +export function createCompiler(compilerFactory: CompilerFactory) { + return compilerFactory.createCompiler(); +} + +@NgModule({ + providers: [ + { provide: COMPILER_OPTIONS, useValue: {}, multi: true }, + { provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] }, + { provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] } + ] +}) +export class HeliumManagerModule {} diff --git a/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts new file mode 100644 index 00000000000..9f2d319f5d3 --- /dev/null +++ b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts @@ -0,0 +1,62 @@ +import { Compiler, Injectable, Injector, NgModuleFactory, OnDestroy, Type } from '@angular/core'; +import { ZeppelinHeliumPackage, ZeppelinHeliumService } from '@zeppelin/helium'; +import { of, BehaviorSubject } from 'rxjs'; +import { HeliumManagerModule } from './helium-manager.module'; + +export interface CompiledPackage { + // tslint:disable-next-line:no-any + moduleFactory: NgModuleFactory; + // tslint:disable-next-line:no-any + component: Type; + injector?: Injector; + name: string; + _raw: ZeppelinHeliumPackage; +} + +@Injectable({ + providedIn: HeliumManagerModule +}) +export class HeliumManagerService implements OnDestroy { + private packages$ = new BehaviorSubject([]); + + constructor(private zeppelinHeliumService: ZeppelinHeliumService, private compiler: Compiler) {} + + initPackages() { + this.getEnabledPackages().subscribe(packages => { + packages.forEach(name => { + this.zeppelinHeliumService.loadPackage(name).then(heliumPackage => { + const loaded = this.packages$.value; + if (!loaded.find(p => p.name === heliumPackage.name)) { + this.compilePackage(heliumPackage); + } + }); + }); + }); + } + + getEnabledPackages() { + return of(['helium-vis-example']); + } + + packagesLoadChange() { + return this.packages$.asObservable(); + } + + compilePackage(pack: ZeppelinHeliumPackage) { + this.compiler.compileModuleAsync(pack.module).then(moduleFactory => { + this.packages$.next([ + ...this.packages$.value, + { + moduleFactory, + name: pack.name, + component: pack.component, + _raw: pack + } + ]); + }); + } + + ngOnDestroy(): void { + this.packages$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/helium-manager/index.ts b/zeppelin-frontend/src/app/helium-manager/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/helium-manager/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/helium-manager/public-api.ts b/zeppelin-frontend/src/app/helium-manager/public-api.ts new file mode 100644 index 00000000000..f275905f5ef --- /dev/null +++ b/zeppelin-frontend/src/app/helium-manager/public-api.ts @@ -0,0 +1,2 @@ +export * from './helium-manager.service'; +export * from './helium-manager.module'; diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html new file mode 100644 index 00000000000..05e514f6511 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html @@ -0,0 +1,146 @@ +
+ + + ID + + + + + + + + URL + + + + + + + + + + + + + + + + + Snapshot + + + + + + + + Username + + + + + + + + Password + + + + + + + + + Protocol + + + + + + + + + + + Host + + + + + + + + Port + + + + + + + + Login + + + + + + + + Password + + + + + +
+ + diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less new file mode 100644 index 00000000000..bd1eeeee9e1 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less @@ -0,0 +1,7 @@ +@import 'theme-mixin'; + +.themeMixin({ + nz-form-item { + margin-bottom: 5px; + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts new file mode 100644 index 00000000000..a495195c733 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts @@ -0,0 +1,66 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { takeUntil } from 'rxjs/operators'; + +import { NzModalRef } from 'ng-zorro-antd'; + +import { DestroyHookComponent } from '@zeppelin/core'; +import { CreateInterpreterRepositoryForm } from '@zeppelin/interfaces'; +import { InterpreterService } from '@zeppelin/services'; + +@Component({ + selector: 'zeppelin-interpreter-create-repository-modal', + templateUrl: './create-repository-modal.component.html', + styleUrls: ['./create-repository-modal.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InterpreterCreateRepositoryModalComponent extends DestroyHookComponent implements OnInit { + validateForm: FormGroup; + submitting = false; + urlProtocol = 'http://'; + + handleCancel() { + this.nzModalRef.close(); + } + + handleSubmit() { + const data = this.validateForm.getRawValue() as CreateInterpreterRepositoryForm; + // set url protocol + data.url = `${this.urlProtocol}${data.url}`; + // reset proxy port + const proxyPort = Number.parseInt(data.proxyPort, 10); + data.proxyPort = Number.isNaN(proxyPort) ? null : `${proxyPort}`; + this.interpreterService + .addRepository(data) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.nzModalRef.close('Done'); + }); + } + + constructor( + private formBuilder: FormBuilder, + private nzModalRef: NzModalRef, + private interpreterService: InterpreterService + ) { + super(); + } + + ngOnInit() { + this.validateForm = this.formBuilder.group({ + id: ['', [Validators.required]], + url: ['', [Validators.required]], + snapshot: [false, [Validators.required]], + username: '', + password: '', + proxyProtocol: 'HTTP', + proxyHost: '', + proxyPort: [ + null, + [Validators.pattern('^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$')] + ], + proxyLogin: '', + proxyPassword: '' + }); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts new file mode 100644 index 00000000000..1f78078c05b --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { InterpreterComponent } from './interpreter.component'; + +const routes: Routes = [ + { + path: '', + component: InterpreterComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class InterpreterRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html new file mode 100644 index 00000000000..bf1400f1688 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html @@ -0,0 +1,54 @@ + + + + + + + + +
+ +

Repositories

+

Available repository lists. These repositories are used to resolve external dependencies of interpreter.

+ + {{repo.id}} + + + + +
+
+
+ + + Create + + + + + +
diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.less b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.less new file mode 100644 index 00000000000..2a53b3180b9 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.less @@ -0,0 +1,39 @@ +@import 'theme-mixin'; + +.themeMixin({ + .search-input { + width: 256px; + } + + @media (max-width:959px){ + .search-input { + width: 100%; + } + } + + nz-tag.repo-item { + border-color: transparent; + background: @primary-6; + color: @text-color-inverse; + ::ng-deep i.anticon-close { + color: @text-color-inverse; + &:hover { + color: @icon-color-hover; + } + } + } + + .editable-tag { + background: @white; + border-style: dashed; + } + + .content { + padding: @card-padding-base / 2; + + .create-interpreter { + text-align: center; + margin-bottom: 10px; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.ts new file mode 100644 index 00000000000..0cd041f03a1 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.ts @@ -0,0 +1,174 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { collapseMotion, NzMessageService, NzModalService } from 'ng-zorro-antd'; + +import { Interpreter, InterpreterPropertyTypes, InterpreterRepository } from '@zeppelin/interfaces'; +import { InterpreterService } from '@zeppelin/services'; + +import { InterpreterCreateRepositoryModalComponent } from './create-repository-modal/create-repository-modal.component'; + +@Component({ + selector: 'zeppelin-interpreter', + templateUrl: './interpreter.component.html', + styleUrls: ['./interpreter.component.less'], + animations: [collapseMotion], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InterpreterComponent implements OnInit, OnDestroy { + searchInterpreter = ''; + search$ = new Subject(); + showRepository = false; + showCreateSetting = false; + propertyTypes: InterpreterPropertyTypes[] = []; + interpreterSettings: Interpreter[] = []; + repositories: InterpreterRepository[] = []; + availableInterpreters: Interpreter[] = []; + filteredInterpreterSettings: Interpreter[] = []; + + onSearchChange(value: string) { + this.search$.next(value); + } + + filterInterpreters(value: string) { + this.filteredInterpreterSettings = this.interpreterSettings.filter(e => e.name.search(value) !== -1); + this.cdr.markForCheck(); + } + + triggerRepository(): void { + this.showRepository = !this.showRepository; + this.cdr.markForCheck(); + } + + removeRepository(repo: InterpreterRepository): void { + this.nzModalService.confirm({ + nzTitle: repo.id, + nzContent: 'Do you want to delete this repository?', + nzOnOk: () => { + this.interpreterService.removeRepository(repo.id).subscribe(() => { + this.repositories = this.repositories.filter(e => e.id !== repo.id); + this.cdr.markForCheck(); + }); + } + }); + } + + addInterpreterSetting(data: Interpreter): void { + this.interpreterService.addInterpreterSetting(data).subscribe(res => { + this.interpreterSettings.push(res); + this.showCreateSetting = false; + this.filterInterpreters(this.searchInterpreter); + this.cdr.markForCheck(); + }); + } + + updateInterpreter(data: Interpreter): void { + this.interpreterService.updateInterpreter(data).subscribe(res => { + const current = this.interpreterSettings.find(e => e.name === res.name); + if (current) { + current.status = res.status; + current.errorReason = res.errorReason; + current.option = res.option; + current.properties = res.properties; + current.dependencies = res.dependencies; + } + this.filterInterpreters(this.searchInterpreter); + this.cdr.markForCheck(); + }); + } + + removeInterpreterSetting(settingId: string): void { + this.nzModalService.confirm({ + nzTitle: 'Remove Interpreter', + nzContent: 'Do you want to delete this interpreter setting?', + nzOnOk: () => { + this.interpreterService.removeInterpreterSetting(settingId).subscribe(() => { + const index = this.interpreterSettings.findIndex(e => e.name === settingId); + this.interpreterSettings.splice(index, 1); + this.filterInterpreters(this.searchInterpreter); + this.cdr.markForCheck(); + }); + } + }); + } + + restartInterpreterSetting(settingId: string): void { + this.nzModalService.confirm({ + nzTitle: 'Restart Interpreter', + nzContent: 'Do you want to restart this interpreter?', + nzOnOk: () => { + this.interpreterService.restartInterpreterSetting(settingId).subscribe(() => { + this.nzMessageService.info('Interpreter stopped. Will be lazily started on next run.'); + }); + } + }); + } + + createRepository(): void { + const modalRef = this.nzModalService.create({ + nzTitle: 'Add New Repository', + nzContent: InterpreterCreateRepositoryModalComponent, + nzFooter: null, + nzWidth: '600px' + }); + modalRef.afterClose.subscribe(data => { + if (data === 'Done') { + this.getRepositories(); + } + }); + } + + getPropertyTypes(): void { + this.interpreterService.getAvailableInterpreterPropertyTypes().subscribe(data => { + this.propertyTypes = data; + this.cdr.markForCheck(); + }); + } + + getInterpreterSettings(): void { + this.interpreterService.getInterpretersSetting().subscribe(data => { + this.interpreterSettings = data; + this.filteredInterpreterSettings = data; + this.cdr.markForCheck(); + }); + } + + getAvailableInterpreters(): void { + this.interpreterService.getAvailableInterpreters().subscribe(data => { + this.availableInterpreters = Object.keys(data) + .sort() + .map(key => data[key]); + this.cdr.markForCheck(); + }); + } + + getRepositories(): void { + this.interpreterService.getRepositories().subscribe(data => { + this.repositories = data; + this.cdr.markForCheck(); + }); + } + + constructor( + private interpreterService: InterpreterService, + private cdr: ChangeDetectorRef, + private nzModalService: NzModalService, + private nzMessageService: NzMessageService + ) {} + + ngOnInit() { + this.getPropertyTypes(); + this.getInterpreterSettings(); + this.getAvailableInterpreters(); + this.getRepositories(); + + this.search$.pipe(debounceTime(150)).subscribe(value => this.filterInterpreters(value)); + } + + ngOnDestroy(): void { + this.search$.next(); + this.search$.complete(); + this.search$ = null; + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.module.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.module.ts new file mode 100644 index 00000000000..c3c2632457f --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.module.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + NzAlertModule, + NzBadgeModule, + NzButtonModule, + NzCardModule, + NzCheckboxModule, + NzDividerModule, + NzDropDownModule, + NzFormModule, + NzIconModule, + NzInputModule, + NzMessageModule, + NzModalModule, + NzRadioModule, + NzSelectModule, + NzSwitchModule, + NzTableModule, + NzTagModule, + NzToolTipModule +} from 'ng-zorro-antd'; + +import { ShareModule } from '@zeppelin/share'; + +import { InterpreterCreateRepositoryModalComponent } from './create-repository-modal/create-repository-modal.component'; +import { InterpreterRoutingModule } from './interpreter-routing.module'; +import { InterpreterComponent } from './interpreter.component'; +import { InterpreterItemComponent } from './item/item.component'; + +@NgModule({ + declarations: [InterpreterComponent, InterpreterCreateRepositoryModalComponent, InterpreterItemComponent], + entryComponents: [InterpreterCreateRepositoryModalComponent], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + InterpreterRoutingModule, + ShareModule, + NzFormModule, + NzSelectModule, + NzSwitchModule, + NzToolTipModule, + NzCheckboxModule, + NzRadioModule, + NzBadgeModule, + NzButtonModule, + NzModalModule, + NzInputModule, + NzDividerModule, + NzTagModule, + NzCardModule, + NzDropDownModule, + NzIconModule, + NzTableModule, + NzMessageModule, + NzAlertModule + ] +}) +export class InterpreterModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.html b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.html new file mode 100644 index 00000000000..e9b09801bde --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.html @@ -0,0 +1,435 @@ + + +
+

+ {{ interpreter.name }} + + , %{{ interpreter.name }}.{{ item.name }} + %{{ interpreter.name }} + + + + + + +

+
+
+ + + + + +
+ + + +
+
+ + +

Create new interpreter

+
+ + Interpreter Name + + + + {{ formGroup.get('name')?.errors?.message }} + + + + + Interpreter group + + + + + + +
+
+

Option

+

+ + + + The interpreter will be instantiated + + {{ interpreterRunningOption }} + + + + +

+ + in + + + {{ optionFormGroup.get('perUser').value }} + + + + {{ optionFormGroup.get('perNote').value }} + + + + + + + + {{ optionFormGroup.get('perUser').value }} + + + + {{ optionFormGroup.get('perNote').value }} + + + + + + + process + + and Per Note in + + {{ optionFormGroup.get('perNote').value }} + + + + + + process + + . + + + + + + +

+
+ + + + + + + + + + + + + + + Host + + + + + + Port + + + + + + + + + + + + + + + Owners + + + + + + Enter comma separated users and groups in the fields. Empty field (*) implies anyone can run this + interpreter. + + + + +
+ + +

Properties

+
+ + + + Name + Value + Description + Action + + + + + {{ control.get('key').value }} + + + + + + + + + + + + ****** + + {{control.get('value').value}} + + {{ control.get('value').value }} + + + + {{ control.get('description').value }} + + + + + + + + + +
+ + + + + + + + + + + +
+ + N/A + + + + + + +
+
+
+ + +

Dependencies

+
+ + + + Artifact + Exclude + Action + + + + + + + + + + + + + + + + + {{ control.get('groupArtifactVersion').value }} + {{ control.get('exclusions').value }} + + + + + + + + + + + + + + + +
+
+ + +
diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.less b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.less new file mode 100644 index 00000000000..4fa03bd3143 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.less @@ -0,0 +1,93 @@ +@import 'theme-mixin'; + +.themeMixin({ + display: block; + margin-bottom: @card-padding-base; + position: relative; + + ::ng-deep .interpreter-item { + &.edit { + background: @orange-1; + } + .ant-card-body { + margin-top: -@card-padding-base; + } + } + + .error-alter { + margin-top: @card-padding-base; + } + + .interpreter-status { + margin-left: 5px; + } + + .card-title { + display: inline-block; + h3 { + color: @primary-color; + margin-bottom: 0; + font-size: 1.4em; + .interpreter-group-item { + font-size: 60%; + font-weight: 400; + color: @text-color-secondary; + } + } + } + + h3.form-title { + margin: @card-padding-base 0; + } + + nz-form-item { + margin-bottom: 5px; + } + + form td textarea.ant-input { + margin-bottom: 0; + resize: none; + } + + .interpreter-form, .option-form { + input, nz-select { + width: 256px; + } + @media (max-width:959px){ + input, nz-select { + width: 100%; + } + } + } + + .edit-properties-value { + display: flex; + align-items: center; + + .value-input { + flex: 1; + } + + .type-selector { + width: 150px; + margin-left: 5px; + } + } + + .extra-wrap { + button + button { + margin-bottom: 0; + margin-left: 8px; + } + } + + .item-footer { + padding: 24px 0px 12px 0px; + text-align: right; + button + button { + margin-bottom: 0; + margin-left: 8px; + } + } + +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.ts new file mode 100644 index 00000000000..f89d7c906dc --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/item/item.component.ts @@ -0,0 +1,393 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { DestroyHookComponent } from '@zeppelin/core'; +import { Interpreter } from '@zeppelin/interfaces'; +import { InterpreterService, SecurityService, TicketService } from '@zeppelin/services'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { debounceTime, filter, map, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { InterpreterComponent } from '../interpreter.component'; + +@Component({ + selector: 'zeppelin-interpreter-item', + templateUrl: './item.component.html', + styleUrls: ['./item.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InterpreterItemComponent extends DestroyHookComponent implements OnInit, OnDestroy { + @Input() mode: 'create' | 'view' | 'edit' = 'view'; + @Input() interpreter: Interpreter; + + formGroup: FormGroup; + optionFormGroup: FormGroup; + editingPropertiesFormGroup: FormGroup; + editingDependenceFormGroup: FormGroup; + propertiesFormArray: FormArray; + dependenciesFormArray: FormArray; + userList$: Observable; + userSearchChange$ = new BehaviorSubject(''); + runningOptionMap = { + sharedModeName: 'shared', + globallyModeName: 'Globally', + perNoteModeName: 'Per Note', + perUserModeName: 'Per User' + }; + + sessionOptionMap = { + isolated: 'isolated', + scoped: 'scoped', + shared: 'shared' + }; + + interpreterRunningOption = 'Globally'; + + switchToEditMode(): void { + this.setupEditableForm(); + this.formGroup.enable(); + this.mode = 'edit'; + this.cdr.markForCheck(); + } + + handleRestart() { + this.parent.restartInterpreterSetting(this.interpreter.name); + } + + handleRemove() { + this.parent.removeInterpreterSetting(this.interpreter.name); + } + + handleSave() { + const formData = this.formGroup.getRawValue(); + const properties = {}; + + formData.properties + .sort(e => e.key) + .forEach(e => { + const { key, value, type } = e; + properties[key] = { + value, + type, + name: key + }; + }); + formData.properties = properties; + + formData.dependencies.forEach(e => { + e.exclusions = e.exclusions.split(',').filter(s => s !== ''); + }); + + if (this.mode === 'create') { + this.parent.addInterpreterSetting(formData); + } else { + this.parent.updateInterpreter(formData); + this.mode = 'view'; + } + } + + handleCancel() { + if (this.mode === 'create') { + this.parent.showCreateSetting = false; + } else { + this.mode = 'view'; + this.buildForm(); + this.formGroup.disable(); + } + } + + interpretersTrackFn(_: number, item: Interpreter) { + return item.name; + } + + onUserSearch(value: string): void { + this.userSearchChange$.next(value); + } + + removeProperty(index: number): void { + this.propertiesFormArray.removeAt(index); + this.cdr.markForCheck(); + } + + removeDependence(index: number): void { + this.dependenciesFormArray.removeAt(index); + this.cdr.markForCheck(); + } + + onTypeChange(type: string) { + let valueSet: string | boolean | number; + switch (type) { + case 'number': + valueSet = 0; + break; + case 'checkbox': + valueSet = false; + break; + default: + valueSet = ''; + } + this.editingPropertiesFormGroup.get('value').setValue(valueSet); + } + + addDependence(): void { + this.editingDependenceFormGroup.updateValueAndValidity(); + if (this.editingDependenceFormGroup.valid) { + const data = this.editingDependenceFormGroup.getRawValue(); + const current = this.dependenciesFormArray.controls.find( + control => control.get('groupArtifactVersion').value === data.groupArtifactVersion + ); + if (current) { + current.get('exclusions').setValue(data.exclusions); + } else { + this.dependenciesFormArray.push( + this.formBuilder.group({ + groupArtifactVersion: [data.groupArtifactVersion, [Validators.required]], + exclusions: data.exclusions + }) + ); + } + this.editingDependenceFormGroup.reset({ + exclusions: '', + groupArtifactVersion: '' + }); + } + } + + addProperties(): void { + this.editingPropertiesFormGroup.updateValueAndValidity(); + if (this.editingPropertiesFormGroup.valid) { + const data = this.editingPropertiesFormGroup.getRawValue(); + + const current = this.propertiesFormArray.controls.find(control => control.get('key').value === data.key); + if (current) { + current.get('value').setValue(data.value); + current.get('type').setValue(data.type); + } else { + this.propertiesFormArray.push( + this.formBuilder.group({ + key: [data.key, [Validators.required]], + value: data.value || '', + description: null, + type: data.type + }) + ); + } + this.editingPropertiesFormGroup.reset({ + key: '', + value: '', + description: null, + type: 'string' + }); + } + } + + setInterpreterRunningOption(perNote: string, perUser: string) { + const { sharedModeName, globallyModeName, perNoteModeName, perUserModeName } = this.runningOptionMap; + + this.optionFormGroup.get('perNote').setValue(perNote); + this.optionFormGroup.get('perUser').setValue(perUser); + + // Globally == shared_perNote + shared_perUser + if (perNote === sharedModeName && perUser === sharedModeName) { + this.interpreterRunningOption = globallyModeName; + return; + } + + const ticket = this.ticketService.originTicket; + + if (ticket.ticket === 'anonymous' && ticket.roles === '[]') { + if (perNote !== undefined && typeof perNote === 'string' && perNote !== '') { + this.interpreterRunningOption = perNoteModeName; + return; + } + } else if (ticket.ticket !== 'anonymous') { + if (perNote !== undefined && typeof perNote === 'string' && perNote !== '') { + if (perUser !== undefined && typeof perUser === 'string' && perUser !== '') { + this.interpreterRunningOption = perUserModeName; + return; + } + this.interpreterRunningOption = perNoteModeName; + return; + } + } + + this.optionFormGroup.get('perNote').setValue(sharedModeName); + this.optionFormGroup.get('perUser').setValue(sharedModeName); + this.interpreterRunningOption = globallyModeName; + } + + setPerNoteOrUserOption(type: 'perNote' | 'perUser', value: string) { + this.optionFormGroup.get(type).setValue(value); + switch (value) { + case this.sessionOptionMap.isolated: + this.optionFormGroup.get('session').setValue(false); + this.optionFormGroup.get('process').setValue(true); + break; + case this.sessionOptionMap.scoped: + this.optionFormGroup.get('session').setValue(true); + this.optionFormGroup.get('process').setValue(false); + break; + case this.sessionOptionMap.shared: + this.optionFormGroup.get('session').setValue(false); + this.optionFormGroup.get('process').setValue(false); + break; + } + } + + nameValidator(control: AbstractControl): ValidationErrors | null { + if (this.mode !== 'create') { + return null; + } + const name = (control.value as string).trim(); + const exist = this.parent.interpreterSettings.find(e => e.name === name); + if (exist) { + return { exist: true, message: `Name '${name}' already exists` }; + } else { + return null; + } + } + + buildForm(): void { + let name = ''; + let group = ''; + this.optionFormGroup = this.formBuilder.group({ + isExistingProcess: false, + isUserImpersonate: false, + owners: [[]], + perNote: '', + perUser: '', + port: [ + null, + [Validators.pattern('^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$')] + ], + host: '', + remote: true, + setPermission: false, + session: false, + process: false + }); + + this.propertiesFormArray = this.formBuilder.array([]); + this.dependenciesFormArray = this.formBuilder.array([]); + + if (this.mode === 'view' && this.interpreter) { + name = this.interpreter.name; + group = this.interpreter.group; + + // set option fields + this.optionFormGroup.reset({ + ...this.interpreter.option, + port: this.interpreter.option.port === -1 ? null : this.interpreter.option.port + }); + + // set dependencies fields + this.interpreter.dependencies.forEach(e => { + this.dependenciesFormArray.push( + this.formBuilder.group({ + exclusions: [e.exclusions.join(',')], + groupArtifactVersion: [e.groupArtifactVersion, [Validators.required]] + }) + ); + }); + + // set properties fields + Object.keys(this.interpreter.properties).forEach(key => { + const item = this.interpreter.properties[key]; + this.propertiesFormArray.push( + this.formBuilder.group({ + key: key, + value: item.value, + description: null, + type: item.type + }) + ); + }); + } + + this.formGroup = this.formBuilder.group({ + name: [name, [Validators.required, c => this.nameValidator(c)]], + group: [group, [Validators.required]], + option: this.optionFormGroup, + properties: this.propertiesFormArray, + dependencies: this.dependenciesFormArray + }); + } + + setupEditableForm(): void { + this.userList$ = this.userSearchChange$.pipe( + debounceTime(500), + filter(value => !!value), + switchMap(value => this.securityService.searchUsers(value)), + map(data => data.users), + tap(() => { + this.cdr.markForCheck(); + }) + ); + + this.editingPropertiesFormGroup = this.formBuilder.group({ + key: ['', [Validators.required]], + value: '', + description: null, + type: 'string' + }); + + this.editingDependenceFormGroup = this.formBuilder.group({ + groupArtifactVersion: ['', [Validators.required]], + exclusions: [''] + }); + + if (this.mode === 'create') { + this.formGroup + .get('group') + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe(value => { + // remove all controls + while (this.propertiesFormArray.length) { + this.propertiesFormArray.removeAt(0); + } + + const interpreters = this.parent.availableInterpreters.filter(e => e.group === value); + interpreters.forEach(interpreter => { + Object.keys(interpreter.properties).forEach(key => { + this.propertiesFormArray.push( + this.formBuilder.group({ + key: [key, [Validators.required]], + value: interpreter.properties[key].defaultValue, + description: interpreter.properties[key].description, + type: interpreter.properties[key].type + }) + ); + }); + }); + this.cdr.markForCheck(); + }); + } + } + + constructor( + public parent: InterpreterComponent, + public ticketService: TicketService, + private securityService: SecurityService, + private interpreterService: InterpreterService, + private formBuilder: FormBuilder, + private cdr: ChangeDetectorRef + ) { + super(); + } + + ngOnInit() { + this.buildForm(); + const option = this.optionFormGroup.getRawValue(); + this.setInterpreterRunningOption(option.perNote, option.perUser); + + if (this.mode !== 'view') { + this.setupEditableForm(); + this.formGroup.enable(); + } else { + this.formGroup.disable(); + } + } + + ngOnDestroy(): void { + this.userSearchChange$.complete(); + this.userSearchChange$ = null; + super.ngOnDestroy(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager-routing.module.ts new file mode 100644 index 00000000000..2e599d0c1f2 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { JobManagerComponent } from './job-manager.component'; + +const routes: Routes = [ + { + path: '', + component: JobManagerComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class JobManagerRoutingModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html new file mode 100644 index 00000000000..f85373ebe04 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html @@ -0,0 +1,62 @@ + +
+ + + + + + + + + + + + Interpreter + + + + + + + + + Sort + + + + + + + + Total + + {{ filteredJobs.length }} + + + + + + + + +
+
+
+ + + + + + + + + + +
diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.less b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.less new file mode 100644 index 00000000000..46bb58e6439 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.less @@ -0,0 +1,38 @@ +@import 'theme-mixin'; + +.themeMixin({ + .header-form { + .interpreter-select { + min-width: 100px; + } + .sort-select { + min-width: 155px; + } + } + + .content { + padding: @card-padding-base / 2; + + ::ng-deep .ant-skeleton-paragraph { + margin-bottom: 0; + } + + nz-empty { + margin-top: 76px; + } + } + + @media (max-width: 1230px) { + .status-legend { + display: none; + } + } + .status-legend { + float: right; + margin-right: 0; + zeppelin-job-manager-job-status { + margin-left: 10px; + display: inline-block; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts new file mode 100644 index 00000000000..2143109612d --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts @@ -0,0 +1,124 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +import { NzModalService } from 'ng-zorro-antd'; + +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { JobsItem, JobStatus, ListNoteJobs, ListUpdateNoteJobs, OP } from '@zeppelin/sdk'; +import { JobManagerService, MessageService } from '@zeppelin/services'; + +enum JobDateSortKeys { + RECENTLY_UPDATED = 'Recently Update', + OLDEST_UPDATED = 'Oldest Updated' +} + +interface FilterForm { + noteName: string; + interpreter: string; + sortBy: string; +} + +@Component({ + selector: 'zeppelin-job-manager', + templateUrl: './job-manager.component.html', + styleUrls: ['./job-manager.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class JobManagerComponent extends MessageListenersManager implements OnInit, OnDestroy { + form: FormGroup; + jobStatusKeys = Object.keys(JobStatus).map(k => JobStatus[k]); + sortKeys = Object.keys(JobDateSortKeys).map(k => JobDateSortKeys[k]); + interpreters: string[] = []; + filteredJobs: JobsItem[] = []; + jobs: JobsItem[] = []; + loading = true; + + @MessageListener(OP.LIST_NOTE_JOBS) + setJobs(data: ListNoteJobs) { + this.jobs = data.noteJobs.jobs.filter(j => typeof j.interpreter !== 'undefined'); + const interpreters = this.jobs.map(job => job.interpreter); + this.interpreters = Array.from(new Set(interpreters)); + this.loading = false; + this.filterJobs(); + } + + @MessageListener(OP.LIST_UPDATE_NOTE_JOBS) + updateJobs(data: ListUpdateNoteJobs) { + data.noteRunningJobs.jobs.forEach(updateJob => { + const currentJobIndex = this.jobs.findIndex(job => job.noteId === updateJob.noteId); + if (currentJobIndex === -1) { + this.jobs.push(updateJob); + } else { + if (updateJob.isRemoved) { + this.jobs.splice(currentJobIndex, 1); + } else { + this.jobs[currentJobIndex] = updateJob; + } + } + }); + this.filterJobs(); + } + + filterJobs() { + const filterData = this.form.getRawValue() as FilterForm; + const isSortByAsc = filterData.sortBy === JobDateSortKeys.OLDEST_UPDATED; + this.filteredJobs = this.jobs + .filter(job => { + const escapedString = filterData.noteName.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$&'); + const noteNameReg = new RegExp(escapedString, 'gi'); + return ( + (filterData.interpreter === '*' || job.interpreter === filterData.interpreter) && + job.noteName.match(noteNameReg) + ); + }) + .sort((x, y) => (isSortByAsc ? x.unixTimeLastRun - y.unixTimeLastRun : y.unixTimeLastRun - x.unixTimeLastRun)); + this.cdr.markForCheck(); + } + + onStart(noteId: string): void { + this.nzModalService.confirm({ + nzTitle: 'Job Dialog', + nzContent: 'Run all paragraphs?', + nzOnOk: () => { + this.jobManagerService.startJob(noteId).subscribe(); + } + }); + } + + onStop(noteId: string): void { + this.nzModalService.confirm({ + nzTitle: 'Job Dialog', + nzContent: 'Stop all paragraphs?', + nzOnOk: () => { + this.jobManagerService.stopJob(noteId).subscribe(); + } + }); + } + + constructor( + public messageService: MessageService, + private jobManagerService: JobManagerService, + private fb: FormBuilder, + private cdr: ChangeDetectorRef, + private nzModalService: NzModalService + ) { + super(messageService); + } + + ngOnInit() { + this.form = this.fb.group({ + noteName: [''], + interpreter: ['*'], + sortBy: [JobDateSortKeys.RECENTLY_UPDATED] + }); + + this.form.valueChanges.subscribe(() => this.filterJobs()); + + this.messageService.listNoteJobs(); + } + + ngOnDestroy(): void { + this.messageService.unsubscribeUpdateNoteJobs(); + super.ngOnDestroy(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts new file mode 100644 index 00000000000..b89a5bfc244 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts @@ -0,0 +1,59 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { IconDefinition } from '@ant-design/icons-angular'; +import { ClockCircleOutline, FileOutline, FileUnknownOutline, SearchOutline } from '@ant-design/icons-angular/icons'; +import { + NzBadgeModule, + NzCardModule, + NzDividerModule, + NzEmptyModule, + NzFormModule, + NzGridModule, + NzIconModule, + NzInputModule, + NzModalModule, + NzProgressModule, + NzSelectModule, + NzSkeletonModule, + NzToolTipModule, + NZ_ICONS +} from 'ng-zorro-antd'; + +import { ShareModule } from '@zeppelin/share'; + +import { JobManagerRoutingModule } from './job-manager-routing.module'; +import { JobManagerComponent } from './job-manager.component'; +import { JobManagerJobStatusComponent } from './job-status/job-status.component'; +import { JobManagerJobComponent } from './job/job.component'; + +const icons: IconDefinition[] = [SearchOutline, FileOutline, FileUnknownOutline, ClockCircleOutline]; + +@NgModule({ + declarations: [JobManagerComponent, JobManagerJobComponent, JobManagerJobStatusComponent], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + ShareModule, + NzIconModule, + NzInputModule, + NzBadgeModule, + NzGridModule, + NzModalModule, + RouterModule, + NzSelectModule, + NzInputModule, + NzFormModule, + JobManagerRoutingModule, + NzDividerModule, + NzCardModule, + NzToolTipModule, + NzProgressModule, + NzSkeletonModule, + NzEmptyModule + ], + providers: [{ provide: NZ_ICONS, useValue: icons }] +}) +export class JobManagerModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.html b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.html new file mode 100644 index 00000000000..dda49ea5938 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.html @@ -0,0 +1,5 @@ + + diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.less b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.less new file mode 100644 index 00000000000..b76edf6d632 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.less @@ -0,0 +1,11 @@ +@import 'theme-mixin'; + +.themeMixin({ + display: inline-block; + nz-badge.ready { + ::ng-deep .ant-badge-status-success { + background: fade(@success-color, 20%); + border: 1px solid @success-color; + } + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.ts new file mode 100644 index 00000000000..010dcea7d77 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-status/job-status.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { JobStatus } from '@zeppelin/sdk'; + +@Component({ + selector: 'zeppelin-job-manager-job-status', + templateUrl: './job-status.component.html', + styleUrls: ['./job-status.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class JobManagerJobStatusComponent { + @Input() status: JobStatus; + @Input() showText = false; + jobStatus = JobStatus; + statusMap = { + [JobStatus.READY]: 'success', + [JobStatus.FINISHED]: 'success', + [JobStatus.ABORT]: 'warning', + [JobStatus.ERROR]: 'error', + [JobStatus.PENDING]: 'default', + [JobStatus.RUNNING]: 'processing' + }; + + constructor() {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html new file mode 100644 index 00000000000..d69dd173859 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html @@ -0,0 +1,46 @@ + +
+ + + {{note.noteName}} - + + {{note.interpreter || 'interpreter is not set'}} + + + {{relativeTime}} + {{note.isRunningJob ? 'RUNNING' : 'READY'}} + {{progress | percent: '1.0-0'}} + + +
+ +
+ + diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.less b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.less new file mode 100644 index 00000000000..c099005a000 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.less @@ -0,0 +1,58 @@ +@import 'theme-mixin'; + +.themeMixin({ + display: block; + margin-bottom: 10px; + position: relative; + + ::ng-deep .job-item .ant-card-body { + padding: @card-padding-base / 2; + } + + .job-title { + margin-bottom: 10px; + } + + .note-icon { + margin-right: 5px; + } + + .right-tools { + display: inline-block; + float: right; + color: @text-color-secondary; + & > * { + margin-left: 5px; + } + .job-control-btn { + cursor: pointer; + color: @processing-color; + &.running { + color: @error-color; + } + } + } + + .interpreter { + color: @text-color; + &.unset { + color: @text-color-secondary; + } + } + + zeppelin-job-manager-job-status { + ::ng-deep .ant-badge-status-text { + margin-left: 0; + } + margin: 0 4px; + } + + .footer-progress { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + margin-bottom: -5px; + padding: 0 1px; + } +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts new file mode 100644 index 00000000000..c257b8327f4 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts @@ -0,0 +1,69 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; + +import * as distanceInWords from 'date-fns/distance_in_words'; + +import { JobsItem, JobStatus } from '@zeppelin/sdk'; + +@Component({ + selector: 'zeppelin-job-manager-job', + templateUrl: './job.component.html', + styleUrls: ['./job.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class JobManagerJobComponent implements OnInit, OnChanges { + @Input() note: JobsItem; + @Output() readonly start = new EventEmitter(); + @Output() readonly stop = new EventEmitter(); + icon = 'file'; + relativeTime = ''; + progress = 0; + + setIcon(): void { + const noteType = this.note.noteType; + if (noteType === 'normal') { + this.icon = 'file'; + } else if (noteType === 'cron') { + this.icon = 'close-circle'; + } else { + this.icon = 'file-unknown'; + } + } + + setRelativeTime(): void { + this.relativeTime = distanceInWords(new Date(), new Date(this.note.unixTimeLastRun)); + } + + setProgress(): void { + const runningCount = this.note.paragraphs.filter( + paragraph => [JobStatus.FINISHED, JobStatus.RUNNING].indexOf(paragraph.status) !== -1 + ).length; + this.progress = runningCount / this.note.paragraphs.length; + } + + onStartClick(): void { + this.start.emit(this.note.noteId); + } + + onStopClick(): void { + this.stop.emit(this.note.noteId); + } + + constructor() {} + + ngOnInit() {} + + ngOnChanges(changes: SimpleChanges): void { + this.setIcon(); + this.setRelativeTime(); + this.setProgress(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts index 20a80638cf8..e67220f093c 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts @@ -22,6 +22,7 @@ import { NzSwitchModule, NzToolTipModule } from 'ng-zorro-antd'; +import { NzCodeEditorModule } from 'ng-zorro-antd/code-editor'; import { NzResizableModule } from 'ng-zorro-antd/resizable'; import { ShareModule } from '@zeppelin/share'; @@ -30,9 +31,11 @@ import { VisualizationModule } from 'src/app/visualizations/visualization.module import { NotebookAddParagraphComponent } from './add-paragraph/add-paragraph.component'; import { NotebookInterpreterBindingComponent } from './interpreter-binding/interpreter-binding.component'; import { NotebookParagraphControlComponent } from './paragraph/control/control.component'; +import { NotebookParagraphDynamicFormsComponent } from './paragraph/dynamic-forms/dynamic-forms.component'; import { NotebookParagraphFooterComponent } from './paragraph/footer/footer.component'; import { NotebookParagraphComponent } from './paragraph/paragraph.component'; import { NotebookParagraphProgressComponent } from './paragraph/progress/progress.component'; +import { NotebookParagraphResultComponent } from './paragraph/result/result.component'; import { NotebookPermissionsComponent } from './permissions/permissions.component'; import { NotebookRevisionsComparatorComponent } from './revisions-comparator/revisions-comparator.component'; @@ -50,9 +53,11 @@ import { NotebookShareModule } from './share/share.module'; NotebookRevisionsComparatorComponent, NotebookParagraphComponent, NotebookAddParagraphComponent, + NotebookParagraphResultComponent, NotebookParagraphProgressComponent, NotebookParagraphFooterComponent, NotebookParagraphControlComponent, + NotebookParagraphDynamicFormsComponent ], imports: [ CommonModule, @@ -80,7 +85,8 @@ import { NotebookShareModule } from './share/share.module'; NzGridModule, NzRadioModule, DragDropModule, - NzResizableModule + NzResizableModule, + NzCodeEditorModule ] }) export class NotebookModule {} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html new file mode 100644 index 00000000000..15f42415b48 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html @@ -0,0 +1,39 @@ +
+
+ + + + + + + + + + +
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less new file mode 100644 index 00000000000..04031bbc40d --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less @@ -0,0 +1,12 @@ +:host { + display: block; + .form-item { + margin-bottom: 24px; + nz-select { + width: 100%; + } + ::ng-deep .ant-checkbox-wrapper { + margin-left: 0; + } + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts new file mode 100644 index 00000000000..e04dea6c3bb --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts @@ -0,0 +1,108 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostListener, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { debounceTime, takeUntil } from 'rxjs/operators'; + +import { NzCheckBoxOptionInterface } from 'ng-zorro-antd'; + +import { DynamicForms, DynamicFormsItem, DynamicFormsType, DynamicFormParams } from '@zeppelin/sdk'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-dynamic-forms', + templateUrl: './dynamic-forms.component.html', + styleUrls: ['./dynamic-forms.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphDynamicFormsComponent implements OnInit, OnChanges, OnDestroy { + private destroy$ = new Subject(); + + @Input() formDefs: DynamicForms; + @Input() paramDefs: DynamicFormParams; + @Input() runOnChange = false; + @Input() disable = false; + @Output() readonly formChange = new EventEmitter(); + + formChange$ = new Subject(); + forms: DynamicFormsItem[] = []; + formType = DynamicFormsType; + checkboxGroups: { + [key: string]: NzCheckBoxOptionInterface[]; + } = {}; + + @HostListener('keydown.enter') + onEnter() { + if (!this.runOnChange) { + this.formChange.emit(); + } + } + + trackByNameFn(_index, form: DynamicFormsItem) { + return form.name; + } + + setForms() { + this.forms = Object.values(this.formDefs); + this.checkboxGroups = {}; + this.forms.forEach(e => { + if (!this.paramDefs[e.name]) { + this.paramDefs[e.name] = e.defaultValue; + } + if (e.type === DynamicFormsType.CheckBox) { + this.checkboxGroups[e.name] = e.options.map(opt => { + let checked = false; + if (this.paramDefs[e.name] && Array.isArray(this.paramDefs[e.name])) { + const param = this.paramDefs[e.name] as string[]; + checked = param.indexOf(opt.value) !== -1; + } + return { + checked, + label: opt.displayName || opt.value, + value: opt.value + }; + }); + } + }); + } + + checkboxChange(value: NzCheckBoxOptionInterface[], name) { + this.paramDefs[name] = value.filter(e => e.checked).map(e => e.value); + this.onFormChange(); + } + + onFormChange() { + if (this.runOnChange) { + this.formChange$.next(); + } + } + + constructor() {} + + ngOnInit() { + this.setForms(); + this.formChange$ + .pipe( + debounceTime(800), + takeUntil(this.destroy$) + ) + .subscribe(() => this.formChange.emit()); + } + + ngOnChanges(changes: SimpleChanges): void { + this.setForms(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html index df38ddeb04a..bf0246be645 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html @@ -41,20 +41,6 @@ (runAllBelowAndCurrent)="runAllBelowAndCurrent()" (cloneParagraph)="cloneParagraph()" (cancelParagraph)="cancelParagraph()"> - diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts index 08350b06095..0e2725fe1ea 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts @@ -9,7 +9,6 @@ import { OnInit, Output, QueryList, - ViewChild, ViewChildren } from '@angular/core'; import { Subject } from 'rxjs'; @@ -45,6 +44,7 @@ import { import { SpellResult } from '@zeppelin/spell/spell-result'; import { NzResizeEvent } from 'ng-zorro-antd/resizable'; +import { NotebookParagraphResultComponent } from './result/result.component'; @Component({ selector: 'zeppelin-notebook-paragraph', @@ -53,6 +53,9 @@ import { NzResizeEvent } from 'ng-zorro-antd/resizable'; changeDetection: ChangeDetectionStrategy.OnPush }) export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy { + @ViewChildren(NotebookParagraphResultComponent) notebookParagraphResultComponents: QueryList< + NotebookParagraphResultComponent + >; @Input() paragraph: ParagraphItem; @Input() note: Note['note']; @Input() looknfeel: string; @@ -547,6 +550,12 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen if (needCommit) { this.commitParagraph(); } + + if (updateResult) { + this.notebookParagraphResultComponents.forEach(comp => { + comp.setGraphConfig(); + }); + } } onSizeChange(resize: NzResizeEvent) { diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html new file mode 100644 index 00000000000..b8cfe836a01 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html @@ -0,0 +1,62 @@ +
+
+ + + +
+ + + + + + +
    +
  • CSV
  • +
  • TSV
  • +
+
+ + + Setting + + +
+
+ + + + + +
+
+
img
+
+
+ +
+
diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less new file mode 100644 index 00000000000..bf8758c1558 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less @@ -0,0 +1,59 @@ +@import "theme-mixin"; + +.themeMixin({ + display: block; + + .nz-resizable-preview { + left: -12px; + padding-right: 10px; + border: none; + &::before { + content: ' '; + display: block; + border: 1px dashed #d1d1d1; + width: 100%; + height: 100%; + } + } + + ::ng-deep { + .inner-html, .text-plain { + + overflow: auto; + + ol, ul, dl { + padding-left: 20px; + } + + img { + max-width: 100%; + height: auto; + width: auto; + } + } + } + + .text-plain { + font-size: 12px; + color: @text-color; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + } + .setting-bar { + display: flex; + margin: 10px 0; + + .export-dropdown { + margin: 0 20px; + } + + .setting-trigger { + line-height: 32px; + + i { + font-size: 12px; + } + } + + } + +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.ts new file mode 100644 index 00000000000..a0c3fc473f7 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.ts @@ -0,0 +1,348 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Injector, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { DomSanitizer, SafeHtml, SafeUrl } from '@angular/platform-browser'; +import { Subject, Subscription } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import * as Convert from 'ansi-to-html'; +import * as hljs from 'highlight.js'; +import { NzResizeEvent } from 'ng-zorro-antd/resizable'; +import { utils, writeFile, WorkSheet, WritingOptions } from 'xlsx'; + +import { + DatasetType, + GraphConfig, + ParagraphConfigResult, + ParagraphIResultsMsgItem, + VisualizationLineChart, + VisualizationMode, + VisualizationMultiBarChart, + VisualizationScatterChart, + VisualizationStackedAreaChart +} from '@zeppelin/sdk'; + +import { ZeppelinHeliumService } from '@zeppelin/helium'; +import { TableData, Visualization } from '@zeppelin/visualization'; + +import { HeliumManagerService } from '@zeppelin/helium-manager'; +import { DynamicTemplate, NgZService, RuntimeCompilerService } from '@zeppelin/services'; +import { AreaChartVisualization } from '@zeppelin/visualizations/area-chart/area-chart-visualization'; +import { BarChartVisualization } from '@zeppelin/visualizations/bar-chart/bar-chart-visualization'; +import { LineChartVisualization } from '@zeppelin/visualizations/line-chart/line-chart-visualization'; +import { PieChartVisualization } from '@zeppelin/visualizations/pie-chart/pie-chart-visualization'; +import { ScatterChartVisualization } from '@zeppelin/visualizations/scatter-chart/scatter-chart-visualization'; +import { TableVisualization } from '@zeppelin/visualizations/table/table-visualization'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-result', + templateUrl: './result.component.html', + styleUrls: ['./result.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphResultComponent implements OnInit, AfterViewInit, OnDestroy { + @Input() result: ParagraphIResultsMsgItem; + @Input() config: ParagraphConfigResult; + @Input() id: string; + @Input() currentCol = 12; + @Output() readonly configChange = new EventEmitter(); + @Output() readonly sizeChange = new EventEmitter(); + @ViewChild(CdkPortalOutlet, { static: false }) portalOutlet: CdkPortalOutlet; + + private destroy$ = new Subject(); + datasetType = DatasetType; + angularComponent: DynamicTemplate; + innerHTML: string | SafeHtml = ''; + plainText: string | SafeHtml = ''; + imgData: string | SafeUrl = ''; + tableData = new TableData(); + // tslint:disable-next-line:no-any + visualizations: any[] = [ + { + id: 'table', + name: 'Table', + icon: 'table', + Class: TableVisualization, + changeSubscription: null, + instance: undefined + }, + { + id: 'multiBarChart', + name: 'Bar Chart', + icon: 'bar-chart', + Class: BarChartVisualization, + changeSubscription: null, + instance: undefined + }, + { + id: 'pieChart', + name: 'Pie Chart', + icon: 'pie-chart', + Class: PieChartVisualization, + changeSubscription: null, + instance: undefined + }, + { + id: 'lineChart', + name: 'Line Chart', + icon: 'line-chart', + Class: LineChartVisualization, + changeSubscription: null, + instance: undefined + }, + { + id: 'stackedAreaChart', + name: 'Area Chart', + icon: 'area-chart', + Class: AreaChartVisualization, + changeSubscription: null, + instance: undefined + }, + { + id: 'scatterChart', + name: 'Scatter Chart', + icon: 'dot-chart', + Class: ScatterChartVisualization, + changeSubscription: null, + instance: undefined + } + ]; + + constructor( + private viewContainerRef: ViewContainerRef, + private cdr: ChangeDetectorRef, + private runtimeCompilerService: RuntimeCompilerService, + private sanitizer: DomSanitizer, + private injector: Injector, + private ngZService: NgZService, + private zeppelinHeliumService: ZeppelinHeliumService, + private heliumManagerService: HeliumManagerService + ) { + this.heliumManagerService + .packagesLoadChange() + .pipe(takeUntil(this.destroy$)) + .subscribe(packages => { + packages.forEach(pack => { + this.visualizations.push({ + id: pack._raw.id, + name: pack.name, + icon: pack._raw.icon, + Class: pack._raw.visualization, + componentFactoryResolver: pack.moduleFactory.create(this.injector).componentFactoryResolver, + changeSubscription: null, + instance: undefined + }); + }); + this.cdr.markForCheck(); + }); + } + + ngOnInit() { + this.ngZService + .contextChanged() + .pipe(takeUntil(this.destroy$)) + .subscribe(change => { + if (change.paragraphId === this.id) { + this.cdr.markForCheck(); + } + }); + } + + exportFile(type: 'csv' | 'tsv'): void { + if (this.tableData && this.tableData.rows) { + const wb = utils.book_new(); + let ws: WorkSheet; + ws = utils.json_to_sheet(this.tableData.rows); + utils.book_append_sheet(wb, ws, 'Sheet1'); + writeFile(wb, `export.${type}`, { + bookType: 'csv', + FS: type === 'tsv' ? '\t' : ',' + } as WritingOptions); + } + } + + switchMode(mode: VisualizationMode) { + this.config.graph.mode = mode; + this.renderGraph(); + this.configChange.emit(this.config); + } + + switchSetting() { + this.config.graph.optionOpen = !this.config.graph.optionOpen; + this.renderGraph(); + this.configChange.emit(this.config); + } + + updateResult(config: ParagraphConfigResult, result: ParagraphIResultsMsgItem) { + this.config = config; + this.result = result; + this.renderDefaultDisplay(); + } + + renderDefaultDisplay() { + switch (this.result.type) { + case DatasetType.TABLE: + this.renderGraph(); + break; + case DatasetType.TEXT: + this.renderText(); + break; + case DatasetType.HTML: + this.renderHTML(); + break; + case DatasetType.IMG: + this.renderImg(); + break; + case DatasetType.ANGULAR: + this.renderAngular(); + break; + } + this.cdr.markForCheck(); + } + + renderHTML(): void { + const div = document.createElement('div'); + div.innerHTML = this.result.data; + const codeEle: HTMLElement = div.querySelector('pre code'); + if (codeEle) { + hljs.highlightBlock(codeEle); + } + this.innerHTML = this.sanitizer.bypassSecurityTrustHtml(div.innerHTML); + } + + renderAngular(): void { + this.runtimeCompilerService.createAndCompileTemplate(this.id, this.result.data).then(data => { + this.angularComponent = data; + // this.angularComponent.moduleFactory + this.cdr.markForCheck(); + }); + } + + renderText(): void { + // tslint:disable-next-line:no-any + const convert: any = new Convert(); + this.plainText = this.sanitizer.bypassSecurityTrustHtml(convert.toHtml(this.result.data)); + } + + renderImg(): void { + this.imgData = this.sanitizer.bypassSecurityTrustUrl(`data:image/png;base64,${this.result.data}`); + } + + setGraphConfig() { + const visualizationItem = this.visualizations.find(v => v.id === this.config.graph.mode); + if (!visualizationItem || !visualizationItem.instance) { + return; + } + visualizationItem.instance.setConfig(this.config.graph); + } + + renderGraph() { + this.setDefaultConfig(); + let instance: Visualization; + const visualizationItem = this.visualizations.find(v => v.id === this.config.graph.mode); + if (!visualizationItem) { + return; + } + this.destroyVisualizations(this.config.graph.mode); + if (!visualizationItem.instance) { + // tslint:disable-next-line:no-any + instance = new visualizationItem.Class( + this.config.graph, + this.portalOutlet, + this.viewContainerRef, + visualizationItem.componentFactoryResolver + ); + visualizationItem.instance = instance; + visualizationItem.changeSubscription = instance.configChanged().subscribe(config => { + this.config.graph = config; + this.renderGraph(); + this.configChange.emit({ + graph: config + }); + }); + } else { + instance = visualizationItem.instance; + instance.setConfig(this.config.graph); + } + this.tableData.loadParagraphResult(this.result); + const transformation = instance.getTransformation(); + transformation.setConfig(this.config.graph); + transformation.setTableData(this.tableData); + const transformed = transformation.transform(this.tableData); + instance.render(transformed); + } + + destroyVisualizations(omit?: string) { + this.visualizations.forEach(v => { + if (v.id !== omit && v.instance) { + if (v.changeSubscription instanceof Subscription) { + v.changeSubscription.unsubscribe(); + v.changeSubscription = null; + } + if (typeof v.instance.destroy === 'function') { + v.instance.destroy(); + } + v.instance = undefined; + } + }); + } + + setDefaultConfig() { + if (!this.config || !this.config.graph) { + this.config = { graph: new GraphConfig() }; + } + if (!this.config.graph.setting) { + this.config.graph.setting = {}; + } + if (!this.config.graph.setting[this.config.graph.mode]) { + switch (this.config.graph.mode) { + case 'multiBarChart': + this.config.graph.setting[this.config.graph.mode] = new VisualizationMultiBarChart(); + break; + case 'stackedAreaChart': + this.config.graph.setting[this.config.graph.mode] = new VisualizationStackedAreaChart(); + break; + case 'lineChart': + this.config.graph.setting[this.config.graph.mode] = new VisualizationLineChart(); + break; + case 'scatterChart': + this.config.graph.setting[this.config.graph.mode] = new VisualizationScatterChart(); + break; + default: + break; + } + } + } + + onResize($event: NzResizeEvent) { + const { width, height, col } = $event; + if (this.result.type === DatasetType.TABLE) { + this.config.graph.height = height; + this.setGraphConfig(); + } + this.sizeChange.emit({ width, height, col }); + } + + ngAfterViewInit(): void { + this.renderDefaultDisplay(); + } + + ngOnDestroy(): void { + this.destroyVisualizations(); + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/zeppelin-frontend/src/app/share/math-jax/math-jax.directive.ts b/zeppelin-frontend/src/app/share/math-jax/math-jax.directive.ts new file mode 100644 index 00000000000..920569046b4 --- /dev/null +++ b/zeppelin-frontend/src/app/share/math-jax/math-jax.directive.ts @@ -0,0 +1,12 @@ +import { AfterViewChecked, Directive, ElementRef } from '@angular/core'; + +@Directive({ + selector: '[zeppelinMathJax]' +}) +export class MathJaxDirective implements AfterViewChecked { + constructor(private el: ElementRef) {} + + ngAfterViewChecked(): void { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, this.el.nativeElement]); + } +} diff --git a/zeppelin-frontend/src/app/share/resize-handle/index.ts b/zeppelin-frontend/src/app/share/resize-handle/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/share/resize-handle/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/share/resize-handle/public-api.ts b/zeppelin-frontend/src/app/share/resize-handle/public-api.ts new file mode 100644 index 00000000000..a527a9c23fa --- /dev/null +++ b/zeppelin-frontend/src/app/share/resize-handle/public-api.ts @@ -0,0 +1 @@ +export * from './resize-handle.component'; diff --git a/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.html b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.html new file mode 100644 index 00000000000..17c1c2329d0 --- /dev/null +++ b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.html @@ -0,0 +1,11 @@ + + + diff --git a/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.less b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.less new file mode 100644 index 00000000000..fb9acca4339 --- /dev/null +++ b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.less @@ -0,0 +1,11 @@ +:host { + width: 24px; + height: 24px; + position: absolute; + right: 1px; + bottom: 1px; + color: #595959; + transition: opacity ease-out .2s; + cursor: se-resize; + z-index: 9; +} diff --git a/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.ts b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.ts new file mode 100644 index 00000000000..07316df692c --- /dev/null +++ b/zeppelin-frontend/src/app/share/resize-handle/resize-handle.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'zeppelin-resize-handle', + templateUrl: './resize-handle.component.html', + styleUrls: ['./resize-handle.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + role: 'resize-handle' + } +}) +export class ResizeHandleComponent { + constructor() {} +} diff --git a/zeppelin-frontend/src/app/share/share.module.ts b/zeppelin-frontend/src/app/share/share.module.ts index 8f1a0bf26d0..3353aa65500 100644 --- a/zeppelin-frontend/src/app/share/share.module.ts +++ b/zeppelin-frontend/src/app/share/share.module.ts @@ -30,6 +30,7 @@ import { import { AboutZeppelinComponent } from '@zeppelin/share/about-zeppelin/about-zeppelin.component'; import { FolderRenameComponent } from '@zeppelin/share/folder-rename/folder-rename.component'; import { HeaderComponent } from '@zeppelin/share/header/header.component'; +import { MathJaxDirective } from '@zeppelin/share/math-jax/math-jax.directive'; import { NodeListComponent } from '@zeppelin/share/node-list/node-list.component'; import { NoteCreateComponent } from '@zeppelin/share/note-create/note-create.component'; import { NoteImportComponent } from '@zeppelin/share/note-import/note-import.component'; @@ -38,6 +39,7 @@ import { PageHeaderComponent } from '@zeppelin/share/page-header/page-header.com import { HumanizeBytesPipe } from '@zeppelin/share/pipes'; import { RunScriptsDirective } from '@zeppelin/share/run-scripts/run-scripts.directive'; import { SpinComponent } from '@zeppelin/share/spin/spin.component'; +import { ResizeHandleComponent } from './resize-handle'; const MODAL_LIST = [ AboutZeppelinComponent, @@ -46,13 +48,13 @@ const MODAL_LIST = [ NoteRenameComponent, FolderRenameComponent ]; -const EXPORT_LIST = [HeaderComponent, NodeListComponent, PageHeaderComponent, SpinComponent]; +const EXPORT_LIST = [HeaderComponent, NodeListComponent, PageHeaderComponent, SpinComponent, ResizeHandleComponent]; const PIPES = [HumanizeBytesPipe]; @NgModule({ - declarations: [MODAL_LIST, EXPORT_LIST, PIPES, RunScriptsDirective], + declarations: [MODAL_LIST, EXPORT_LIST, PIPES, MathJaxDirective, RunScriptsDirective], entryComponents: [MODAL_LIST], - exports: [EXPORT_LIST, PIPES, RunScriptsDirective], + exports: [EXPORT_LIST, PIPES, MathJaxDirective, RunScriptsDirective], imports: [ FormsModule, CommonModule, diff --git a/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.html b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.html new file mode 100644 index 00000000000..280556d858d --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.html @@ -0,0 +1,23 @@ +
+ + + + +
+
+ + View + + + + + + + + +
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.less b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.less new file mode 100644 index 00000000000..f5f75706f6e --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.less @@ -0,0 +1,3 @@ +.area-chart-setting { + margin: 10px 0; +} diff --git a/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.ts new file mode 100644 index 00000000000..6416c296111 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.component.ts @@ -0,0 +1,90 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnInit, + ViewChild +} from '@angular/core'; + +import { G2VisualizationComponentBase, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +import { VisualizationPivotSettingComponent } from '../common/pivot-setting/pivot-setting.component'; +import { calcTickCount } from '../common/util/calc-tick-count'; +import { setChartXAxis } from '../common/util/set-x-axis'; +import { VisualizationXAxisSettingComponent } from '../common/x-axis-setting/x-axis-setting.component'; + +@Component({ + selector: 'zeppelin-area-chart-visualization', + templateUrl: './area-chart-visualization.component.html', + styleUrls: ['./area-chart-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AreaChartVisualizationComponent extends G2VisualizationComponentBase implements OnInit, AfterViewInit { + @ViewChild('container', { static: false }) container: ElementRef; + @ViewChild(VisualizationXAxisSettingComponent, { static: false }) + xAxisSettingComponent: VisualizationXAxisSettingComponent; + @ViewChild(VisualizationPivotSettingComponent, { static: false }) + pivotSettingComponent: VisualizationPivotSettingComponent; + style: 'stream' | 'expand' | 'stack' = 'stack'; + + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) { + super(visualization); + } + + viewChange() { + this.config.setting.stackedAreaChart.style = this.style; + this.visualization.configChange$.next(this.config); + } + + ngOnInit() {} + + refreshSetting() { + this.style = this.config.setting.stackedAreaChart.style; + this.pivotSettingComponent.init(); + this.xAxisSettingComponent.init(); + this.cdr.markForCheck(); + } + + ngAfterViewInit(): void { + this.render(); + } + + setScale() { + const key = this.getKey(); + const tickCount = calcTickCount(this.container.nativeElement); + this.chart.scale(key, { + tickCount, + type: 'cat' + }); + } + + renderBefore() { + const key = this.getKey(); + this.setScale(); + if (this.style === 'stack') { + // area:stack + this.chart + .areaStack() + .position(`${key}*__value__`) + .color('__key__'); + } else if (this.style === 'stream') { + // area:stream + this.chart + .area() + .position(`${key}*__value__`) + .adjust(['stack', 'symmetric']) + .color('__key__'); + } else { + // area:percent + this.chart + .areaStack() + .position(`${key}*__percent__`) + .color('__key__'); + } + + setChartXAxis(this.visualization, 'stackedAreaChart', this.chart, key); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.ts b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.ts new file mode 100644 index 00000000000..e1c30b78898 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/area-chart/area-chart-visualization.ts @@ -0,0 +1,20 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { G2VisualizationBase, VisualizationComponentPortal } from '@zeppelin/visualization'; + +import { AreaChartVisualizationComponent } from './area-chart-visualization.component'; + +export class AreaChartVisualization extends G2VisualizationBase { + componentPortal = new VisualizationComponentPortal( + this, + AreaChartVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.html b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.html new file mode 100644 index 00000000000..e8397171066 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.html @@ -0,0 +1,22 @@ +
+ + + + +
+ + View + + + + + + + +
+
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.less b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.less new file mode 100644 index 00000000000..26b3edd4589 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.less @@ -0,0 +1,13 @@ +@import "theme-mixin"; + +.themeMixin({ + bar-chart-setting { + margin: 10px 0; + } + .field-setting-wrap { + margin-top: 10px; + } + .drag-wrap { + min-height: 23px; + } +}); diff --git a/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.ts new file mode 100644 index 00000000000..b8ffa35daae --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.component.ts @@ -0,0 +1,95 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnInit, + ViewChild +} from '@angular/core'; + +import { get } from 'lodash'; + +import { VisualizationMultiBarChart } from '@zeppelin/sdk'; +import { G2VisualizationComponentBase, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +import { VisualizationPivotSettingComponent } from '../common/pivot-setting/pivot-setting.component'; +import { calcTickCount } from '../common/util/calc-tick-count'; +import { setChartXAxis } from '../common/util/set-x-axis'; +import { VisualizationXAxisSettingComponent } from '../common/x-axis-setting/x-axis-setting.component'; + +@Component({ + selector: 'zeppelin-bar-chart-visualization', + templateUrl: './bar-chart-visualization.component.html', + styleUrls: ['./bar-chart-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BarChartVisualizationComponent extends G2VisualizationComponentBase implements OnInit, AfterViewInit { + @ViewChild('container', { static: false }) container: ElementRef; + @ViewChild(VisualizationXAxisSettingComponent, { static: false }) + xAxisSettingComponent: VisualizationXAxisSettingComponent; + @ViewChild(VisualizationPivotSettingComponent, { static: false }) + pivotSettingComponent: VisualizationPivotSettingComponent; + stacked = false; + + viewChange() { + if (!this.config.setting.multiBarChart) { + this.config.setting.multiBarChart = new VisualizationMultiBarChart(); + } + this.config.setting.multiBarChart.stacked = this.stacked; + this.visualization.configChange$.next(this.config); + } + + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) { + super(visualization); + } + + ngOnInit() {} + + ngAfterViewInit() { + this.render(); + } + + refreshSetting() { + this.stacked = get(this.config.setting, 'multiBarChart.stacked', false); + this.pivotSettingComponent.init(); + this.xAxisSettingComponent.init(); + this.cdr.markForCheck(); + } + + setScale() { + const key = this.getKey(); + const tickCount = calcTickCount(this.container.nativeElement); + this.chart.scale(key, { + tickCount, + type: 'cat' + }); + } + + renderBefore(chart) { + const key = this.getKey(); + this.setScale(); + + if (get(this.config.setting, 'multiBarChart.stacked', false)) { + this.chart + .intervalStack() + .position(`${key}*__value__`) + .color('__key__') + .opacity(1); + } else { + this.chart + .interval() + .position(`${key}*__value__`) + .color('__key__') + .opacity(1) + .adjust([ + { + type: 'dodge', + marginRatio: 0 + } + ]); + } + setChartXAxis(this.visualization, 'multiBarChart', this.chart, key); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.ts b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.ts new file mode 100644 index 00000000000..38b9fcb6b57 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/bar-chart/bar-chart-visualization.ts @@ -0,0 +1,19 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { G2VisualizationBase, VisualizationComponentPortal } from '@zeppelin/visualization'; + +import { BarChartVisualizationComponent } from './bar-chart-visualization.component'; + +export class BarChartVisualization extends G2VisualizationBase { + componentPortal = new VisualizationComponentPortal( + this, + BarChartVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.html b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.html new file mode 100644 index 00000000000..63de3f104f0 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.html @@ -0,0 +1,73 @@ + +
+ {{item.name}} +
+
+
+ +
+ + {{item.name}} + + +
+
+
+
+ +
+ + {{item.name}} + + +
+
+
+
+ +
+ + {{item.name}} {{item.aggr | uppercase}}  + + +
    +
  • + {{aggregate}} +
  • +
+
+
+
+
+
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less new file mode 100644 index 00000000000..2de808c2c0a --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less @@ -0,0 +1,12 @@ +@import "theme-mixin"; + +.themeMixin({ + display: block; + margin: 10px 0; + .field-setting-wrap { + margin-top: 24px; + } + .drag-wrap { + min-height: 23px; + } +}); diff --git a/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.ts b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.ts new file mode 100644 index 00000000000..a8b6b618263 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.ts @@ -0,0 +1,77 @@ +import { copyArrayItem, moveItemInArray, transferArrayItem, CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { TableData, Visualization } from '@zeppelin/visualization'; + +@Component({ + selector: 'zeppelin-visualization-pivot-setting', + templateUrl: './pivot-setting.component.html', + styleUrls: ['./pivot-setting.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VisualizationPivotSettingComponent implements OnInit { + @Input() visualization: Visualization; + + tableData: TableData; + config: GraphConfig; + columns = []; + aggregates = ['sum', 'count', 'avg', 'min', 'max']; + + // tslint:disable-next-line + drop(event: CdkDragDrop) { + if (event.container.id === 'columns-list') { + return; + } + if (event.previousContainer === event.container) { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } else { + if ( + event.container.id !== 'value-list' && + event.container.data.findIndex(e => e.name === event.previousContainer.data[event.previousIndex].name) !== -1 + ) { + return; + } + if (event.previousContainer.id === 'columns-list') { + copyArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex); + } else { + transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex); + } + } + this.visualization.configChange$.next(this.config); + } + + // tslint:disable-next-line + removeFieldAt(data: any[], index: number): void { + data.splice(index, 1); + this.visualization.configChange$.next(this.config); + this.cdr.markForCheck(); + } + + changeAggregate(aggregates: string, index: number): void { + this.config.values[index].aggr = aggregates; + this.visualization.configChange$.next(this.config); + this.cdr.markForCheck(); + } + + noReturnPredicate() { + return false; + } + + init() { + this.tableData = this.visualization.getTransformation().getTableData() as TableData; + this.config = this.visualization.getConfig(); + this.columns = this.tableData.columns.map((name, index) => ({ + name, + index, + aggr: 'sum' + })); + this.cdr.markForCheck(); + } + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.init(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.html b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.html new file mode 100644 index 00000000000..7d8877a0803 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.html @@ -0,0 +1,86 @@ + +
+ {{item.name}} +
+
+
+ +
+ + {{item.name}} + + +
+
+
+
+ +
+ + {{item.name}} + + +
+
+
+
+ +
+ + {{item.name}} + + +
+
+
+
+ +
+ + {{item.name}} + + +
+
+
+ +
+
diff --git a/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less new file mode 100644 index 00000000000..b3f7094ace6 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less @@ -0,0 +1,12 @@ +@import "theme-mixin"; + +.themeMixin({ + display: block; + margin: 10px 0; + .field-setting-wrap { + margin-top: 10px; + } + .drag-wrap { + min-height: 23px; + } +}); diff --git a/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.ts b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.ts new file mode 100644 index 00000000000..34f15f80bcc --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.ts @@ -0,0 +1,90 @@ +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { get } from 'lodash'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { TableData, Visualization } from '@zeppelin/visualization'; + +@Component({ + selector: 'zeppelin-visualization-scatter-setting', + templateUrl: './scatter-setting.component.html', + styleUrls: ['./scatter-setting.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VisualizationScatterSettingComponent implements OnInit { + @Input() visualization: Visualization; + + tableData: TableData; + config: GraphConfig; + columns = []; + + field = { + xAxis: [], + yAxis: [], + group: [], + size: [] + }; + + // tslint:disable-next-line + drop(event: CdkDragDrop) { + this.clean(event.container.data, false); + event.container.data.push(event.previousContainer.data[event.previousIndex]); + this.cdr.markForCheck(); + this.updateConfig(); + } + + // tslint:disable-next-line + clean(data: any[], update = true): void { + while (data.length > 0) { + data.splice(0, 1); + } + if (update) { + this.updateConfig(); + } + this.cdr.markForCheck(); + } + + noReturnPredicate() { + return false; + } + + updateConfig() { + if (!this.config.setting.scatterChart) { + this.config.setting.scatterChart = {}; + } + const scatterSetting = this.config.setting.scatterChart; + scatterSetting.xAxis = this.field.xAxis[0]; + scatterSetting.yAxis = this.field.yAxis[0]; + scatterSetting.size = this.field.size[0]; + scatterSetting.group = this.field.group[0]; + this.visualization.configChange$.next(this.config); + } + + constructor(private cdr: ChangeDetectorRef) {} + + init() { + this.tableData = this.visualization.getTransformation().getTableData() as TableData; + this.config = this.visualization.getConfig(); + this.columns = this.tableData.columns.map((name, index) => ({ + name, + index, + aggr: 'sum' + })); + + const xAxis = get(this.config.setting, 'scatterChart.xAxis', this.columns[0]); + const yAxis = get(this.config.setting, 'scatterChart.yAxis', this.columns[1]); + const group = get(this.config.setting, 'scatterChart.group'); + const size = get(this.config.setting, 'scatterChart.size'); + const arrayWrapper = value => (value ? [value] : []); + this.field.xAxis = arrayWrapper(xAxis); + this.field.yAxis = arrayWrapper(yAxis); + this.field.group = arrayWrapper(group); + this.field.size = arrayWrapper(size); + this.cdr.markForCheck(); + } + + ngOnInit() { + this.init(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/common/util/calc-tick-count.ts b/zeppelin-frontend/src/app/visualizations/common/util/calc-tick-count.ts new file mode 100644 index 00000000000..9be2f00ecc8 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/util/calc-tick-count.ts @@ -0,0 +1,10 @@ +export const DEFAULT_TICK_COUNT = 16; + +export function calcTickCount(el: HTMLElement) { + if (el && el.getBoundingClientRect) { + const tickCount = Math.round(el.getBoundingClientRect().width / 60); + return Number.isNaN(tickCount) ? DEFAULT_TICK_COUNT : tickCount; + } else { + return DEFAULT_TICK_COUNT; + } +} diff --git a/zeppelin-frontend/src/app/visualizations/common/util/set-x-axis.ts b/zeppelin-frontend/src/app/visualizations/common/util/set-x-axis.ts new file mode 100644 index 00000000000..36f8c530d5d --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/util/set-x-axis.ts @@ -0,0 +1,34 @@ +import * as G2 from '@antv/g2'; +import { get } from 'lodash'; + +import { Visualization } from '@zeppelin/visualization'; + +export function setChartXAxis( + visualization: Visualization, + mode: 'lineChart' | 'multiBarChart' | 'stackedAreaChart', + chart: G2.Chart, + key: string +) { + const config = visualization.getConfig(); + const setting = config.setting[mode]; + chart.axis(key, { + label: { + textStyle: { + rotate: 0 + } + } + }); + switch (setting.xLabelStatus) { + case 'hide': + chart.axis(key, false); + break; + case 'rotate': + chart.axis(key, { + label: { + textStyle: { + rotate: Number.parseInt(get(setting, 'rotate.degree', '-45'), 10) + } + } + }); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.html b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.html new file mode 100644 index 00000000000..d2fb87081cf --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.html @@ -0,0 +1,26 @@ +
+ + xAxis + + + + + + + + + + degree + + + + + + + +
diff --git a/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.less b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.less new file mode 100644 index 00000000000..825d745fbd4 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.less @@ -0,0 +1,6 @@ +@import "theme-mixin"; + +.themeMixin({ + display: block; + margin: 10px 0; +}); diff --git a/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.ts b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.ts new file mode 100644 index 00000000000..a1a2b66e204 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/common/x-axis-setting/x-axis-setting.component.ts @@ -0,0 +1,66 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; + +import { get } from 'lodash'; + +import { GraphConfig, XAxisSetting, XLabelStatus } from '@zeppelin/sdk'; +import { Visualization } from '@zeppelin/visualization'; + +@Component({ + selector: 'zeppelin-visualization-x-axis-setting', + templateUrl: './x-axis-setting.component.html', + styleUrls: ['./x-axis-setting.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VisualizationXAxisSettingComponent implements OnInit { + @Input() visualization: Visualization; + @Input() mode: 'lineChart' | 'multiBarChart' | 'stackedAreaChart'; + + setting: XAxisSetting; + config: GraphConfig; + xLabelStatus: XLabelStatus = 'default'; + degree = '-45'; + previousDegree: string; + constructor(private cdr: ChangeDetectorRef) {} + + onStatusChange() { + this.setting.xLabelStatus = this.xLabelStatus; + this.updateConfig(); + } + + onDegreeChange() { + if (this.degree === this.previousDegree) { + return; + } + const degree = Number.parseInt(this.degree, 10); + if (Number.isNaN(degree)) { + this.degree = this.previousDegree; + return; + } else { + this.degree = `${degree}`; + this.previousDegree = this.degree; + } + this.updateConfig(); + } + + updateConfig() { + this.setting.rotate.degree = this.degree; + this.setting.xLabelStatus = this.xLabelStatus; + this.visualization.configChange$.next(this.config); + } + + init() { + this.config = this.visualization.getConfig(); + this.setting = this.config.setting[this.mode]; + if (!this.setting.rotate) { + this.setting.rotate = { degree: '-45' }; + } + this.xLabelStatus = get(this.setting, ['xLabelStatus'], 'default'); + this.degree = get(this.setting, ['rotate', 'degree'], '-45'); + this.previousDegree = this.degree; + this.cdr.markForCheck(); + } + + ngOnInit() { + this.init(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.html b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.html new file mode 100644 index 00000000000..fe37b07cffd --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.html @@ -0,0 +1,32 @@ +
+ + +
+ +
+ +
+ +
+ + +
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.less b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.less new file mode 100644 index 00000000000..f61d40c653c --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.less @@ -0,0 +1,13 @@ +@import "theme-mixin"; + +.themeMixin({ + .zoom-tips { + color: @text-color-secondary; + } + .line-setting { + margin: 10px 0; + input.format-input { + width: 160px; + } + } +}); diff --git a/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.ts new file mode 100644 index 00000000000..4afc0df215d --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.component.ts @@ -0,0 +1,125 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnInit, + ViewChild +} from '@angular/core'; + +import { G2VisualizationComponentBase, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +import { VisualizationPivotSettingComponent } from '../common/pivot-setting/pivot-setting.component'; +import { calcTickCount } from '../common/util/calc-tick-count'; +import { setChartXAxis } from '../common/util/set-x-axis'; +import { VisualizationXAxisSettingComponent } from '../common/x-axis-setting/x-axis-setting.component'; + +@Component({ + selector: 'zeppelin-line-chart-visualization', + templateUrl: './line-chart-visualization.component.html', + styleUrls: ['./line-chart-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LineChartVisualizationComponent extends G2VisualizationComponentBase implements OnInit, AfterViewInit { + @ViewChild('container', { static: false }) container: ElementRef; + @ViewChild(VisualizationXAxisSettingComponent, { static: false }) + xAxisSettingComponent: VisualizationXAxisSettingComponent; + @ViewChild(VisualizationPivotSettingComponent, { static: false }) + pivotSettingComponent: VisualizationPivotSettingComponent; + forceY = false; + lineWithFocus = false; + isDateFormat = false; + dateFormat = ''; + + settingChange(): void { + const setting = this.config.setting.lineChart; + setting.lineWithFocus = this.lineWithFocus; + setting.forceY = this.forceY; + setting.isDateFormat = this.isDateFormat; + setting.dateFormat = this.dateFormat; + this.visualization.configChange$.next(this.config); + } + + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) { + super(visualization); + } + + ngOnInit() {} + + refreshSetting() { + const setting = this.config.setting.lineChart; + this.forceY = setting.forceY || false; + this.lineWithFocus = setting.lineWithFocus || false; + this.isDateFormat = setting.isDateFormat || false; + this.dateFormat = setting.dateFormat || ''; + this.pivotSettingComponent.init(); + this.xAxisSettingComponent.init(); + this.cdr.markForCheck(); + } + + setScale() { + const key = this.getKey(); + const tickCount = calcTickCount(this.container.nativeElement); + this.chart.scale(key, { + tickCount, + type: 'cat' + }); + } + + renderBefore() { + const key = this.getKey(); + const setting = this.config.setting.lineChart; + this.setScale(); + this.chart + .line() + .position(`${key}*__value__`) + .color('__key__'); + setChartXAxis(this.visualization, 'lineChart', this.chart, key); + + if (setting.isDateFormat) { + if (this.visualization.transformed && this.visualization.transformed.rows) { + const invalid = this.visualization.transformed.rows.some(r => { + const isInvalidDate = Number.isNaN(new Date(r[key]).valueOf()); + if (isInvalidDate) { + console.warn(`${r[key]} is [Invalid Date]`); + } + return isInvalidDate; + }); + if (invalid) { + return; + } + this.chart.scale({ + [key]: { + type: 'time', + mask: setting.dateFormat || 'YYYY-MM-DD' + } + }); + } + } + + if (setting.forceY) { + this.chart.scale({ + __value__: { + min: 0 + } + }); + } + } + + renderAfter() { + const setting = this.config.setting.lineChart; + if (setting.lineWithFocus) { + // tslint:disable-next-line + (this.chart as any).interact('brush'); + } else { + // tslint:disable-next-line:no-any + (this.chart as any).clearInteraction(); + } + } + + ngAfterViewInit() { + this.render(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.ts b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.ts new file mode 100644 index 00000000000..459c3f89a89 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/line-chart/line-chart-visualization.ts @@ -0,0 +1,20 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { G2VisualizationBase, VisualizationComponentPortal } from '@zeppelin/visualization'; + +import { LineChartVisualizationComponent } from './line-chart-visualization.component'; + +export class LineChartVisualization extends G2VisualizationBase { + componentPortal = new VisualizationComponentPortal( + this, + LineChartVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.html b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.html new file mode 100644 index 00000000000..eb31d3ed772 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.html @@ -0,0 +1,7 @@ +
+ + +
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.less b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.ts new file mode 100644 index 00000000000..9176ca29fb4 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.component.ts @@ -0,0 +1,61 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Inject, + OnInit, + ViewChild +} from '@angular/core'; + +import { G2VisualizationComponentBase, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +import { VisualizationPivotSettingComponent } from '../common/pivot-setting/pivot-setting.component'; + +@Component({ + selector: 'zeppelin-pie-chart-visualization', + templateUrl: './pie-chart-visualization.component.html', + styleUrls: ['./pie-chart-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PieChartVisualizationComponent extends G2VisualizationComponentBase implements OnInit, AfterViewInit { + @ViewChild('container', { static: false }) container: ElementRef; + @ViewChild(VisualizationPivotSettingComponent, { static: false }) + pivotSettingComponent: VisualizationPivotSettingComponent; + + constructor(@Inject(VISUALIZATION) public visualization: Visualization) { + super(visualization); + } + + ngOnInit() {} + + refreshSetting() { + this.pivotSettingComponent.init(); + } + + setScale() { + // Noop + } + + renderBefore() { + this.chart.tooltip({ + showTitle: false + }); + this.chart.coord('theta', { + radius: 0.75 + }); + this.chart + .intervalStack() + .position('__value__') + .color('__key__') + .style({ + lineWidth: 1, + stroke: '#fff' + }) + .tooltip('__key__*__value__', (name, value) => ({ name, value })); + } + + ngAfterViewInit() { + this.render(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.ts b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.ts new file mode 100644 index 00000000000..26104347968 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/pie-chart/pie-chart-visualization.ts @@ -0,0 +1,20 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { G2VisualizationBase, VisualizationComponentPortal } from '@zeppelin/visualization'; + +import { PieChartVisualizationComponent } from './pie-chart-visualization.component'; + +export class PieChartVisualization extends G2VisualizationBase { + componentPortal = new VisualizationComponentPortal( + this, + PieChartVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.html b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.html new file mode 100644 index 00000000000..c74daf53b02 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.html @@ -0,0 +1,7 @@ +
+ + +
+
+
diff --git a/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.less b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.ts new file mode 100644 index 00000000000..0d679596d46 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.component.ts @@ -0,0 +1,77 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnInit, + ViewChild +} from '@angular/core'; + +import { get } from 'lodash'; + +import { G2VisualizationComponentBase, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +import { VisualizationScatterSettingComponent } from '../common/scatter-setting/scatter-setting.component'; +import { calcTickCount } from '../common/util/calc-tick-count'; + +@Component({ + selector: 'zeppelin-scatter-chart-visualization', + templateUrl: './scatter-chart-visualization.component.html', + styleUrls: ['./scatter-chart-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ScatterChartVisualizationComponent extends G2VisualizationComponentBase implements OnInit, AfterViewInit { + @ViewChild('container', { static: false }) container: ElementRef; + @ViewChild(VisualizationScatterSettingComponent, { static: false }) + scatterSettingComponent: VisualizationScatterSettingComponent; + + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) { + super(visualization); + } + + refreshSetting() { + this.scatterSettingComponent.init(); + this.cdr.markForCheck(); + } + + setScale() { + const key = this.getKey(); + const tickCount = calcTickCount(this.container.nativeElement); + this.chart.scale(key, { + tickCount, + type: 'cat' + }); + } + + renderBefore() { + const key = this.getKey(); + const size = get(this.config.setting, 'scatterChart.size.name'); + this.setScale(); + this.chart.tooltip({ + crosshairs: { + type: 'cross' + } + }); + this.chart.legend('__value__', false); + // point + const geom = this.chart + .point() + .position(`${key}*__value__`) + .color('__key__') + // .adjust('jitter') + .opacity(0.65) + .shape('circle'); + + if (size) { + geom.size('__value__'); + } + } + + ngOnInit() {} + + ngAfterViewInit() { + this.render(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.ts b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.ts new file mode 100644 index 00000000000..1ba0c9514ae --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/scatter-chart/scatter-chart-visualization.ts @@ -0,0 +1,20 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { G2VisualizationBase, VisualizationComponentPortal } from '@zeppelin/visualization'; + +import { ScatterChartVisualizationComponent } from './scatter-chart-visualization.component'; + +export class ScatterChartVisualization extends G2VisualizationBase { + componentPortal = new VisualizationComponentPortal( + this, + ScatterChartVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.html b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.html new file mode 100644 index 00000000000..92dec1eff0d --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.html @@ -0,0 +1,111 @@ + + + + + + + + + {{col}} + + +
    + +
  • +
  • + Type +
      +
    • + {{type | titlecase}} +
    • +
    +
  • +
  • +
  • + Aggregation +
      +
    • + {{aggregation | titlecase}} +
    • +
    +
  • +
+
+ + + + + + {{data[col]}} + + +
+ +
+ + + {{aggregation}}({{col}}): {{colOptions.get(col).aggregationValue}} + + +
+
diff --git a/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.less b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.less new file mode 100644 index 00000000000..57c4ed1dff8 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.less @@ -0,0 +1,32 @@ +@import "theme-mixin"; + +.th-dropdown { + .search-bar.ant-menu-item-group { + padding: 5px 12px; + ::ng-deep .ant-menu-item-group-title{ + display: none; + } + } +} + +.export-dropdown { + margin-bottom: 16px; + position: absolute; + bottom: 0; + left: 0; +} + +.themeMixin({ + display: block; + position: relative; + ::ng-deep .filter-icon svg { + transition: transform 300ms; + } + + .aggregation-wrap { + text-align: right; + .aggregation-item { + margin-left: 10px; + } + } +}); diff --git a/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.ts b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.ts new file mode 100644 index 00000000000..35f3d08ce4a --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/table/table-visualization.component.ts @@ -0,0 +1,162 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, ViewChild } from '@angular/core'; + +import { filter, maxBy, minBy, orderBy, sumBy } from 'lodash'; +import { NzTableComponent } from 'ng-zorro-antd'; +import { utils, writeFile, WorkSheet } from 'xlsx'; + +import { TableData, Visualization, VISUALIZATION } from '@zeppelin/visualization'; + +type ColType = 'string' | 'date' | 'number'; +type AggregationType = 'count' | 'sum' | 'min' | 'max' | 'avg'; + +class FilterOption { + sort: 'desc' | 'asc' | '' = ''; + type: ColType = 'string'; + visible = true; + pinned?: string; + term = ''; + width: string | '*' = '*'; + aggregation: AggregationType | null = null; + aggregationValue: number | null = null; +} + +function typeCoercion(value: string, type: ColType): string | number | Date { + switch (type) { + case 'number': + const num = Number.parseFloat(value); + return Number.isNaN(num) ? value : num; + case 'date': + const date = new Date(value); + return Number.isNaN(date.valueOf()) ? value : date; + default: + return value; + } +} + +@Component({ + selector: 'zeppelin-visualization-table-visualization', + templateUrl: './table-visualization.component.html', + styleUrls: ['./table-visualization.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableVisualizationComponent implements OnInit { + tableData: TableData; + // tslint:disable-next-line:no-any + rows: any[] = []; + columns: string[] = []; + colOptions = new Map(); + types: ColType[] = ['string', 'number', 'date']; + aggregations: AggregationType[] = ['count', 'sum', 'min', 'max', 'avg']; + @ViewChild(NzTableComponent, { static: false }) nzTable: NzTableComponent; + + exportFile(type: 'csv' | 'xlsx', all = true) { + const wb = utils.book_new(); + let ws: WorkSheet; + if (all) { + ws = utils.json_to_sheet(this.rows); + } else { + ws = utils.json_to_sheet(this.nzTable.data); + } + utils.book_append_sheet(wb, ws, 'Sheet1'); + writeFile(wb, `export.${type}`); + } + + onChangeType(type: ColType, col: string) { + const opt = this.colOptions.get(col); + opt.type = type; + this.filterRows(); + this.aggregate(); + } + + onChangeAggregation(aggregation: AggregationType, col: string) { + const opt = this.colOptions.get(col); + opt.aggregation = opt.aggregation === aggregation ? null : aggregation; + this.aggregate(); + } + + onSearch(): void { + this.filterRows(); + } + + onSortChange(type: 'descend' | 'ascend' | string, key: string): void { + const opt: FilterOption = this.colOptions.get(key); + this.colOptions.delete(key); + if (type) { + opt.sort = type === 'descend' ? 'desc' : 'asc'; + } else { + opt.sort = ''; + } + this.colOptions.set(key, opt); + this.filterRows(); + } + + aggregate() { + this.colOptions.forEach((opt, key) => { + const numValue = row => { + const value = typeCoercion(row[key], opt.type); + if (typeof value === 'number') { + return value; + } + if (value instanceof Date) { + return value.valueOf(); + } + return value; + }; + const getSum = () => + sumBy(this.tableData.rows, row => { + const value = typeCoercion(row[key], 'number'); + return typeof value === 'number' ? value : 0; + }); + + switch (opt.aggregation) { + case 'sum': + opt.aggregationValue = getSum(); + break; + case 'avg': + opt.aggregationValue = getSum() / this.tableData.rows.length; + break; + case 'count': + opt.aggregationValue = this.tableData.rows.length; + break; + case 'max': + opt.aggregationValue = maxBy(this.tableData.rows, numValue)[key]; + break; + case 'min': + opt.aggregationValue = minBy(this.tableData.rows, numValue)[key]; + break; + default: + opt.aggregationValue = null; + } + }); + } + + filterRows() { + const sortKeys = []; + const sortTypes = []; + const terms = []; + this.colOptions.forEach((value, key) => { + if (value.sort) { + sortKeys.push(row => typeCoercion(row[key], value.type)); + sortTypes.push(value.sort); + } + terms.push(row => String(row[key]).search(value.term) !== -1); + }); + this.rows = filter(this.tableData.rows, row => terms.every(term => term(row))); + this.rows = orderBy(this.rows, sortKeys, sortTypes); + this.cdr.markForCheck(); + } + + constructor(@Inject(VISUALIZATION) public visualization: Visualization, private cdr: ChangeDetectorRef) {} + + ngOnInit() {} + + render() { + this.tableData = this.visualization.transformed; + this.columns = this.tableData.columns; + this.rows = [...this.tableData.rows]; + this.columns.forEach(col => { + this.colOptions.set(col, new FilterOption()); + }); + this.filterRows(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/table/table-visualization.ts b/zeppelin-frontend/src/app/visualizations/table/table-visualization.ts new file mode 100644 index 00000000000..69059d9f133 --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/table/table-visualization.ts @@ -0,0 +1,48 @@ +import { CdkPortalOutlet } from '@angular/cdk/portal'; +import { ViewContainerRef } from '@angular/core'; + +import { GraphConfig } from '@zeppelin/sdk'; +import { + TableTransformation, + Transformation, + Visualization, + VisualizationComponentPortal +} from '@zeppelin/visualization'; + +import { TableVisualizationComponent } from './table-visualization.component'; + +export class TableVisualization extends Visualization { + tableTransformation = new TableTransformation(this.getConfig()); + componentPortal = new VisualizationComponentPortal( + this, + TableVisualizationComponent, + this.portalOutlet, + this.viewContainerRef + ); + constructor(config: GraphConfig, private portalOutlet: CdkPortalOutlet, private viewContainerRef: ViewContainerRef) { + super(config); + } + + destroy(): void { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this.configChange$.complete(); + this.configChange$ = null; + } + + getTransformation(): Transformation { + return this.tableTransformation; + } + + refresh(): void {} + + render(data): void { + this.transformed = data; + if (!this.componentRef) { + this.componentRef = this.componentPortal.attachComponentPortal(); + } + this.componentRef.instance.render(); + } +} diff --git a/zeppelin-frontend/src/app/visualizations/visualization.module.ts b/zeppelin-frontend/src/app/visualizations/visualization.module.ts new file mode 100644 index 00000000000..ae1e60f77ca --- /dev/null +++ b/zeppelin-frontend/src/app/visualizations/visualization.module.ts @@ -0,0 +1,67 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { + NzButtonModule, + NzCardModule, + NzCheckboxModule, + NzDropDownModule, + NzFormModule, + NzGridModule, + NzIconModule, + NzInputModule, + NzMenuModule, + NzRadioModule, + NzTableModule, + NzTagModule +} from 'ng-zorro-antd'; + +import { AreaChartVisualizationComponent } from './area-chart/area-chart-visualization.component'; +import { BarChartVisualizationComponent } from './bar-chart/bar-chart-visualization.component'; +import { VisualizationPivotSettingComponent } from './common/pivot-setting/pivot-setting.component'; +import { VisualizationScatterSettingComponent } from './common/scatter-setting/scatter-setting.component'; +import { VisualizationXAxisSettingComponent } from './common/x-axis-setting/x-axis-setting.component'; +import { LineChartVisualizationComponent } from './line-chart/line-chart-visualization.component'; +import { PieChartVisualizationComponent } from './pie-chart/pie-chart-visualization.component'; +import { ScatterChartVisualizationComponent } from './scatter-chart/scatter-chart-visualization.component'; +import { TableVisualizationComponent } from './table/table-visualization.component'; + +const VisualizationComponents = [ + TableVisualizationComponent, + AreaChartVisualizationComponent, + BarChartVisualizationComponent, + LineChartVisualizationComponent, + PieChartVisualizationComponent, + ScatterChartVisualizationComponent +]; + +@NgModule({ + declarations: [ + ...VisualizationComponents, + VisualizationPivotSettingComponent, + VisualizationScatterSettingComponent, + VisualizationXAxisSettingComponent + ], + entryComponents: [...VisualizationComponents], + exports: [...VisualizationComponents], + imports: [ + CommonModule, + FormsModule, + DragDropModule, + NzTableModule, + NzCardModule, + NzTagModule, + NzFormModule, + NzInputModule, + NzGridModule, + NzIconModule, + NzMenuModule, + NzDropDownModule, + NzRadioModule, + NzCheckboxModule, + NzButtonModule + ] +}) +export class VisualizationModule {} From 95bccaf45c19d360af909fee933a1e36d9f53d2e Mon Sep 17 00:00:00 2001 From: Wendell Date: Tue, 22 Oct 2019 14:20:13 +0800 Subject: [PATCH 06/19] feat: add code editor --- .../workspace/notebook/notebook.module.ts | 2 + .../code-editor/code-editor.component.html | 4 + .../code-editor/code-editor.component.less | 19 ++ .../code-editor/code-editor.component.ts | 226 +++++++++++++++++ .../paragraph/paragraph.component.html | 14 ++ .../notebook/paragraph/paragraph.component.ts | 7 + .../code-editor/code-editor.component.html | 7 + .../code-editor/code-editor.component.ts | 232 ++++++++++++++++++ .../share/code-editor/code-editor.module.ts | 14 ++ .../share/code-editor/code-editor.service.ts | 92 +++++++ .../src/app/share/code-editor/index.ts | 1 + .../code-editor/nz-code-editor.definitions.ts | 34 +++ .../src/app/share/code-editor/public-api.ts | 4 + .../src/app/share/share.module.ts | 6 +- 14 files changed, 660 insertions(+), 2 deletions(-) create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less create mode 100644 zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/code-editor.component.html create mode 100644 zeppelin-frontend/src/app/share/code-editor/code-editor.component.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/code-editor.module.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/code-editor.service.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/index.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/nz-code-editor.definitions.ts create mode 100644 zeppelin-frontend/src/app/share/code-editor/public-api.ts diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts index e67220f093c..8e7bd97a57d 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.module.ts @@ -30,6 +30,7 @@ import { ShareModule } from '@zeppelin/share'; import { VisualizationModule } from 'src/app/visualizations/visualization.module'; import { NotebookAddParagraphComponent } from './add-paragraph/add-paragraph.component'; import { NotebookInterpreterBindingComponent } from './interpreter-binding/interpreter-binding.component'; +import { NotebookParagraphCodeEditorComponent } from './paragraph/code-editor/code-editor.component'; import { NotebookParagraphControlComponent } from './paragraph/control/control.component'; import { NotebookParagraphDynamicFormsComponent } from './paragraph/dynamic-forms/dynamic-forms.component'; import { NotebookParagraphFooterComponent } from './paragraph/footer/footer.component'; @@ -53,6 +54,7 @@ import { NotebookShareModule } from './share/share.module'; NotebookRevisionsComparatorComponent, NotebookParagraphComponent, NotebookAddParagraphComponent, + NotebookParagraphCodeEditorComponent, NotebookParagraphResultComponent, NotebookParagraphProgressComponent, NotebookParagraphFooterComponent, diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html new file mode 100644 index 00000000000..d39d2a098ea --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html @@ -0,0 +1,4 @@ + + diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less new file mode 100644 index 00000000000..0aad982837f --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less @@ -0,0 +1,19 @@ +@import "theme-mixin"; + +:host { + display: block; +} + +.themeMixin({ + + zeppelin-monaco-editor { + display: block; + border-left: 4px solid @border-color-split; + overflow: hidden; + + &.dirty { + border-left-color: @warning-color; + } + } + +}); diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts new file mode 100644 index 00000000000..2ba04359eb1 --- /dev/null +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts @@ -0,0 +1,226 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + NgZone, + OnChanges, + OnDestroy, + Output, + SimpleChanges +} from '@angular/core'; + +import { editor as MonacoEditor, IDisposable } from 'monaco-editor'; +import IStandaloneCodeEditor = MonacoEditor.IStandaloneCodeEditor; +import IEditor = monaco.editor.IEditor; + +import { InterpreterBindingItem } from '@zeppelin/sdk'; +import { CompletionService, MessageService } from '@zeppelin/services'; + +import { NotebookParagraphControlComponent } from '../control/control.component'; + +@Component({ + selector: 'zeppelin-notebook-paragraph-code-editor', + templateUrl: './code-editor.component.html', + styleUrls: ['./code-editor.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestroy, AfterViewInit { + // TODO: + // 1. cursor position + @Input() readOnly = false; + @Input() language = 'text'; + @Input() paragraphControl: NotebookParagraphControlComponent; + @Input() lineNumbers = false; + @Input() focus = false; + @Input() collaborativeMode = false; + @Input() text: string; + @Input() fontSize: number; + @Input() dirty = false; + @Input() interpreterBindings: InterpreterBindingItem[] = []; + @Input() pid: string; + @Output() readonly textChanged = new EventEmitter(); + @Output() readonly editorBlur = new EventEmitter(); + private editor: IStandaloneCodeEditor; + private monacoDisposables: IDisposable[] = []; + height = 0; + interpreterName: string; + + autoAdjustEditorHeight() { + if (this.editor) { + this.ngZone.run(() => { + this.height = + this.editor.getTopForLineNumber(Number.MAX_SAFE_INTEGER) + this.editor.getConfiguration().lineHeight * 2; + this.editor.layout(); + this.cdr.markForCheck(); + }); + } + } + + initEditorListener() { + const editor = this.editor; + this.monacoDisposables.push( + editor.onDidFocusEditorText(() => { + this.ngZone.runOutsideAngular(() => { + editor.updateOptions({ renderLineHighlight: 'all' }); + }); + }), + editor.onDidBlurEditorText(() => { + this.editorBlur.emit(); + this.ngZone.runOutsideAngular(() => { + editor.updateOptions({ renderLineHighlight: 'none' }); + }); + }), + editor.onDidChangeModelContent(() => { + this.ngZone.run(() => { + this.text = editor.getModel().getValue(); + this.textChanged.emit(this.text); + this.setParagraphMode(true); + this.autoAdjustEditorHeight(); + setTimeout(() => { + this.autoAdjustEditorHeight(); + }); + }); + }) + ); + } + + setEditorValue() { + if (this.editor && this.editor.getModel() && this.editor.getModel().getValue() !== this.text) { + this.editor.getModel().setValue(this.text || ''); + } + } + + initializedEditor(editor: IEditor) { + this.editor = editor as IStandaloneCodeEditor; + this.paragraphControl.updateListOfMenu(monaco); + if (this.paragraphControl) { + this.paragraphControl.listOfMenu.forEach((item, index) => { + this.editor.addAction({ + id: item.icon, + label: item.label, + keybindings: item.keyBindings, + precondition: null, + keybindingContext: null, + contextMenuGroupId: 'navigation', + contextMenuOrder: index, + run: () => item.trigger() + }); + }); + } + + this.updateEditorOptions(); + this.setParagraphMode(); + this.initEditorListener(); + this.initEditorFocus(); + this.initCompletionService(); + this.setEditorValue(); + setTimeout(() => { + this.autoAdjustEditorHeight(); + }); + } + + initCompletionService(): void { + this.completionService.registerAsCompletionReceiver(this.editor.getModel(), this.paragraphControl.pid); + } + + initEditorFocus() { + if (this.focus && this.editor) { + this.editor.focus(); + } + } + + updateEditorOptions() { + if (this.editor) { + this.editor.updateOptions({ + readOnly: this.readOnly, + fontSize: this.fontSize, + renderLineHighlight: this.focus ? 'all' : 'none', + minimap: { enabled: false }, + lineNumbers: this.lineNumbers ? 'on' : 'off', + glyphMargin: false, + folding: false, + scrollBeyondLastLine: false + }); + } + } + + getInterpreterName(paragraphText: string) { + const match = /^\s*%(.+?)(\s|\()/g.exec(paragraphText); + if (match) { + return match[1].trim(); + // get default interpreter name if paragraph text doesn't start with '%' + // TODO(mina): dig into the cause what makes interpreterBindings to have no element + } else if (this.interpreterBindings && this.interpreterBindings.length !== 0) { + return this.interpreterBindings[0].name; + } + return ''; + } + + setParagraphMode(changed = false) { + if (this.editor && !changed) { + const model = this.editor.getModel(); + if (this.language) { + // TODO: config convertMap + const convertMap = { + sh: 'shell' + }; + monaco.editor.setModelLanguage(model, convertMap[this.language] || this.language); + } + } else { + const interpreterName = this.getInterpreterName(this.text); + if (this.interpreterName !== interpreterName) { + this.interpreterName = interpreterName; + this.getEditorSetting(interpreterName); + } + } + } + + getEditorSetting(interpreterName: string) { + this.messageService.editorSetting(this.pid, interpreterName); + } + + layout() { + if (this.editor) { + setTimeout(() => { + this.editor.layout(); + }); + } + } + + constructor( + private cdr: ChangeDetectorRef, + private ngZone: NgZone, + private messageService: MessageService, + private completionService: CompletionService + ) {} + + ngOnChanges(changes: SimpleChanges): void { + const { text, interpreterBindings, language, readOnly, focus, lineNumbers, fontSize } = changes; + if (readOnly || focus || lineNumbers || fontSize) { + this.updateEditorOptions(); + } + if (focus) { + this.initEditorFocus(); + } + if (text) { + this.setEditorValue(); + } + + if (interpreterBindings || language) { + this.setParagraphMode(); + } + if (text || fontSize) { + this.autoAdjustEditorHeight(); + } + } + + ngOnDestroy(): void { + this.completionService.unregister(this.editor.getModel()); + this.monacoDisposables.forEach(d => d.dispose()); + } + + ngAfterViewInit(): void {} +} diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html index bf0246be645..df38ddeb04a 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.html @@ -41,6 +41,20 @@ (runAllBelowAndCurrent)="runAllBelowAndCurrent()" (cloneParagraph)="cloneParagraph()" (cancelParagraph)="cancelParagraph()"> + diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts index 0e2725fe1ea..094d7990ddd 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts @@ -9,6 +9,7 @@ import { OnInit, Output, QueryList, + ViewChild, ViewChildren } from '@angular/core'; import { Subject } from 'rxjs'; @@ -44,6 +45,7 @@ import { import { SpellResult } from '@zeppelin/spell/spell-result'; import { NzResizeEvent } from 'ng-zorro-antd/resizable'; +import { NotebookParagraphCodeEditorComponent } from './code-editor/code-editor.component'; import { NotebookParagraphResultComponent } from './result/result.component'; @Component({ @@ -53,6 +55,8 @@ import { NotebookParagraphResultComponent } from './result/result.component'; changeDetection: ChangeDetectionStrategy.OnPush }) export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy { + @ViewChild(NotebookParagraphCodeEditorComponent, { static: false }) + notebookParagraphCodeEditorComponent: NotebookParagraphCodeEditorComponent; @ViewChildren(NotebookParagraphResultComponent) notebookParagraphResultComponents: QueryList< NotebookParagraphResultComponent >; @@ -550,6 +554,9 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen if (needCommit) { this.commitParagraph(); } + if (this.notebookParagraphCodeEditorComponent) { + this.notebookParagraphCodeEditorComponent.layout(); + } if (updateResult) { this.notebookParagraphResultComponents.forEach(comp => { diff --git a/zeppelin-frontend/src/app/share/code-editor/code-editor.component.html b/zeppelin-frontend/src/app/share/code-editor/code-editor.component.html new file mode 100644 index 00000000000..13dd6bafc3a --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/code-editor.component.html @@ -0,0 +1,7 @@ +
+ +
+ +
+ +
diff --git a/zeppelin-frontend/src/app/share/code-editor/code-editor.component.ts b/zeppelin-frontend/src/app/share/code-editor/code-editor.component.ts new file mode 100644 index 00000000000..ef00e9e1dec --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/code-editor.component.ts @@ -0,0 +1,232 @@ +import { + forwardRef, + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + OnDestroy, + Output, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { combineLatest, fromEvent, BehaviorSubject, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, filter, map, takeUntil } from 'rxjs/operators'; + +import { inNextTick, warn, InputBoolean } from 'ng-zorro-antd/core'; + +import { CodeEditorService } from './code-editor.service'; +import { DiffEditorOptions, EditorOptions, JoinedEditorOptions, NzEditorMode } from './nz-code-editor.definitions'; + +// Import types from monaco editor. +import { editor } from 'monaco-editor'; +import IEditor = editor.IEditor; +import IDiffEditor = editor.IDiffEditor; +import ITextModel = editor.ITextModel; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + selector: 'zeppelin-code-editor', + exportAs: 'CodeEditor', + templateUrl: './code-editor.component.html', + host: { + '[class.ant-code-editor]': 'true' + }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CodeEditorComponent), + multi: true + } + ] +}) +export class CodeEditorComponent implements OnDestroy, AfterViewInit { + @Input() nzEditorMode: NzEditorMode = 'normal'; + @Input() nzOriginalText = ''; + @Input() @InputBoolean() nzLoading = false; + @Input() @InputBoolean() nzFullControl = false; + @Input() nzToolkit: TemplateRef; + + @Input() set nzEditorOption(value: JoinedEditorOptions) { + this.editorOption$.next(value); + } + + @Output() readonly nzEditorInitialized = new EventEmitter(); + + editorOptionCached: JoinedEditorOptions = {}; + + private readonly el: HTMLElement; + private destroy$ = new Subject(); + private resize$ = new Subject(); + private editorOption$ = new BehaviorSubject({}); + private editorInstance: IEditor | IDiffEditor; + private value = ''; + private modelSet = false; + + constructor(private nzCodeEditorService: CodeEditorService, private ngZone: NgZone, elementRef: ElementRef) { + this.el = elementRef.nativeElement; + } + + /** + * Initialize a monaco editor instance. + */ + ngAfterViewInit(): void { + this.nzCodeEditorService.requestToInit().subscribe(option => this.setup(option)); + } + + ngOnDestroy(): void { + if (this.editorInstance) { + this.editorInstance.dispose(); + } + + this.destroy$.next(); + this.destroy$.complete(); + } + + writeValue(value: string): void { + this.value = value; + this.setValue(); + } + + // tslint:disable-next-line no-any + registerOnChange(fn: (value: string) => void): any { + this.onChange = fn; + } + + // tslint:disable-next-line no-any + registerOnTouched(fn: any): void { + this.onTouch = fn; + } + + onChange(_value: string): void {} + + onTouch(): void {} + + layout(): void { + this.resize$.next(); + } + + private setup(option: JoinedEditorOptions): void { + this.editorOptionCached = option; + this.registerOptionChanges(); + this.initMonacoEditorInstance(); + this.registerResizeChange(); + this.setValue(); + + if (!this.nzFullControl) { + this.setValueEmitter(); + } + this.nzEditorInitialized.emit(this.editorInstance); + } + + private registerOptionChanges(): void { + combineLatest([this.editorOption$, this.nzCodeEditorService.option$]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([selfOpt, defaultOpt]) => { + this.editorOptionCached = { + ...this.editorOptionCached, + ...defaultOpt, + ...selfOpt + }; + this.updateOptionToMonaco(); + }); + } + + private initMonacoEditorInstance(): void { + this.ngZone.runOutsideAngular(() => { + this.editorInstance = + this.nzEditorMode === 'normal' + ? editor.create(this.el, { ...this.editorOptionCached }) + : editor.createDiffEditor(this.el, { + ...(this.editorOptionCached as DiffEditorOptions) + }); + }); + } + + private registerResizeChange(): void { + this.ngZone.runOutsideAngular(() => { + fromEvent(window, 'resize') + .pipe( + debounceTime(300), + takeUntil(this.destroy$) + ) + .subscribe(() => { + this.layout(); + }); + + this.resize$ + .pipe( + takeUntil(this.destroy$), + filter(() => !!this.editorInstance), + map(() => ({ + width: this.el.clientWidth, + height: this.el.clientHeight + })), + distinctUntilChanged((a, b) => a.width === b.width && a.height === b.height), + debounceTime(50) + ) + .subscribe(() => { + this.editorInstance.layout(); + }); + }); + } + + private setValue(): void { + if (!this.editorInstance) { + return; + } + + if (this.nzFullControl && this.value) { + warn(`should not set value when you are using full control mode! It would result in ambiguous data flow!`); + return; + } + + if (this.nzEditorMode === 'normal') { + if (this.modelSet) { + (this.editorInstance.getModel() as ITextModel).setValue(this.value); + } else { + (this.editorInstance as IEditor).setModel( + editor.createModel(this.value, (this.editorOptionCached as EditorOptions).language) + ); + this.modelSet = true; + } + } else { + if (this.modelSet) { + const model = (this.editorInstance as IDiffEditor).getModel()!; + model.modified.setValue(this.value); + model.original.setValue(this.nzOriginalText); + } else { + const language = (this.editorOptionCached as EditorOptions).language; + (this.editorInstance as IDiffEditor).setModel({ + original: editor.createModel(this.value, language), + modified: editor.createModel(this.nzOriginalText, language) + }); + } + } + } + + private setValueEmitter(): void { + const model = (this.nzEditorMode === 'normal' + ? (this.editorInstance as IEditor).getModel() + : (this.editorInstance as IDiffEditor).getModel()!.modified) as ITextModel; + + model.onDidChangeContent(() => { + this.emitValue(model.getValue()); + }); + } + + private emitValue(value: string): void { + this.value = value; + this.onChange(value); + } + + private updateOptionToMonaco(): void { + if (this.editorInstance) { + this.editorInstance.updateOptions({ ...this.editorOptionCached }); + } + } +} diff --git a/zeppelin-frontend/src/app/share/code-editor/code-editor.module.ts b/zeppelin-frontend/src/app/share/code-editor/code-editor.module.ts new file mode 100644 index 00000000000..d21ed183f89 --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/code-editor.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzSpinModule } from 'ng-zorro-antd/spin'; + +import { CodeEditorComponent } from './code-editor.component'; + +@NgModule({ + declarations: [CodeEditorComponent], + imports: [CommonModule, NzIconModule, NzSpinModule], + exports: [CodeEditorComponent] +}) +export class CodeEditorModule {} diff --git a/zeppelin-frontend/src/app/share/code-editor/code-editor.service.ts b/zeppelin-frontend/src/app/share/code-editor/code-editor.service.ts new file mode 100644 index 00000000000..4d9d5593400 --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/code-editor.service.ts @@ -0,0 +1,92 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; +import { of as observableOf, BehaviorSubject, Observable, Subject } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +import { + JoinedEditorOptions, + NzCodeEditorConfig, + NzCodeEditorLoadingStatus, + NZ_CODE_EDITOR_CONFIG +} from './nz-code-editor.definitions'; + +import { editor } from 'monaco-editor'; + +// tslint:disable no-any +function tryTriggerFunc(fn?: (...args: any[]) => any): (...args: any) => void { + return (...args: any[]) => { + if (fn) { + fn(...args); + } + }; +} +// tslint:enable no-any + +@Injectable({ + providedIn: 'root' +}) +export class CodeEditorService { + private document: Document; + private firstEditorInitialized = false; + private loaded$ = new Subject(); + private loadingStatus = NzCodeEditorLoadingStatus.UNLOAD; + private option: JoinedEditorOptions; + + option$ = new BehaviorSubject(this.option); + + constructor( + @Inject(NZ_CODE_EDITOR_CONFIG) private config: NzCodeEditorConfig, + @Inject(DOCUMENT) _document: any // tslint:disable-line no-any + ) { + this.document = _document; + this.option = this.config.defaultEditorOption || {}; + } + + // TODO: use config service later. + updateDefaultOption(option: JoinedEditorOptions): void { + this.option = { ...this.option, ...option }; + this.option$.next(this.option); + + if (option.theme) { + editor.setTheme(option.theme); + } + } + + requestToInit(): Observable { + if (this.loadingStatus === NzCodeEditorLoadingStatus.LOADED) { + this.onInit(); + return observableOf(this.getLatestOption()); + } + + if (this.loadingStatus === NzCodeEditorLoadingStatus.UNLOAD) { + this.loadingStatus = NzCodeEditorLoadingStatus.LOADED; + this.loaded$.next(true); + this.loaded$.complete(); + this.onLoad(); + this.onInit(); + return observableOf(this.getLatestOption()); + } + + return this.loaded$.asObservable().pipe( + tap(() => this.onInit()), + map(() => this.getLatestOption()) + ); + } + + private onInit(): void { + if (!this.firstEditorInitialized) { + this.firstEditorInitialized = true; + tryTriggerFunc(this.config.onFirstEditorInit)(); + } + + tryTriggerFunc(this.config.onInit)(); + } + + private onLoad(): void { + tryTriggerFunc(this.config.onLoad)(); + } + + private getLatestOption(): JoinedEditorOptions { + return { ...this.option }; + } +} diff --git a/zeppelin-frontend/src/app/share/code-editor/index.ts b/zeppelin-frontend/src/app/share/code-editor/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/zeppelin-frontend/src/app/share/code-editor/nz-code-editor.definitions.ts b/zeppelin-frontend/src/app/share/code-editor/nz-code-editor.definitions.ts new file mode 100644 index 00000000000..aca1425f121 --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/nz-code-editor.definitions.ts @@ -0,0 +1,34 @@ +import { InjectionToken } from '@angular/core'; +import { SafeUrl } from '@angular/platform-browser'; +import { editor } from 'monaco-editor'; +import IEditorConstructionOptions = editor.IEditorConstructionOptions; +import IDiffEditorConstructionOptions = editor.IDiffEditorConstructionOptions; + +export type EditorOptions = IEditorConstructionOptions; +export type DiffEditorOptions = IDiffEditorConstructionOptions; +export type JoinedEditorOptions = EditorOptions | DiffEditorOptions; + +export type NzEditorMode = 'normal' | 'diff'; + +export enum NzCodeEditorLoadingStatus { + UNLOAD = 'unload', + LOADING = 'loading', + LOADED = 'LOADED' +} + +export interface NzCodeEditorConfig { + assetsRoot?: string | SafeUrl; + defaultEditorOption?: JoinedEditorOptions; + onLoad?(): void; + onFirstEditorInit?(): void; + onInit?(): void; +} + +export const NZ_CODE_EDITOR_CONFIG = new InjectionToken('nz-code-editor-config', { + providedIn: 'root', + factory: NZ_CODE_EDITOR_CONFIG_FACTORY +}); + +export function NZ_CODE_EDITOR_CONFIG_FACTORY(): NzCodeEditorConfig { + return {}; +} diff --git a/zeppelin-frontend/src/app/share/code-editor/public-api.ts b/zeppelin-frontend/src/app/share/code-editor/public-api.ts new file mode 100644 index 00000000000..aaeb34e5844 --- /dev/null +++ b/zeppelin-frontend/src/app/share/code-editor/public-api.ts @@ -0,0 +1,4 @@ +export * from './nz-code-editor.definitions'; +export * from './code-editor.component'; +export * from './code-editor.module'; +export * from './code-editor.service'; diff --git a/zeppelin-frontend/src/app/share/share.module.ts b/zeppelin-frontend/src/app/share/share.module.ts index 3353aa65500..51d1e02ef2e 100644 --- a/zeppelin-frontend/src/app/share/share.module.ts +++ b/zeppelin-frontend/src/app/share/share.module.ts @@ -28,6 +28,7 @@ import { } from 'ng-zorro-antd'; import { AboutZeppelinComponent } from '@zeppelin/share/about-zeppelin/about-zeppelin.component'; +import { CodeEditorModule } from '@zeppelin/share/code-editor'; import { FolderRenameComponent } from '@zeppelin/share/folder-rename/folder-rename.component'; import { HeaderComponent } from '@zeppelin/share/header/header.component'; import { MathJaxDirective } from '@zeppelin/share/math-jax/math-jax.directive'; @@ -54,7 +55,7 @@ const PIPES = [HumanizeBytesPipe]; @NgModule({ declarations: [MODAL_LIST, EXPORT_LIST, PIPES, MathJaxDirective, RunScriptsDirective], entryComponents: [MODAL_LIST], - exports: [EXPORT_LIST, PIPES, MathJaxDirective, RunScriptsDirective], + exports: [EXPORT_LIST, PIPES, MathJaxDirective, RunScriptsDirective, CodeEditorModule], imports: [ FormsModule, CommonModule, @@ -80,7 +81,8 @@ const PIPES = [HumanizeBytesPipe]; NzUploadModule, NzSelectModule, NzAlertModule, - NzProgressModule + NzProgressModule, + CodeEditorModule ] }) export class ShareModule {} From f53ad54500b0decdfa4a436f5d2998ce5d1e010b Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Tue, 22 Oct 2019 19:48:19 +0800 Subject: [PATCH 07/19] docs: update frontend README --- zeppelin-frontend/README.md | 263 ++++++++++++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 13 deletions(-) diff --git a/zeppelin-frontend/README.md b/zeppelin-frontend/README.md index af201a3e63d..d53121821cc 100644 --- a/zeppelin-frontend/README.md +++ b/zeppelin-frontend/README.md @@ -1,27 +1,264 @@ -# Zeppelin +# Zeppelin Front-end -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.2. +Zeppelin notebooks front-end built with Angular. -## Development server +- Jira issue [ZEPPELIN-4321](https://issues.apache.org/jira/browse/ZEPPELIN-4321) +- Design Document: [Zeppelin Notebook Rework Proposal](https://docs.google.com/document/d/1z_VscS81Xwx_3QaexKB2s0uEMEuWKsPXh9mWFRq0-hY) -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +![screenshot](/screenshot.png?raw=true "Screenshot") -## Code scaffolding +## Setup -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. +### Prerequisites -## Build +- [Node.js](https://nodejs.org) version 10.9.0 or later or use [creationix/nvm](https://github.com/creationix/nvm). +- NPM package manager (which is installed with Node.js by default). +- [Angular CLI](https://angular.io/cli) version 8.3.0 or later. -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. +### Install -## Running unit tests +Run the `npm install` command to install dependencies in the project directory. + +### Start Zeppelin server + +[Run Zeppelin server](https://zeppelin.apache.org/contribution/contributions.html#run-zeppelin-server-in-development-mode) on `http://localhost:8080`. + +If you are using a custom port instead of the default(http://localhost:8080) or other network address, you can create `.env` file in the project directory and set `SERVER_PROXY`. + +*.env* + +``` +SERVER_PROXY=http://localhost:8080 +``` + +### Development server + +Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +### Build + +Run `npm build` to build the project. The build artifacts will be stored in the `dist/` directory. + +### Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). -## Running end-to-end tests +## Implementation Progress + +### Pages + +| Name | Route | Module | UI | +| --- | ----- | ---------- | -- | +| Home | `/` | HomeModule | Y | +| Login | `/login` | LoginModule | Y | +| Job Manager | `/jobmanager` | JobManagerModule | Y | +| Interpreter Setting | `/interpreter` | InterpreterModule | Y | +| Notebook | `/notebook/{id}` | NotebookModule | Y | +| Notebook Repos | `/notebookRepos` | | | +| Credential | `/credential` | | | +| Helium | `/helium` | | WIP | +| Configuration | `/configuration` | | | + +### Notebook Features + +| Feature | Note | Status | +| ------ | ---- | ---- | +| Files System | Create/ Rename/ Import etc. | Y | +| Toolbar Actions | The top toolbar actions | Y | + +### Paragraph Features + +| Feature | Note | Status | +| ------ | ---- | ---- | +| Grid layout and resizable | | Y | +| Code Editor | | Y | +| Actions | The Corresponding actions of the drop-down menu in the setting button | Y | +| Actions(hot-keys) | Support hot-keys for the actions | WIP | +| Publishable | [publish paragraphs](http://zeppelin.apache.org/docs/0.8.0/usage/other_features/publishing_paragraphs.html) | | +| Stream | | | + +### Result Display + +| Type | Status | +| ------ | ---- | +| Dynamic Form | Y | +| Text | Y | +| Html | Y | +| Table | Y | +| Network | | + +### Table Visualization + +| Type | State | +| ------ | ---- | +| Line Chart | Y | +| Bard Chart | Y | +| Pie Chart | Y | +| Area Chart | Y | +| Scatter Chart | Y | + +### Helium Visualization + +| Type | Note | Status | +| ------ | ---- | ---- | +| Prototype | To verify the implementable prototype | Y | +| Publish Dependencies | Just like [zeppelin-vis](https://github.com/apache/zeppelin/tree/master/zeppelin-web/src/app/visualization) | WIP | +| Example Projects | | Y | +| Development Documents | | WIP | + +## Contributing + +### Dev Mode + +Follow the [Setup](#Setup) steps to starting the frontend service. The app will automatically reload if you change any of the source files. + +### Technologies + +Zeppelin-Frontend-Next is using Angular as the main Framework, before developing we hope highly recommended to have a good knowledge of [Angular](https://angular.io/) and [RxJs](https://github.com/ReactiveX/rxjs). + +In addition: + +- We use [G2](https://github.com/antvis/g2) visualization +- We use [Lodash](https://lodash.com/) to process complex data +- We use [Monaco Editor](https://github.com/microsoft/monaco-editor) to make code editor + +### Coding style + +- We follow mainly the [Angular Style Guide](https://angular.io/guide/styleguide) +- We use a 2 spaces indentation +- We use single quotes + +But don't worry, TSLint and prettier will make you remember it for the most part. +Git hooks will automatically check and fix it when commit. + +### Folder Structure + +We follow mainly the [Workspace and project file structure](https://angular.io/guide/styleguide) to organize the folder structure and files. + +#### Src Folder Structure + +`src` folder contains the source code for Zeppelin-Frontend-Next. + +``` +├── app +│ ├── core +│ │ └── message-listener # handle WebSocket message +│ ├── interfaces # interfaces +│ ├── pages +│ │ ├── login # login module +│ │ └── workspace +│ │ ├── home # welcome module +│ │ ├── interpreter # interpreter settings +│ │ ├── job-manager # job manager module +│ │ └── notebook # notebook module +│ │ ├── action-bar # notebook settings +│ │ ├── interpreter-binding # interpreter binding +│ │ ├── permissions # permissions +│ │ └── paragraph # paragraph module +│ │ ├── code-editor # code editor module +│ │ ├── control # paragraph controls +│ │ ├── dynamic-forms # dynamic forms +│ │ └── result # display result +│ ├── sdk # Zeppelin API Frontend SDK +│ ├── share # Share Components +│ ├── services # API Service +│ └── visualization +│ ├── area-chart # Area Chart Component +│ ├── bar-chart # Bar Chart Component +│ ├── line-chart # Line Chart Component +│ ├── pie-chart # Pie Chart Component +│ ├── scatter-chart # Scatter Chart Component +│ └── table # Data Table Component +├── assets # Assets +└── styles + └── theme # Theme Files + ├── dark + └── light +``` + +#### Import Path Rules + +We specify path mapping in the `tsconfig.json` file to get a clear import path. + +So please follow the rules following: + +- Add `public-api.ts` and `index.ts` to the folder where want to export the modules +- `public-api.ts` File only included you wish to export modules +- `index.ts` File only export `./public-api.ts` +- Use relative paths instead of mapped paths when the same level to prevent circular references + +### Good Practices + +The following guide for this project only. Most of the time you only need to follow Angular's guide. + +#### Change Detection Strategy + +Use [OnPush](https://angular.io/api/core/ChangeDetectionStrategy#OnPush) as the change detection strategy for components. + +#### WebSocket Listen and Send + +*Send Message*: Inject the `MessageService` and then use its instance methods. + +```ts + +import { MessageService } from '@zeppelin/services'; + +export class SomeComponent { + + constructor(public messageService: MessageService) { } + + fun() { + // Do something + this.messageService.listNoteJobs(); + } +} +``` + +*Listen to Message* + +Make sure the class extends from `MessageListenersManager` and inject the `MessageService` and ensures that it is public. + +After that, you can use the `@MessageListener` decorator to decorate the corresponding message method. + +```ts +import { MessageListener, MessageListenersManager } from '@zeppelin/core'; +import { MessageService } from '@zeppelin/services'; +import { OP, ListNoteJobs } from '@zeppelin/sdk'; + +export class SomeComponent extends MessageListenersManager { + + constructor(public messageService: MessageService) { } + + @MessageListener(OP.LIST_NOTE_JOBS) + fun(data: ListNoteJobs) { + // Do something + } +} +``` + +#### Theming + +Use we provide the function to wrap component styles to implement theming. You can find the theme variables in the `src/styles/theme/` folder. + +```less +@import "theme-mixin"; + +.themeMixin({ + // component styles +}); +``` -Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +#### Imports order -## Further help +Follow of the following imports order: -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). +```ts +import * from '@angular/*' // Angular modules +import * from 'rxjs/*' // Rxjs modules +// BLANK LINE +import * from '*' // Other third party modules +// BLANK LINE +import * from '@zeppelin/*' // This project modules +// BLANK LINE +import * from './*' // Same level modules +``` \ No newline at end of file From 03ff8d58ac7ea419dea99e27efcc3f169c620fe6 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Wed, 23 Oct 2019 17:37:33 +0800 Subject: [PATCH 08/19] fix: code editor language highlight --- zeppelin-frontend/src/app/app.module.ts | 2 +- .../src/app/helium-manager/helium-manager.service.ts | 3 ++- zeppelin-frontend/src/app/languages/load.ts | 7 ++++--- .../notebook/paragraph/result/result.component.html | 2 +- .../notebook/paragraph/result/result.component.less | 7 +++++++ .../common/pivot-setting/pivot-setting.component.less | 3 +++ .../common/scatter-setting/scatter-setting.component.less | 3 +++ 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/zeppelin-frontend/src/app/app.module.ts b/zeppelin-frontend/src/app/app.module.ts index 8f7300453be..15e2e87f8e5 100644 --- a/zeppelin-frontend/src/app/app.module.ts +++ b/zeppelin-frontend/src/app/app.module.ts @@ -9,13 +9,13 @@ import { Router, RouterModule } from '@angular/router'; import { ZeppelinHeliumModule } from '@zeppelin/helium'; import { en_US, NzModalService, NzNotificationService, NZ_I18N } from 'ng-zorro-antd'; -import { NZ_CODE_EDITOR_CONFIG } from 'ng-zorro-antd/code-editor'; import { MESSAGE_INTERCEPTOR, TRASH_FOLDER_ID_TOKEN } from '@zeppelin/interfaces'; import { loadMonacoLanguage } from '@zeppelin/languages'; import { TicketService } from '@zeppelin/services'; import { ShareModule } from '@zeppelin/share'; +import { NZ_CODE_EDITOR_CONFIG } from '@zeppelin/share/code-editor'; import { AppHttpInterceptor } from './app-http.interceptor'; import { AppMessageInterceptor } from './app-message.interceptor'; import { AppRoutingModule } from './app-routing.module'; diff --git a/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts index 9f2d319f5d3..2342f97412a 100644 --- a/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts +++ b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts @@ -35,7 +35,8 @@ export class HeliumManagerService implements OnDestroy { } getEnabledPackages() { - return of(['helium-vis-example']); + // return of(['helium-vis-example']); + return of([]); } packagesLoadChange() { diff --git a/zeppelin-frontend/src/app/languages/load.ts b/zeppelin-frontend/src/app/languages/load.ts index 8d1d7c25206..92ae43a99af 100644 --- a/zeppelin-frontend/src/app/languages/load.ts +++ b/zeppelin-frontend/src/app/languages/load.ts @@ -1,7 +1,8 @@ +import { languages } from 'monaco-editor'; import { conf as ScalaConf, language as ScalaLanguage } from './scala'; export const loadMonacoLanguage = () => { - monaco.languages.register({ id: 'scala' }); - monaco.languages.setMonarchTokensProvider('scala', ScalaLanguage); - monaco.languages.setLanguageConfiguration('scala', ScalaConf); + languages.register({ id: 'scala' }); + languages.setMonarchTokensProvider('scala', ScalaLanguage); + languages.setLanguageConfiguration('scala', ScalaConf); }; diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html index b8cfe836a01..e6f4e729642 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.html @@ -14,7 +14,7 @@ - diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less index bf8758c1558..55401522871 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/result/result.component.less @@ -44,6 +44,13 @@ .export-dropdown { margin: 0 20px; + + .export-dropdown-icon-btn { + width: 32px; + height: 32px; + padding: 0; + font-size: 16px; + } } .setting-trigger { diff --git a/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less index 2de808c2c0a..5d0aa751b77 100644 --- a/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less +++ b/zeppelin-frontend/src/app/visualizations/common/pivot-setting/pivot-setting.component.less @@ -9,4 +9,7 @@ .drag-wrap { min-height: 23px; } + nz-card { + background: #fff; + } }); diff --git a/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less index b3f7094ace6..e30494d542a 100644 --- a/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less +++ b/zeppelin-frontend/src/app/visualizations/common/scatter-setting/scatter-setting.component.less @@ -9,4 +9,7 @@ .drag-wrap { min-height: 23px; } + nz-card { + background: #fff; + } }); From 10f7c78e00781fe902680bc337736bbe0b133003 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Wed, 23 Oct 2019 17:39:46 +0800 Subject: [PATCH 09/19] chore: remove commit-lint --- zeppelin-frontend/commitlint.config.js | 13 ------------- zeppelin-frontend/package.json | 4 ---- 2 files changed, 17 deletions(-) delete mode 100644 zeppelin-frontend/commitlint.config.js diff --git a/zeppelin-frontend/commitlint.config.js b/zeppelin-frontend/commitlint.config.js deleted file mode 100644 index 984584dd970..00000000000 --- a/zeppelin-frontend/commitlint.config.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], - rules: { - 'body-max-line-length': [1, 'always', 100], - 'header-case': [1, 'always', 'kebab-case'], - 'scope-case': [1, 'always', 'kebab-case'], - 'type-enum': [ - 2, - 'always', - ['build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'release', 'style', 'test', 'chore', 'revert'] - ] - } -}; diff --git a/zeppelin-frontend/package.json b/zeppelin-frontend/package.json index 109409d4ae9..a367e1d8867 100644 --- a/zeppelin-frontend/package.json +++ b/zeppelin-frontend/package.json @@ -28,9 +28,6 @@ "@angular/router": "~8.2.10", "@antv/data-set": "^0.10.2", "@antv/g2": "^3.5.4", - "@commitlint/cli": "^7.5.2", - "@commitlint/config-conventional": "^7.5.0", - "@commitlint/prompt-cli": "^7.5.0", "ansi-to-html": "^0.6.11", "core-js": "^2.5.4", "date-fns": "^1.30.1", @@ -93,7 +90,6 @@ }, "husky": { "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", "pre-commit": "lint-staged" } } From 8a07902ade3994580df376bd1e907af62f9aba40 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Wed, 23 Oct 2019 18:49:04 +0800 Subject: [PATCH 10/19] chore: update theme variables --- .../job-manager/job-manager.component.html | 1 + .../job-manager/job-manager.component.ts | 2 + .../job-manager/job-manager.module.ts | 2 + .../job-manager/job/job.component.html | 2 +- .../job-manager/job/job.component.ts | 2 + zeppelin-frontend/src/styles/global.less | 4 + .../src/styles/theme/dark/theme-dark.less | 88 +++++++++++-------- .../src/styles/theme/light/theme-light.less | 56 ++++++++++-- 8 files changed, 111 insertions(+), 46 deletions(-) diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html index f85373ebe04..2b43ec6e245 100644 --- a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.html @@ -54,6 +54,7 @@ diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts index 2143109612d..a48bd18b3d1 100644 --- a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.component.ts @@ -30,6 +30,7 @@ export class JobManagerComponent extends MessageListenersManager implements OnIn sortKeys = Object.keys(JobDateSortKeys).map(k => JobDateSortKeys[k]); interpreters: string[] = []; filteredJobs: JobsItem[] = []; + filterString: string = ''; jobs: JobsItem[] = []; loading = true; @@ -61,6 +62,7 @@ export class JobManagerComponent extends MessageListenersManager implements OnIn filterJobs() { const filterData = this.form.getRawValue() as FilterForm; + this.filterString = filterData.noteName; const isSortByAsc = filterData.sortBy === JobDateSortKeys.OLDEST_UPDATED; this.filteredJobs = this.jobs .filter(job => { diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts index b89a5bfc244..2a4245c8751 100644 --- a/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job-manager.module.ts @@ -11,6 +11,7 @@ import { NzEmptyModule, NzFormModule, NzGridModule, + NzHighlightModule, NzIconModule, NzInputModule, NzModalModule, @@ -36,6 +37,7 @@ const icons: IconDefinition[] = [SearchOutline, FileOutline, FileUnknownOutline, CommonModule, FormsModule, ReactiveFormsModule, + NzHighlightModule, ShareModule, NzIconModule, NzInputModule, diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html index d69dd173859..86bd0f70313 100644 --- a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.html @@ -5,7 +5,7 @@ [nzType]="icon" nzTheme="outline">
- {{note.noteName}} - + - {{note.interpreter || 'interpreter is not set'}} diff --git a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts index c257b8327f4..6016c928bea 100644 --- a/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/job-manager/job/job.component.ts @@ -21,8 +21,10 @@ import { JobsItem, JobStatus } from '@zeppelin/sdk'; }) export class JobManagerJobComponent implements OnInit, OnChanges { @Input() note: JobsItem; + @Input() highlight: string | null = null; @Output() readonly start = new EventEmitter(); @Output() readonly stop = new EventEmitter(); + icon = 'file'; relativeTime = ''; progress = 0; diff --git a/zeppelin-frontend/src/styles/global.less b/zeppelin-frontend/src/styles/global.less index 29042aba826..3c191e606f6 100644 --- a/zeppelin-frontend/src/styles/global.less +++ b/zeppelin-frontend/src/styles/global.less @@ -1,5 +1,9 @@ @import '../../node_modules/ng-zorro-antd/src/style/color/colors'; +.mark-highlight { + color: crimson; +} + .tips { &.warning { color: @volcano-6; diff --git a/zeppelin-frontend/src/styles/theme/dark/theme-dark.less b/zeppelin-frontend/src/styles/theme/dark/theme-dark.less index 4d06ba060b4..e58c869d045 100644 --- a/zeppelin-frontend/src/styles/theme/dark/theme-dark.less +++ b/zeppelin-frontend/src/styles/theme/dark/theme-dark.less @@ -175,6 +175,10 @@ @btn-circle-size-lg: @btn-height-lg; @btn-circle-size-sm: @btn-height-sm; +@btn-square-size: @btn-height-base; +@btn-square-size-lg: @btn-height-lg; +@btn-square-size-sm: @btn-height-sm; + @btn-group-border: @primary-5; // Checkbox @@ -183,6 +187,9 @@ @checkbox-check-color: #fff; @checkbox-border-width: @border-width-base; +// Descriptions +@descriptions-bg: #fafafa; + // Empty @empty-font-size: @font-size-base; @@ -298,6 +305,9 @@ @input-border-color: @border-color-base; @input-bg: #fff; @input-number-handler-active-bg: #f4f4f4; +@input-number-handler-hover-bg: @primary-5; +@input-number-handler-bg: @component-background; +@input-number-handler-border-color: @border-color-base; @input-addon-bg: @background-color-light; @input-hover-border-color: @primary-color; @input-disabled-bg: @disabled-bg; @@ -307,6 +317,13 @@ // --- @select-border-color: @border-color-base; @select-item-selected-font-weight: 600; +@select-dropdown-bg: @component-background; +@select-item-selected-bg: @background-color-light; +@select-item-active-bg: @item-active-bg; + +// Anchor +// --- +@anchor-border-color: @border-color-split; // Tooltip // --- @@ -345,8 +362,9 @@ // -- @modal-body-padding: 24px; @modal-header-bg: @component-background; -@modal-footer-bg: tranparent; -@modal-mask-bg: fade(@black, 65%); +@modal-footer-bg: transparent; +@modal-footer-border-color-split: @border-color-split; +@modal-mask-bg: fade(@black, 45%); // Progress // -- @@ -366,6 +384,18 @@ @menu-item-active-bg: @item-active-bg; @menu-item-active-border-width: 3px; @menu-item-group-title-color: @text-color-secondary; +@menu-icon-size: @font-size-base; +@menu-icon-size-lg: @font-size-lg; + +@menu-item-vertical-margin: 4px; +@menu-item-font-size: @font-size-base; +@menu-item-boundary-margin: 8px; +@menu-icon-size: @font-size-base; +@menu-icon-size-lg: @font-size-lg; +@menu-dark-selected-item-icon-color: @white; +@menu-dark-selected-item-text-color: @white; +@dark-menu-item-hover-bg: transparent; + // dark theme @menu-dark-color: @text-color-secondary-dark; @menu-dark-bg: @layout-header-background; @@ -373,6 +403,9 @@ @menu-dark-submenu-bg: #000c17; @menu-dark-highlight-color: #fff; @menu-dark-item-active-bg: @primary-color; +@menu-dark-selected-item-icon-color: @white; +@menu-dark-selected-item-text-color: @white; +@menu-dark-item-hover-bg: transparent; // Spin // --- @@ -392,6 +425,8 @@ @table-padding-vertical: 16px; @table-padding-horizontal: 16px; @table-border-radius-base: @border-radius-base; +@table-footer-bg: @background-color-light; +@table-footer-color: @heading-color; // Tag // -- @@ -433,7 +468,8 @@ @card-inner-head-padding: 12px; @card-padding-base: 24px; @card-actions-background: @background-color-light; -@card-background: #cfd8dc; +@card-skeleton-bg: #cfd8dc; +@card-background: @component-background; @card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); @card-radius: @border-radius-sm; @@ -504,8 +540,10 @@ // PageHeader // --- -@page-header-padding-horizontal: 24px; -@page-header-padding-vertical: 16px; +@page-header-padding: 24px; +@page-header-padding-vertical: 16px; @page-header-padding-vertical: 16px; +@page-header-padding-breadcrumb: 12px; +@page-header-back-color: #000; // Breadcrumb // --- @@ -541,6 +579,8 @@ @tree-child-padding: 18px; @tree-directory-selected-color: #fff; @tree-directory-selected-bg: @primary-color; +@tree-node-hover-bg: @item-hover-bg; +@tree-node-selected-bg: @primary-2; // Collapse // --- @@ -595,43 +635,13 @@ @statistic-title-font-size: @font-size-base; @statistic-content-font-size: 24px; @statistic-unit-font-size: 16px; -@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family; +@statistic-font-family: @font-family; // Drawer // --- @drawer-header-padding: 16px 24px; @drawer-body-padding: 24px; - - -@layout-body-background: #171717; -@background-color-base: #262626; -@body-background: #404041; -@layout-sider-background: #001529; -@component-background: #262626; -@input-bg: #313133; -@btn-default-bg: #262626; -@border-color-base: #1e1e1e; -@border-color-split: #363636; -@heading-color: #E3E3E3; -@text-color: #E3E3E3; -@text-color-secondary: fade(#fff, 65%); -@table-selected-row-bg: #3a3a3a; -@table-expanded-row-bg: #3b3b3b; -@table-header-bg: #3a3a3b; -@table-row-hover-bg: #3a3a3b; -@layout-trigger-color: fade(#fff, 80%); -@layout-trigger-background: #313232; -@alert-message-color: fade(#000, 67%); -@item-hover-bg: fade(@blue-5, 20%); -@item-active-bg: fade(@blue-5, 40%); -@disabled-color: rgba(255, 255, 255, 0.25); -@tag-default-bg: #262628; -@popover-bg: #262629; -@wait-icon-color: fade(#fff, 64%); -@background-color-light: fade(@blue-5, 40%); -@collapse-header-bg: #262629; -@info-color: #313133; -@primary-color: @blue-7; -@highlight-color: @red-7; -@warning-color: @gold-9; +// Typography +// --- +@typography-title-font-weight: 600; diff --git a/zeppelin-frontend/src/styles/theme/light/theme-light.less b/zeppelin-frontend/src/styles/theme/light/theme-light.less index 2dc2b48ce06..a59eeb903dc 100644 --- a/zeppelin-frontend/src/styles/theme/light/theme-light.less +++ b/zeppelin-frontend/src/styles/theme/light/theme-light.less @@ -175,6 +175,10 @@ @btn-circle-size-lg: @btn-height-lg; @btn-circle-size-sm: @btn-height-sm; +@btn-square-size: @btn-height-base; +@btn-square-size-lg: @btn-height-lg; +@btn-square-size-sm: @btn-height-sm; + @btn-group-border: @primary-5; // Checkbox @@ -183,6 +187,9 @@ @checkbox-check-color: #fff; @checkbox-border-width: @border-width-base; +// Descriptions +@descriptions-bg: #fafafa; + // Empty @empty-font-size: @font-size-base; @@ -298,6 +305,9 @@ @input-border-color: @border-color-base; @input-bg: #fff; @input-number-handler-active-bg: #f4f4f4; +@input-number-handler-hover-bg: @primary-5; +@input-number-handler-bg: @component-background; +@input-number-handler-border-color: @border-color-base; @input-addon-bg: @background-color-light; @input-hover-border-color: @primary-color; @input-disabled-bg: @disabled-bg; @@ -307,6 +317,13 @@ // --- @select-border-color: @border-color-base; @select-item-selected-font-weight: 600; +@select-dropdown-bg: @component-background; +@select-item-selected-bg: @background-color-light; +@select-item-active-bg: @item-active-bg; + +// Anchor +// --- +@anchor-border-color: @border-color-split; // Tooltip // --- @@ -345,8 +362,9 @@ // -- @modal-body-padding: 24px; @modal-header-bg: @component-background; -@modal-footer-bg: tranparent; -@modal-mask-bg: fade(@black, 65%); +@modal-footer-bg: transparent; +@modal-footer-border-color-split: @border-color-split; +@modal-mask-bg: fade(@black, 45%); // Progress // -- @@ -366,6 +384,18 @@ @menu-item-active-bg: @item-active-bg; @menu-item-active-border-width: 3px; @menu-item-group-title-color: @text-color-secondary; +@menu-icon-size: @font-size-base; +@menu-icon-size-lg: @font-size-lg; + +@menu-item-vertical-margin: 4px; +@menu-item-font-size: @font-size-base; +@menu-item-boundary-margin: 8px; +@menu-icon-size: @font-size-base; +@menu-icon-size-lg: @font-size-lg; +@menu-dark-selected-item-icon-color: @white; +@menu-dark-selected-item-text-color: @white; +@dark-menu-item-hover-bg: transparent; + // dark theme @menu-dark-color: @text-color-secondary-dark; @menu-dark-bg: @layout-header-background; @@ -373,6 +403,9 @@ @menu-dark-submenu-bg: #000c17; @menu-dark-highlight-color: #fff; @menu-dark-item-active-bg: @primary-color; +@menu-dark-selected-item-icon-color: @white; +@menu-dark-selected-item-text-color: @white; +@menu-dark-item-hover-bg: transparent; // Spin // --- @@ -392,6 +425,8 @@ @table-padding-vertical: 16px; @table-padding-horizontal: 16px; @table-border-radius-base: @border-radius-base; +@table-footer-bg: @background-color-light; +@table-footer-color: @heading-color; // Tag // -- @@ -433,7 +468,8 @@ @card-inner-head-padding: 12px; @card-padding-base: 24px; @card-actions-background: @background-color-light; -@card-background: #cfd8dc; +@card-skeleton-bg: #cfd8dc; +@card-background: @component-background; @card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); @card-radius: @border-radius-sm; @@ -504,8 +540,10 @@ // PageHeader // --- -@page-header-padding-horizontal: 24px; -@page-header-padding-vertical: 16px; +@page-header-padding: 24px; +@page-header-padding-vertical: 16px; @page-header-padding-vertical: 16px; +@page-header-padding-breadcrumb: 12px; +@page-header-back-color: #000; // Breadcrumb // --- @@ -541,6 +579,8 @@ @tree-child-padding: 18px; @tree-directory-selected-color: #fff; @tree-directory-selected-bg: @primary-color; +@tree-node-hover-bg: @item-hover-bg; +@tree-node-selected-bg: @primary-2; // Collapse // --- @@ -595,9 +635,13 @@ @statistic-title-font-size: @font-size-base; @statistic-content-font-size: 24px; @statistic-unit-font-size: 16px; -@statistic-font-family: Tahoma, 'Helvetica Neue', @font-family; +@statistic-font-family: @font-family; // Drawer // --- @drawer-header-padding: 16px 24px; @drawer-body-padding: 24px; + +// Typography +// --- +@typography-title-font-weight: 600; From 56493067622787996f76773828e6462050154f15 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Thu, 24 Oct 2019 10:23:47 +0800 Subject: [PATCH 11/19] chore: add license header --- .../src/json-vis.component.ts | 12 ++++++++++ .../helium-vis-example/src/json-vis.module.ts | 12 ++++++++++ .../src/json-visualization.ts | 12 ++++++++++ .../helium-vis-example/src/public-api.ts | 12 ++++++++++ .../projects/helium-vis-example/src/test.ts | 12 ++++++++++ .../zeppelin-helium/src/common-deps.ts | 12 ++++++++++ .../projects/zeppelin-helium/src/index.ts | 12 ++++++++++ .../zeppelin-helium/src/public-api.ts | 12 ++++++++++ .../projects/zeppelin-helium/src/test.ts | 12 ++++++++++ .../src/zeppelin-helium.module.ts | 12 ++++++++++ .../src/zeppelin-helium.service.ts | 12 ++++++++++ .../projects/zeppelin-sdk/src/index.ts | 12 ++++++++++ .../zeppelin-sdk/src/interfaces/index.ts | 12 ++++++++++ .../interfaces/message-common.interface.ts | 12 ++++++++++ .../message-data-type-map.interface.ts | 12 ++++++++++ .../message-interpreter.interface.ts | 12 ++++++++++ .../src/interfaces/message-job.interface.ts | 12 ++++++++++ .../interfaces/message-notebook.interface.ts | 12 ++++++++++ .../interfaces/message-operator.interface.ts | 12 ++++++++++ .../interfaces/message-paragraph.interface.ts | 12 ++++++++++ .../zeppelin-sdk/src/interfaces/public-api.ts | 12 ++++++++++ .../interfaces/websocket-message.interface.ts | 12 ++++++++++ .../projects/zeppelin-sdk/src/message.ts | 12 ++++++++++ .../projects/zeppelin-sdk/src/public-api.ts | 12 ++++++++++ .../zeppelin-visualization/src/data-set.ts | 12 ++++++++++ .../src/g2-visualization-base.ts | 12 ++++++++++ .../src/g2-visualization-component-base.ts | 12 ++++++++++ .../zeppelin-visualization/src/index.ts | 12 ++++++++++ .../src/pivot-transformation.ts | 12 ++++++++++ .../zeppelin-visualization/src/public-api.ts | 12 ++++++++++ .../zeppelin-visualization/src/table-data.ts | 12 ++++++++++ .../src/table-transformation.ts | 12 ++++++++++ .../src/transformation.ts | 12 ++++++++++ .../src/visualization-component-portal.ts | 12 ++++++++++ .../src/visualization.ts | 12 ++++++++++ .../src/app/app-http.interceptor.ts | 12 ++++++++++ .../src/app/app-message.interceptor.ts | 12 ++++++++++ .../src/app/app-routing.module.ts | 12 ++++++++++ .../src/app/app-runtime-compiler.providers.ts | 12 ++++++++++ zeppelin-frontend/src/app/app.component.html | 12 ++++++++++ zeppelin-frontend/src/app/app.component.less | 12 ++++++++++ .../src/app/app.component.spec.ts | 22 +++++++++++++------ zeppelin-frontend/src/app/app.component.ts | 12 ++++++++++ zeppelin-frontend/src/app/app.module.ts | 12 ++++++++++ .../core/copy-text/copy-text-to-clipboard.ts | 12 ++++++++++ .../src/app/core/copy-text/index.ts | 12 ++++++++++ .../src/app/core/copy-text/public-api.ts | 12 ++++++++++ .../destroy-hook/destroy-hook.component.ts | 12 ++++++++++ .../src/app/core/destroy-hook/index.ts | 12 ++++++++++ .../src/app/core/destroy-hook/public-api.ts | 12 ++++++++++ zeppelin-frontend/src/app/core/index.ts | 12 ++++++++++ .../src/app/core/message-listener/index.ts | 12 ++++++++++ .../core/message-listener/message-listener.ts | 12 ++++++++++ .../app/core/message-listener/public-api.ts | 12 ++++++++++ zeppelin-frontend/src/app/core/public-api.ts | 12 ++++++++++ .../helium-manager/helium-manager.module.ts | 12 ++++++++++ .../helium-manager/helium-manager.service.ts | 12 ++++++++++ .../src/app/helium-manager/index.ts | 12 ++++++++++ .../src/app/helium-manager/public-api.ts | 12 ++++++++++ zeppelin-frontend/src/app/interfaces/index.ts | 12 ++++++++++ .../src/app/interfaces/interpreter.ts | 12 ++++++++++ .../src/app/interfaces/message-interceptor.ts | 12 ++++++++++ .../src/app/interfaces/node-list.ts | 12 ++++++++++ .../src/app/interfaces/public-api.ts | 12 ++++++++++ .../src/app/interfaces/security.ts | 12 ++++++++++ .../src/app/interfaces/ticket.ts | 12 ++++++++++ .../src/app/interfaces/trash-folder-id.ts | 12 ++++++++++ zeppelin-frontend/src/app/languages/index.ts | 12 ++++++++++ zeppelin-frontend/src/app/languages/load.ts | 12 ++++++++++ .../src/app/languages/public-api.ts | 12 ++++++++++ zeppelin-frontend/src/app/languages/scala.ts | 15 +++++++++---- .../app/pages/login/login-routing.module.ts | 12 ++++++++++ .../src/app/pages/login/login.component.html | 12 ++++++++++ .../src/app/pages/login/login.component.less | 12 ++++++++++ .../src/app/pages/login/login.component.ts | 12 ++++++++++ .../src/app/pages/login/login.module.ts | 12 ++++++++++ .../workspace/home/home-routing.module.ts | 12 ++++++++++ .../pages/workspace/home/home.component.html | 12 ++++++++++ .../pages/workspace/home/home.component.less | 12 ++++++++++ .../pages/workspace/home/home.component.ts | 12 ++++++++++ .../app/pages/workspace/home/home.module.ts | 12 ++++++++++ .../create-repository-modal.component.html | 12 ++++++++++ .../create-repository-modal.component.less | 12 ++++++++++ .../create-repository-modal.component.ts | 12 ++++++++++ .../interpreter/interpreter-routing.module.ts | 12 ++++++++++ .../interpreter/interpreter.component.html | 12 ++++++++++ .../interpreter/interpreter.component.less | 12 ++++++++++ .../interpreter/interpreter.component.ts | 12 ++++++++++ .../interpreter/interpreter.module.ts | 12 ++++++++++ .../interpreter/item/item.component.html | 12 ++++++++++ .../interpreter/item/item.component.less | 12 ++++++++++ .../interpreter/item/item.component.ts | 12 ++++++++++ .../job-manager/job-manager-routing.module.ts | 12 ++++++++++ .../job-manager/job-manager.component.html | 12 ++++++++++ .../job-manager/job-manager.component.less | 12 ++++++++++ .../job-manager/job-manager.component.ts | 12 ++++++++++ .../job-manager/job-manager.module.ts | 12 ++++++++++ .../job-status/job-status.component.html | 12 ++++++++++ .../job-status/job-status.component.less | 12 ++++++++++ .../job-status/job-status.component.ts | 12 ++++++++++ .../job-manager/job/job.component.html | 12 ++++++++++ .../job-manager/job/job.component.less | 12 ++++++++++ .../job-manager/job/job.component.ts | 12 ++++++++++ .../action-bar/action-bar.component.html | 12 ++++++++++ .../action-bar/action-bar.component.less | 12 ++++++++++ .../action-bar/action-bar.component.ts | 12 ++++++++++ .../add-paragraph.component.html | 12 ++++++++++ .../add-paragraph.component.less | 12 ++++++++++ .../add-paragraph/add-paragraph.component.ts | 12 ++++++++++ .../interpreter-binding.component.html | 12 ++++++++++ .../interpreter-binding.component.less | 12 ++++++++++ .../interpreter-binding.component.ts | 12 ++++++++++ .../notebook/notebook-routing.module.ts | 12 ++++++++++ .../notebook/notebook.component.html | 12 ++++++++++ .../notebook/notebook.component.less | 12 ++++++++++ .../workspace/notebook/notebook.component.ts | 12 ++++++++++ .../workspace/notebook/notebook.module.ts | 12 ++++++++++ .../code-editor/code-editor.component.html | 12 ++++++++++ .../code-editor/code-editor.component.less | 12 ++++++++++ .../code-editor/code-editor.component.ts | 12 ++++++++++ .../paragraph/control/control.component.html | 12 ++++++++++ .../paragraph/control/control.component.less | 12 ++++++++++ .../paragraph/control/control.component.ts | 12 ++++++++++ .../dynamic-forms.component.html | 12 ++++++++++ .../dynamic-forms.component.less | 12 ++++++++++ .../dynamic-forms/dynamic-forms.component.ts | 12 ++++++++++ .../paragraph/footer/footer.component.html | 12 ++++++++++ .../paragraph/footer/footer.component.less | 12 ++++++++++ .../paragraph/footer/footer.component.ts | 12 ++++++++++ .../paragraph/paragraph.component.html | 12 ++++++++++ .../paragraph/paragraph.component.less | 12 ++++++++++ .../notebook/paragraph/paragraph.component.ts | 12 ++++++++++ .../progress/progress.component.html | 12 ++++++++++ .../progress/progress.component.less | 12 ++++++++++ .../paragraph/progress/progress.component.ts | 12 ++++++++++ .../paragraph/result/result.component.html | 12 ++++++++++ .../paragraph/result/result.component.less | 12 ++++++++++ .../paragraph/result/result.component.ts | 12 ++++++++++ .../permissions/permissions.component.html | 12 ++++++++++ .../permissions/permissions.component.less | 12 ++++++++++ .../permissions/permissions.component.ts | 12 ++++++++++ .../revisions-comparator.component.html | 12 ++++++++++ .../revisions-comparator.component.less | 12 ++++++++++ .../revisions-comparator.component.ts | 12 ++++++++++ .../elastic-input.component.html | 12 ++++++++++ .../elastic-input.component.less | 12 ++++++++++ .../elastic-input/elastic-input.component.ts | 12 ++++++++++ .../workspace/notebook/share/share.module.ts | 12 ++++++++++ .../workspace/workspace-routing.module.ts | 12 ++++++++++ .../pages/workspace/workspace.component.html | 12 ++++++++++ .../pages/workspace/workspace.component.less | 12 ++++++++++ .../pages/workspace/workspace.component.ts | 12 ++++++++++ .../app/pages/workspace/workspace.guard.ts | 12 ++++++++++ .../app/pages/workspace/workspace.module.ts | 12 ++++++++++ .../app/services/array-ordering.service.ts | 12 ++++++++++ .../src/app/services/base-rest.ts | 12 ++++++++++ .../src/app/services/base-url.service.ts | 12 ++++++++++ .../src/app/services/completion.service.ts | 12 ++++++++++ .../src/app/services/helium.service.ts | 12 ++++++++++ zeppelin-frontend/src/app/services/index.ts | 12 ++++++++++ .../src/app/services/interpreter.service.ts | 12 ++++++++++ .../src/app/services/job-manager.service.ts | 12 ++++++++++ .../src/app/services/message.service.ts | 12 ++++++++++ .../src/app/services/ng-z.service.ts | 12 ++++++++++ .../src/app/services/note-action.service.ts | 12 ++++++++++ .../src/app/services/note-list.service.ts | 12 ++++++++++ .../src/app/services/note-status.service.ts | 12 ++++++++++ .../app/services/note-var-share.service.ts | 12 ++++++++++ .../src/app/services/public-api.ts | 12 ++++++++++ .../app/services/runtime-compiler.service.ts | 12 ++++++++++ .../src/app/services/save-as.service.ts | 12 ++++++++++ .../src/app/services/security.service.ts | 12 ++++++++++ .../src/app/services/ticket.service.ts | 12 ++++++++++ .../about-zeppelin.component.html | 12 ++++++++++ .../about-zeppelin.component.less | 12 ++++++++++ .../about-zeppelin.component.ts | 12 ++++++++++ .../code-editor/code-editor.component.html | 12 ++++++++++ .../code-editor/code-editor.component.ts | 12 ++++++++++ .../share/code-editor/code-editor.module.ts | 12 ++++++++++ .../share/code-editor/code-editor.service.ts | 12 ++++++++++ .../src/app/share/code-editor/index.ts | 12 ++++++++++ .../code-editor/nz-code-editor.definitions.ts | 12 ++++++++++ .../src/app/share/code-editor/public-api.ts | 12 ++++++++++ .../folder-rename.component.html | 12 ++++++++++ .../folder-rename.component.less | 12 ++++++++++ .../folder-rename/folder-rename.component.ts | 12 ++++++++++ .../app/share/header/header.component.html | 12 ++++++++++ .../app/share/header/header.component.less | 12 ++++++++++ .../src/app/share/header/header.component.ts | 12 ++++++++++ zeppelin-frontend/src/app/share/index.ts | 12 ++++++++++ .../app/share/math-jax/math-jax.directive.ts | 12 ++++++++++ .../share/node-list/node-list.component.html | 12 ++++++++++ .../share/node-list/node-list.component.less | 12 ++++++++++ .../share/node-list/node-list.component.ts | 12 ++++++++++ .../note-create/note-create.component.html | 12 ++++++++++ .../note-create/note-create.component.less | 12 ++++++++++ .../note-create/note-create.component.ts | 12 ++++++++++ .../note-import/note-import.component.html | 12 ++++++++++ .../note-import/note-import.component.less | 12 ++++++++++ .../note-import/note-import.component.ts | 12 ++++++++++ .../note-rename/note-rename.component.html | 12 ++++++++++ .../note-rename/note-rename.component.less | 12 ++++++++++ .../note-rename/note-rename.component.ts | 12 ++++++++++ .../page-header/page-header.component.html | 12 ++++++++++ .../page-header/page-header.component.less | 12 ++++++++++ .../page-header/page-header.component.ts | 12 ++++++++++ .../app/share/pipes/humanize-bytes.pipe.ts | 12 +++------- .../src/app/share/pipes/index.ts | 12 ++++++++++ .../src/app/share/pipes/public-api.ts | 12 ++++++++++ zeppelin-frontend/src/app/share/public-api.ts | 12 ++++++++++ .../src/app/share/resize-handle/index.ts | 12 ++++++++++ .../src/app/share/resize-handle/public-api.ts | 12 ++++++++++ .../resize-handle.component.html | 12 ++++++++++ .../resize-handle.component.less | 12 ++++++++++ .../resize-handle/resize-handle.component.ts | 12 ++++++++++ .../run-scripts/run-scripts.directive.ts | 12 ++++++++++ .../src/app/share/share.module.ts | 12 ++++++++++ .../src/app/share/spin/spin.component.html | 12 ++++++++++ .../src/app/share/spin/spin.component.less | 12 ++++++++++ .../src/app/share/spin/spin.component.ts | 12 ++++++++++ .../src/app/spell/spell-result.ts | 12 ++++++++++ .../area-chart-visualization.component.html | 12 ++++++++++ .../area-chart-visualization.component.less | 12 ++++++++++ .../area-chart-visualization.component.ts | 12 ++++++++++ .../area-chart/area-chart-visualization.ts | 12 ++++++++++ .../bar-chart-visualization.component.html | 12 ++++++++++ .../bar-chart-visualization.component.less | 12 ++++++++++ .../bar-chart-visualization.component.ts | 12 ++++++++++ .../bar-chart/bar-chart-visualization.ts | 12 ++++++++++ .../pivot-setting.component.html | 12 ++++++++++ .../pivot-setting.component.less | 12 ++++++++++ .../pivot-setting/pivot-setting.component.ts | 12 ++++++++++ .../scatter-setting.component.html | 12 ++++++++++ .../scatter-setting.component.less | 12 ++++++++++ .../scatter-setting.component.ts | 12 ++++++++++ .../common/util/calc-tick-count.ts | 12 ++++++++++ .../visualizations/common/util/set-x-axis.ts | 12 ++++++++++ .../x-axis-setting.component.html | 12 ++++++++++ .../x-axis-setting.component.less | 12 ++++++++++ .../x-axis-setting.component.ts | 12 ++++++++++ .../line-chart-visualization.component.html | 12 ++++++++++ .../line-chart-visualization.component.less | 12 ++++++++++ .../line-chart-visualization.component.ts | 12 ++++++++++ .../line-chart/line-chart-visualization.ts | 12 ++++++++++ .../pie-chart-visualization.component.html | 12 ++++++++++ .../pie-chart-visualization.component.less | 12 ++++++++++ .../pie-chart-visualization.component.ts | 12 ++++++++++ .../pie-chart/pie-chart-visualization.ts | 12 ++++++++++ ...scatter-chart-visualization.component.html | 12 ++++++++++ ...scatter-chart-visualization.component.less | 12 ++++++++++ .../scatter-chart-visualization.component.ts | 12 ++++++++++ .../scatter-chart-visualization.ts | 12 ++++++++++ .../table/table-visualization.component.html | 12 ++++++++++ .../table/table-visualization.component.less | 12 ++++++++++ .../table/table-visualization.component.ts | 12 ++++++++++ .../table/table-visualization.ts | 12 ++++++++++ .../visualizations/visualization.module.ts | 12 ++++++++++ .../src/environments/environment.prod.ts | 12 ++++++++++ .../src/environments/environment.ts | 12 ++++++++++ zeppelin-frontend/src/index.html | 12 ++++++++++ zeppelin-frontend/src/main.ts | 12 ++++++++++ zeppelin-frontend/src/polyfills.ts | 12 ++++++++++ zeppelin-frontend/src/styles.less | 12 ++++++++++ zeppelin-frontend/src/styles/base.less | 12 ++++++++++ zeppelin-frontend/src/styles/font.less | 12 ++++++++++ zeppelin-frontend/src/styles/global.less | 12 ++++++++++ zeppelin-frontend/src/styles/rewrite.less | 12 ++++++++++ zeppelin-frontend/src/styles/spin.less | 12 ++++++++++ .../src/styles/theme/dark/antd-dark.less | 12 ++++++++++ .../src/styles/theme/dark/theme-dark.less | 12 ++++++++++ .../src/styles/theme/light/antd-light.less | 12 ++++++++++ .../src/styles/theme/light/theme-light.less | 12 ++++++++++ .../src/styles/theme/markdown.less | 12 ++++++++++ .../src/styles/theme/theme-mixin.less | 12 ++++++++++ zeppelin-frontend/src/test.ts | 12 ++++++++++ 275 files changed, 3293 insertions(+), 20 deletions(-) diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts index e0da1129877..12234517668 100644 --- a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; import { TableData, Visualization, VISUALIZATION } from '@zeppelin/visualization'; diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts index 7f0d109b907..2578f26df39 100644 --- a/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-vis.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { JsonVisComponent } from './json-vis.component'; import { CommonModule } from '@angular/common'; diff --git a/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts b/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts index bd269599b50..9f97db4598f 100644 --- a/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts +++ b/zeppelin-frontend/projects/helium-vis-example/src/json-visualization.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { CdkPortalOutlet } from '@angular/cdk/portal'; import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core'; diff --git a/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts b/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts index c1f61fe8b01..82ba8d0ce42 100644 --- a/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts +++ b/zeppelin-frontend/projects/helium-vis-example/src/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + /* * Public API Surface of helium-vis-example */ diff --git a/zeppelin-frontend/projects/helium-vis-example/src/test.ts b/zeppelin-frontend/projects/helium-vis-example/src/test.ts index 978c64fb83f..9be59f628dd 100644 --- a/zeppelin-frontend/projects/helium-vis-example/src/test.ts +++ b/zeppelin-frontend/projects/helium-vis-example/src/test.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/dist/zone'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts b/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts index 020f5947eab..677b3d7d1cf 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/common-deps.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import * as common from '@angular/common'; import * as core from '@angular/core'; import * as forms from '@angular/forms'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/index.ts b/zeppelin-frontend/projects/zeppelin-helium/src/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/index.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts index 480ac3ebb15..4b9b89fd7db 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + /* * Public API Surface of zeppelin-helium */ diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/test.ts b/zeppelin-frontend/projects/zeppelin-helium/src/test.ts index 978c64fb83f..9be59f628dd 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/test.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/test.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/dist/zone'; diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts index e86e5039bf3..e24ac800a78 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; @NgModule({}) diff --git a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts index 50f726b6a18..10f8997dbf7 100644 --- a/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts +++ b/zeppelin-frontend/projects/zeppelin-helium/src/zeppelin-helium.service.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Injectable, Type } from '@angular/core'; import { Visualization } from '@zeppelin/visualization'; import { COMMON_DEPS } from './common-deps'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts index d080a79bf4e..0a5ad6dadd5 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-common.interface.ts @@ -1,3 +1,15 @@ +/* + * 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 type EditorMode = | 'ace/mode/scala' | 'ace/mode/python' diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts index 1ea8f0dd6e0..dde8242fc63 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { AuthInfo, ConfigurationsInfo, ErrorInfo } from './message-common.interface'; import { CheckpointNote, diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts index 3eb9d81902f..c59e459410e 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-interpreter.interface.ts @@ -1,3 +1,15 @@ +/* + * 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 interface InterpreterSetting { interpreterSettings: InterpreterItem[]; } diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts index 5e48b4065d6..c59122b3a38 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts @@ -1,3 +1,15 @@ +/* + * 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 interface ListNoteJobs { noteJobs: NoteJobs; } diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts index 425913f6590..f7b22de6daf 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-notebook.interface.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ParagraphItem } from './message-paragraph.interface'; interface ID { diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts index 141908bdab5..d3ce82b6029 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + // tslint:disable:no-redundant-jsdoc /** * Representation of event type. diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts index 712d0564067..a9c05af7aad 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { EditorCompletionKey, EditorLanguage, EditorMode } from './message-common.interface'; export enum DynamicFormsType { diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts index ac4dbbd25fb..4160fa7f035 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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 * from './message-common.interface'; export * from './message-data-type-map.interface'; export * from './message-notebook.interface'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts index f6bf8fa273f..bdc71e1f428 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/interfaces/websocket-message.interface.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { MixMessageDataTypeMap } from './message-data-type-map.interface'; export interface WebSocketMessage { diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts index 54b7ca86f95..65ceec605de 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/message.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { interval, Observable, Subject, Subscription } from 'rxjs'; import { delay, filter, map, mergeMap, retryWhen, take } from 'rxjs/operators'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; diff --git a/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts index 96d41ac6252..c5417873004 100644 --- a/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts +++ b/zeppelin-frontend/projects/zeppelin-sdk/src/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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 * from './message'; // https://github.com/ng-packagr/ng-packagr/issues/1093 export * from './interfaces/public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts index 415ebdb21ad..98d4632fcab 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/data-set.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ParagraphIResultsMsgItem } from '@zeppelin/sdk'; export abstract class DataSet { diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts index 07b02457dae..d663788be0e 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-base.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { GraphConfig } from '@zeppelin/sdk'; import { G2VisualizationComponentBase } from './g2-visualization-component-base'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts index 15868457bbf..8e52dcdc9de 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/g2-visualization-component-base.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ElementRef, OnDestroy } from '@angular/core'; import * as G2 from '@antv/g2'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts index 6c9bb110161..cade722cd81 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/pivot-transformation.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { DataSet } from '@antv/data-set'; import { get } from 'lodash'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts index a3ea0b01796..c28b0e2a649 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + /* * Public API Surface of zeppelin-visualization */ diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts index 5b57d653536..f0bd5ad4227 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/table-data.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { DataSet as AntvDataSet } from '@antv/data-set'; import { DatasetType, ParagraphIResultsMsgItem } from '@zeppelin/sdk'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts index a1af386c193..19111ecad88 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/table-transformation.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { TableData } from './table-data'; import { Transformation } from './transformation'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts index 8e8b5915031..522ac021326 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/transformation.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { GraphConfig } from '@zeppelin/sdk'; import { DataSet } from './data-set'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts index 8080d85ad42..a9418d98307 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization-component-portal.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { CdkPortalOutlet, ComponentPortal, ComponentType, PortalInjector } from '@angular/cdk/portal'; import { ComponentFactoryResolver, InjectionToken, ViewContainerRef } from '@angular/core'; diff --git a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts index 6e8a8a62adf..bb483e58766 100644 --- a/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts +++ b/zeppelin-frontend/projects/zeppelin-visualization/src/visualization.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ComponentRef } from '@angular/core'; import { Subject } from 'rxjs'; diff --git a/zeppelin-frontend/src/app/app-http.interceptor.ts b/zeppelin-frontend/src/app/app-http.interceptor.ts index 46762a4fa71..520eea89418 100644 --- a/zeppelin-frontend/src/app/app-http.interceptor.ts +++ b/zeppelin-frontend/src/app/app-http.interceptor.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { throwError, Observable } from 'rxjs'; diff --git a/zeppelin-frontend/src/app/app-message.interceptor.ts b/zeppelin-frontend/src/app/app-message.interceptor.ts index c7183d7ed87..0e9843d8960 100644 --- a/zeppelin-frontend/src/app/app-message.interceptor.ts +++ b/zeppelin-frontend/src/app/app-message.interceptor.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/app-routing.module.ts b/zeppelin-frontend/src/app/app-routing.module.ts index 805cb239437..4a580cf9c8f 100644 --- a/zeppelin-frontend/src/app/app-routing.module.ts +++ b/zeppelin-frontend/src/app/app-routing.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts b/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts index 96cfd186c2e..dc7e6bc7d52 100644 --- a/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts +++ b/zeppelin-frontend/src/app/app-runtime-compiler.providers.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Compiler, CompilerFactory, diff --git a/zeppelin-frontend/src/app/app.component.html b/zeppelin-frontend/src/app/app.component.html index 8e2fd10575a..d101c77150d 100644 --- a/zeppelin-frontend/src/app/app.component.html +++ b/zeppelin-frontend/src/app/app.component.html @@ -1,3 +1,15 @@ + + Getting Ticket Data ... Logging out ... diff --git a/zeppelin-frontend/src/app/app.component.less b/zeppelin-frontend/src/app/app.component.less index 89b27550ca0..f9587c7330a 100644 --- a/zeppelin-frontend/src/app/app.component.less +++ b/zeppelin-frontend/src/app/app.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import 'theme-mixin'; .themeMixin({ diff --git a/zeppelin-frontend/src/app/app.component.spec.ts b/zeppelin-frontend/src/app/app.component.spec.ts index ebd58d89220..15d4ceb7190 100644 --- a/zeppelin-frontend/src/app/app.component.spec.ts +++ b/zeppelin-frontend/src/app/app.component.spec.ts @@ -1,16 +1,24 @@ -import { TestBed, async } from '@angular/core/testing'; +/* + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - RouterTestingModule - ], - declarations: [ - AppComponent - ], + imports: [RouterTestingModule], + declarations: [AppComponent] }).compileComponents(); })); diff --git a/zeppelin-frontend/src/app/app.component.ts b/zeppelin-frontend/src/app/app.component.ts index 4273469019a..dc9fcb3afbb 100644 --- a/zeppelin-frontend/src/app/app.component.ts +++ b/zeppelin-frontend/src/app/app.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Component } from '@angular/core'; import { NavigationEnd, NavigationStart, Router } from '@angular/router'; import { filter, map } from 'rxjs/operators'; diff --git a/zeppelin-frontend/src/app/app.module.ts b/zeppelin-frontend/src/app/app.module.ts index 15e2e87f8e5..ae3347c350b 100644 --- a/zeppelin-frontend/src/app/app.module.ts +++ b/zeppelin-frontend/src/app/app.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { registerLocaleData } from '@angular/common'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import en from '@angular/common/locales/en'; diff --git a/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts b/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts index 3eded4ee484..fb1e6208e78 100644 --- a/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts +++ b/zeppelin-frontend/src/app/core/copy-text/copy-text-to-clipboard.ts @@ -1,3 +1,15 @@ +/* + * 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 function copyTextToClipboard(text: string): void { const textArea: HTMLTextAreaElement = document.createElement('textarea'); diff --git a/zeppelin-frontend/src/app/core/copy-text/index.ts b/zeppelin-frontend/src/app/core/copy-text/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/core/copy-text/index.ts +++ b/zeppelin-frontend/src/app/core/copy-text/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/copy-text/public-api.ts b/zeppelin-frontend/src/app/core/copy-text/public-api.ts index 1335378243e..aa47aad6726 100644 --- a/zeppelin-frontend/src/app/core/copy-text/public-api.ts +++ b/zeppelin-frontend/src/app/core/copy-text/public-api.ts @@ -1 +1,13 @@ +/* + * 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 * from './copy-text-to-clipboard'; diff --git a/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts b/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts index d4cd6fea1d2..7f394c0c4c9 100644 --- a/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts +++ b/zeppelin-frontend/src/app/core/destroy-hook/destroy-hook.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { OnDestroy } from '@angular/core'; import { Subject } from 'rxjs'; diff --git a/zeppelin-frontend/src/app/core/destroy-hook/index.ts b/zeppelin-frontend/src/app/core/destroy-hook/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/core/destroy-hook/index.ts +++ b/zeppelin-frontend/src/app/core/destroy-hook/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts b/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts index 851b581557a..c4b93aa16d8 100644 --- a/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts +++ b/zeppelin-frontend/src/app/core/destroy-hook/public-api.ts @@ -1 +1,13 @@ +/* + * 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 * from './destroy-hook.component'; diff --git a/zeppelin-frontend/src/app/core/index.ts b/zeppelin-frontend/src/app/core/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/core/index.ts +++ b/zeppelin-frontend/src/app/core/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/message-listener/index.ts b/zeppelin-frontend/src/app/core/message-listener/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/core/message-listener/index.ts +++ b/zeppelin-frontend/src/app/core/message-listener/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/core/message-listener/message-listener.ts b/zeppelin-frontend/src/app/core/message-listener/message-listener.ts index 627ab3ec0cf..5c29be8aaa0 100644 --- a/zeppelin-frontend/src/app/core/message-listener/message-listener.ts +++ b/zeppelin-frontend/src/app/core/message-listener/message-listener.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { OnDestroy } from '@angular/core'; import { Subscriber } from 'rxjs'; diff --git a/zeppelin-frontend/src/app/core/message-listener/public-api.ts b/zeppelin-frontend/src/app/core/message-listener/public-api.ts index ef611e2851a..61a92db9183 100644 --- a/zeppelin-frontend/src/app/core/message-listener/public-api.ts +++ b/zeppelin-frontend/src/app/core/message-listener/public-api.ts @@ -1 +1,13 @@ +/* + * 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 * from './message-listener'; diff --git a/zeppelin-frontend/src/app/core/public-api.ts b/zeppelin-frontend/src/app/core/public-api.ts index 3eeb4271901..c5141035cc3 100644 --- a/zeppelin-frontend/src/app/core/public-api.ts +++ b/zeppelin-frontend/src/app/core/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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 * from './message-listener'; export * from './destroy-hook'; export * from './copy-text'; diff --git a/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts b/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts index 1f812518dca..940a29249fc 100644 --- a/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts +++ b/zeppelin-frontend/src/app/helium-manager/helium-manager.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Compiler, CompilerFactory, COMPILER_OPTIONS, NgModule } from '@angular/core'; import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; diff --git a/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts index 2342f97412a..1a1a03c55c3 100644 --- a/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts +++ b/zeppelin-frontend/src/app/helium-manager/helium-manager.service.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { Compiler, Injectable, Injector, NgModuleFactory, OnDestroy, Type } from '@angular/core'; import { ZeppelinHeliumPackage, ZeppelinHeliumService } from '@zeppelin/helium'; import { of, BehaviorSubject } from 'rxjs'; diff --git a/zeppelin-frontend/src/app/helium-manager/index.ts b/zeppelin-frontend/src/app/helium-manager/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/helium-manager/index.ts +++ b/zeppelin-frontend/src/app/helium-manager/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/helium-manager/public-api.ts b/zeppelin-frontend/src/app/helium-manager/public-api.ts index f275905f5ef..8ba1a21026a 100644 --- a/zeppelin-frontend/src/app/helium-manager/public-api.ts +++ b/zeppelin-frontend/src/app/helium-manager/public-api.ts @@ -1,2 +1,14 @@ +/* + * 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 * from './helium-manager.service'; export * from './helium-manager.module'; diff --git a/zeppelin-frontend/src/app/interfaces/index.ts b/zeppelin-frontend/src/app/interfaces/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/interfaces/index.ts +++ b/zeppelin-frontend/src/app/interfaces/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/interfaces/interpreter.ts b/zeppelin-frontend/src/app/interfaces/interpreter.ts index 1e3b5a7cc35..2e1d661d678 100644 --- a/zeppelin-frontend/src/app/interfaces/interpreter.ts +++ b/zeppelin-frontend/src/app/interfaces/interpreter.ts @@ -1,3 +1,15 @@ +/* + * 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 type InterpreterPropertyTypes = 'textarea' | 'string' | 'number' | 'url' | 'password' | 'checkbox'; export interface Interpreter { diff --git a/zeppelin-frontend/src/app/interfaces/message-interceptor.ts b/zeppelin-frontend/src/app/interfaces/message-interceptor.ts index ca126b5e874..85618dfa44a 100644 --- a/zeppelin-frontend/src/app/interfaces/message-interceptor.ts +++ b/zeppelin-frontend/src/app/interfaces/message-interceptor.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { InjectionToken } from '@angular/core'; import { MessageReceiveDataTypeMap, WebSocketMessage } from '@zeppelin/sdk'; diff --git a/zeppelin-frontend/src/app/interfaces/node-list.ts b/zeppelin-frontend/src/app/interfaces/node-list.ts index da4854e5e3a..3330e03aa75 100644 --- a/zeppelin-frontend/src/app/interfaces/node-list.ts +++ b/zeppelin-frontend/src/app/interfaces/node-list.ts @@ -1,3 +1,15 @@ +/* + * 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 interface NodeList { root: RootNode; flatList: FlatListNodeItem[]; diff --git a/zeppelin-frontend/src/app/interfaces/public-api.ts b/zeppelin-frontend/src/app/interfaces/public-api.ts index 11d21360bea..1e07b8e496a 100644 --- a/zeppelin-frontend/src/app/interfaces/public-api.ts +++ b/zeppelin-frontend/src/app/interfaces/public-api.ts @@ -1,3 +1,15 @@ +/* + * 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 * from './ticket'; export * from './trash-folder-id'; export * from './interpreter'; diff --git a/zeppelin-frontend/src/app/interfaces/security.ts b/zeppelin-frontend/src/app/interfaces/security.ts index b7d769c75a3..ff7db2a97d2 100644 --- a/zeppelin-frontend/src/app/interfaces/security.ts +++ b/zeppelin-frontend/src/app/interfaces/security.ts @@ -1,3 +1,15 @@ +/* + * 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 interface SecurityUserList { roles: string[]; users: string[]; diff --git a/zeppelin-frontend/src/app/interfaces/ticket.ts b/zeppelin-frontend/src/app/interfaces/ticket.ts index 542d8617586..0f4883e0d2b 100644 --- a/zeppelin-frontend/src/app/interfaces/ticket.ts +++ b/zeppelin-frontend/src/app/interfaces/ticket.ts @@ -1,3 +1,15 @@ +/* + * 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 class ITicket { principal = ''; ticket = ''; diff --git a/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts b/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts index 11ca0045165..035b92a1789 100644 --- a/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts +++ b/zeppelin-frontend/src/app/interfaces/trash-folder-id.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { InjectionToken } from '@angular/core'; export const TRASH_FOLDER_ID_TOKEN = new InjectionToken('TRASH_FOLDER_ID'); diff --git a/zeppelin-frontend/src/app/languages/index.ts b/zeppelin-frontend/src/app/languages/index.ts index 7e1a213e3ea..49e47404422 100644 --- a/zeppelin-frontend/src/app/languages/index.ts +++ b/zeppelin-frontend/src/app/languages/index.ts @@ -1 +1,13 @@ +/* + * 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 * from './public-api'; diff --git a/zeppelin-frontend/src/app/languages/load.ts b/zeppelin-frontend/src/app/languages/load.ts index 92ae43a99af..5dad459c888 100644 --- a/zeppelin-frontend/src/app/languages/load.ts +++ b/zeppelin-frontend/src/app/languages/load.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { languages } from 'monaco-editor'; import { conf as ScalaConf, language as ScalaLanguage } from './scala'; diff --git a/zeppelin-frontend/src/app/languages/public-api.ts b/zeppelin-frontend/src/app/languages/public-api.ts index d3eca94a978..973c93158ba 100644 --- a/zeppelin-frontend/src/app/languages/public-api.ts +++ b/zeppelin-frontend/src/app/languages/public-api.ts @@ -1,2 +1,14 @@ +/* + * 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 * from './scala'; export * from './load'; diff --git a/zeppelin-frontend/src/app/languages/scala.ts b/zeppelin-frontend/src/app/languages/scala.ts index bee7f545199..581049129e0 100644 --- a/zeppelin-frontend/src/app/languages/scala.ts +++ b/zeppelin-frontend/src/app/languages/scala.ts @@ -1,7 +1,14 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +/* + * 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. + */ 'use strict'; diff --git a/zeppelin-frontend/src/app/pages/login/login-routing.module.ts b/zeppelin-frontend/src/app/pages/login/login-routing.module.ts index 32f29152ebe..06dd0304db8 100644 --- a/zeppelin-frontend/src/app/pages/login/login-routing.module.ts +++ b/zeppelin-frontend/src/app/pages/login/login-routing.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/pages/login/login.component.html b/zeppelin-frontend/src/app/pages/login/login.component.html index 6ef9b161e04..faa16b67189 100644 --- a/zeppelin-frontend/src/app/pages/login/login.component.html +++ b/zeppelin-frontend/src/app/pages/login/login.component.html @@ -1,3 +1,15 @@ + +
diff --git a/zeppelin-frontend/src/app/pages/login/login.component.less b/zeppelin-frontend/src/app/pages/login/login.component.less index 902c59e1f5c..7aac210300d 100644 --- a/zeppelin-frontend/src/app/pages/login/login.component.less +++ b/zeppelin-frontend/src/app/pages/login/login.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import "theme-mixin"; .themeMixin({ diff --git a/zeppelin-frontend/src/app/pages/login/login.component.ts b/zeppelin-frontend/src/app/pages/login/login.component.ts index acd687f2480..937724974b6 100644 --- a/zeppelin-frontend/src/app/pages/login/login.component.ts +++ b/zeppelin-frontend/src/app/pages/login/login.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/pages/login/login.module.ts b/zeppelin-frontend/src/app/pages/login/login.module.ts index bb1f7a3465b..46f40a055d5 100644 --- a/zeppelin-frontend/src/app/pages/login/login.module.ts +++ b/zeppelin-frontend/src/app/pages/login/login.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts index 9c008c0a7ec..23a345f025a 100644 --- a/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/home/home-routing.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.html b/zeppelin-frontend/src/app/pages/workspace/home/home.component.html index 922ceaf3dba..5a26044c970 100644 --- a/zeppelin-frontend/src/app/pages/workspace/home/home.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.html @@ -1,3 +1,15 @@ + +

diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.less b/zeppelin-frontend/src/app/pages/workspace/home/home.component.less index 2dd9d3ad1d0..3f79862ddc6 100644 --- a/zeppelin-frontend/src/app/pages/workspace/home/home.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import 'theme-mixin'; .themeMixin({ diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts b/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts index c2899555dc9..ecc40995d68 100644 --- a/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { MessageListener, MessageListenersManager } from '@zeppelin/core'; diff --git a/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts b/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts index 6fbf65c2aff..24e59a71853 100644 --- a/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/home/home.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html index 05e514f6511..57a0b6339d7 100644 --- a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html @@ -1,3 +1,15 @@ + +
diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less index bd1eeeee9e1..f348ef32466 100644 --- a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import 'theme-mixin'; .themeMixin({ diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts index a495195c733..8dd38c97766 100644 --- a/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { takeUntil } from 'rxjs/operators'; diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts index 1f78078c05b..c9c995825cb 100644 --- a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter-routing.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html index bf1400f1688..144a57d2fca 100644 --- a/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/interpreter/interpreter.component.html @@ -1,3 +1,15 @@ + + + + + +
+
+
Add Paragraph diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less index 4b63645644f..c51b70e61a6 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import "theme-mixin"; .themeMixin({ diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts index 230af235258..bb77a832979 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; @Component({ diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html index 245a050e4fc..263390cbce6 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.html @@ -1,3 +1,15 @@ + +

Settings

diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less index 7774ba7f9c1..fcaea0a913f 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import "theme-mixin"; .themeMixin({ diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts index 8d7e7b1d8be..377a869d1a4 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts index 8d4bb780e37..6c177b6ba80 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook-routing.module.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html index 40e318011ae..3bad5eec173 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/notebook.component.html @@ -1,3 +1,15 @@ + +
+ diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less index 0aad982837f..72a1f689dc8 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less @@ -1,3 +1,15 @@ +/* + * 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. + */ + @import "theme-mixin"; :host { diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts index 2ba04359eb1..3cf8eab2d07 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts @@ -1,3 +1,15 @@ +/* + * 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. + */ + import { AfterViewInit, ChangeDetectionStrategy, diff --git a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html index c4f3d313cd4..5ec5f52fccf 100644 --- a/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html +++ b/zeppelin-frontend/src/app/pages/workspace/notebook/paragraph/control/control.component.html @@ -1,3 +1,15 @@ + +