This is an automated email from the ASF dual-hosted git repository. zjffdu pushed a commit to branch web_angular in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/web_angular by this push: new 831c6ef [ZEPPELIN-4450] Provide Angular.js Template Migration Tool 831c6ef is described below commit 831c6ef9c341d87c02bc538ce88742bacec15a39 Author: Hsuan Lee <hsua...@gmail.com> AuthorDate: Tue Nov 26 11:28:28 2019 +0800 [ZEPPELIN-4450] Provide Angular.js Template Migration Tool ### What is this PR for? We have implemented the frontend API of Angular.js using the latest Angular. But the templates some [differences](https://angular.io/guide/ajs-quick-reference) between Angular.js and Angular. So to help users migrate templates, we provide a migration tool that will be integrated into the Zeppelin web. This is its [DEMO](https://ng1-updater.hsuan.xyz/) it can quickly fix these differences. We plan to do the following work: 1. Add a new type `%ng` (official abbreviation) to distinguish between Angular.js and Angular templates. 2. When the user runs a paragraph with the `%angular` type, the upgrade dialog will be open. 3. Upgrade the template in the dialog and click the `Update and Copy` button. 4. Automatically create a paragraph of type `%ng` template in below ### What type of PR is it? [Feature] ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-4321 https://issues.apache.org/jira/browse/ZEPPELIN-4450 ### How should this be tested? * First time? Setup Travis CI as described on https://zeppelin.apache.org/contribution/contributions.html#continuous-integration * Strongly recommended: add automated unit tests for any new or changed behavior * Outline any manual steps to test the PR here. ### Screenshots (if appropriate)  ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? NO * Does this needs documentation? NO Author: Hsuan Lee <hsua...@gmail.com> Closes #3528 from hsuanxyz/angularjs-template-compatible and squashes the following commits: 3474f81c5 [Hsuan Lee] fix: fix editor focus d639a2b13 [Hsuan Lee] fix: fix focus bar 38d0c4659 [Hsuan Lee] chore: update code editor actions 1a28a7a9f [Hsuan Lee] feat: provide Angular.js template migration tool --- .../src/main/resources/interpreter-setting.json | 11 + zeppelin-web-angular/package-lock.json | 18 +- zeppelin-web-angular/package.json | 3 + .../code-editor/code-editor.component.html | 5 +- .../code-editor/code-editor.component.less | 6 +- .../paragraph/code-editor/code-editor.component.ts | 10 +- .../paragraph/control/control.component.ts | 117 ++++---- .../notebook/paragraph/paragraph.component.ts | 297 ++++++++++++--------- .../paragraph/result/result.component.html | 2 +- .../notebook/paragraph/result/result.component.ts | 17 +- .../app/services/ng-template-adapter.service.ts | 65 +++++ .../ng1-migration/ng1-migration.component.html | 54 ++++ .../ng1-migration/ng1-migration.component.less | 77 ++++++ .../share/ng1-migration/ng1-migration.component.ts | 174 ++++++++++++ zeppelin-web-angular/src/app/share/share.module.ts | 4 +- 15 files changed, 646 insertions(+), 214 deletions(-) diff --git a/angular/src/main/resources/interpreter-setting.json b/angular/src/main/resources/interpreter-setting.json index 723348d..957295f 100644 --- a/angular/src/main/resources/interpreter-setting.json +++ b/angular/src/main/resources/interpreter-setting.json @@ -9,5 +9,16 @@ "editOnDblClick": true, "completionSupport": false } + }, + { + "group": "angular", + "name": "ng", + "className": "org.apache.zeppelin.angular.AngularInterpreter", + "properties": { + }, + "editor": { + "editOnDblClick": true, + "completionSupport": false + } } ] diff --git a/zeppelin-web-angular/package-lock.json b/zeppelin-web-angular/package-lock.json index 85ad2a6..cd5fb2f 100644 --- a/zeppelin-web-angular/package-lock.json +++ b/zeppelin-web-angular/package-lock.json @@ -2370,6 +2370,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/parse5": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.2.tgz", + "integrity": "sha512-BOl+6KDs4ItndUWUFchy3aEqGdHhw0BC4Uu+qoDonN/f0rbUnJbm71Ulj8Tt9jLFRaAxPLKvdS1bBLfx1qXR9g==", + "dev": true + }, "@types/q": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", @@ -9967,6 +9973,11 @@ "tslib": "^1.9.0" } }, + "ng1-template-updater": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ng1-template-updater/-/ng1-template-updater-0.0.4.tgz", + "integrity": "sha512-GgmAV7Zbj8ZLQ/IJGjjSi40bXTHFP/k5fhlxcH0V2fWaya5lu6y07Vh4LKvuUqNbkbKl28XW8Z1fhL5pwHxgsA==" + }, "ngx-build-plus": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-8.1.5.tgz", @@ -10695,10 +10706,9 @@ "integrity": "sha1-en7A0esG+lMlx9PgCbhZoJtdSes=" }, "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "optional": true + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, "parseqs": { "version": "0.0.5", diff --git a/zeppelin-web-angular/package.json b/zeppelin-web-angular/package.json index 63b6be1..9d9aa93 100644 --- a/zeppelin-web-angular/package.json +++ b/zeppelin-web-angular/package.json @@ -37,6 +37,8 @@ "mathjax": "2.7.5", "monaco-editor": "^0.18.1", "ng-zorro-antd": "^8.4.0", + "ng1-template-updater": "0.0.4", + "parse5": "^5.1.1", "rxjs": "~6.5.3", "systemjs": "^5.0.0", "tslib": "^1.9.0", @@ -56,6 +58,7 @@ "@types/lodash": "^4.14.124", "@types/mathjax": "^0.0.35", "@types/node": "~8.9.4", + "@types/parse5": "^5.0.2", "codelyzer": "^5.0.0", "dotenv": "^8.0.0", "https-proxy-agent": "^2.2.1", diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html index 4d382f2..d543e67 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.html @@ -11,6 +11,7 @@ --> <zeppelin-code-editor [style.height.px]="height" - [class.dirty]="dirty" - (nzEditorInitialized)="initializedEditor($event)"> + [class.focused]="focus" + [class.dirty]="dirty" + (nzEditorInitialized)="initializedEditor($event)"> </zeppelin-code-editor> diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less index 72a1f68..8f61bd5 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less @@ -18,7 +18,7 @@ .themeMixin({ - zeppelin-monaco-editor { + zeppelin-code-editor { display: block; border-left: 4px solid @border-color-split; overflow: hidden; @@ -26,6 +26,10 @@ &.dirty { border-left-color: @warning-color; } + + &.focused:not(.dirty) { + border-left-color: @primary-color; + } } }); diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts index 0711e81..916afef 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts @@ -76,16 +76,10 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro const editor = this.editor; this.monacoDisposables.push( editor.onDidFocusEditorText(() => { - this.ngZone.runOutsideAngular(() => { - this.editorFocus.emit(); - editor.updateOptions({ renderLineHighlight: 'all' }); - }); + this.editorFocus.emit(); }), editor.onDidBlurEditorText(() => { this.editorBlur.emit(); - this.ngZone.runOutsideAngular(() => { - editor.updateOptions({ renderLineHighlight: 'none' }); - }); }), editor.onDidChangeModelContent(() => { @@ -110,13 +104,11 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro 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', diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts index 5b95953..bda003d 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts @@ -81,20 +81,66 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { disabled: boolean; icon: string; shortCut: string; - keyBindings: number[]; trigger(): void; }> = []; - updateListOfMenu(monaco?) { + updateListOfMenu() { this.listOfMenu = [ { + label: 'Run', + show: !this.first, + disabled: this.isEntireNoteRunning, + icon: 'play-circle', + trigger: () => this.trigger(this.runParagraph), + shortCut: this.isMac ? '⇧+⌘+Enter' : 'Shift+Ctrl+Enter' + }, + { + label: 'Run all above', + show: !this.first, + disabled: this.isEntireNoteRunning, + icon: 'up-square', + trigger: () => this.trigger(this.runAllAbove), + shortCut: this.isMac ? '⇧+⌘+Enter' : 'Shift+Ctrl+Enter' + }, + { + label: 'Run all below', + show: !this.last, + disabled: this.isEntireNoteRunning, + icon: 'down-square', + trigger: () => this.trigger(this.runAllBelowAndCurrent), + shortCut: this.isMac ? '⇧+⌘+Enter' : 'Shift+Ctrl+Enter' + }, + { + label: 'Link this paragraph', + show: true, + disabled: false, + icon: 'export', + trigger: () => this.goToSingleParagraph(), + shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+W` + }, + { + label: 'Clear output', + show: true, + disabled: this.isEntireNoteRunning, + icon: 'fire', + trigger: () => this.clearParagraphOutput(), + shortCut: this.isMac ? '⌥+⌘+L' : 'Alt+Ctrl+L' + }, + { + label: 'Remove', + show: this.paragraphLength > 1, + disabled: this.isEntireNoteRunning, + icon: 'delete', + trigger: () => this.onRemoveParagraph(), + shortCut: this.isMac ? '⇧+Del (Command)' : 'Shift+Del (Command)' + }, + { 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] : [] + shortCut: `${this.isMac ? '⌘' : 'Ctrl'}+K (Command)` }, { label: 'Move down', @@ -102,8 +148,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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] : [] + shortCut: `${this.isMac ? '⌘' : 'Ctrl'}+J (Command)` }, { label: 'Insert new', @@ -111,26 +156,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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] : [] + shortCut: `B (Command)` }, { label: 'Clone paragraph', @@ -138,8 +164,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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] : [] + shortCut: `C (Command)` }, { label: this.title ? 'Hide Title' : 'Show Title', @@ -147,8 +172,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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] : [] + shortCut: `T (Command)` }, { label: this.lineNumbers ? 'Hide line numbers' : 'Show line numbers', @@ -156,8 +180,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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] : [] + shortCut: `L (Command)` }, { label: this.enabled ? 'Disable run' : 'Enable run', @@ -165,35 +188,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges { 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.onRemoveParagraph(), - shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+D`, - keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_D] : [] + shortCut: `R (Command)` } ]; } diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts index 1dde62d..f5b61e8 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts @@ -13,7 +13,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, ElementRef, + Component, + ElementRef, EventEmitter, Input, OnChanges, @@ -25,8 +26,8 @@ import { ViewChild, ViewChildren } from '@angular/core'; -import {merge, Observable, Subject} from 'rxjs'; -import {map, takeUntil} from 'rxjs/operators'; +import { merge, Observable, Subject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; import DiffMatchPatch from 'diff-match-patch'; import { isEmpty, isEqual } from 'lodash'; @@ -60,6 +61,7 @@ import { } from '@zeppelin/services'; import { SpellResult } from '@zeppelin/spell/spell-result'; +import { NgTemplateAdapterService } from '@zeppelin/services/ng-template-adapter.service'; import { NzResizeEvent } from 'ng-zorro-antd/resizable'; import { NotebookParagraphCodeEditorComponent } from './code-editor/code-editor.component'; import { NotebookParagraphResultComponent } from './result/result.component'; @@ -71,7 +73,7 @@ type Mode = 'edit' | 'command'; templateUrl: './paragraph.component.html', styleUrls: ['./paragraph.component.less'], host: { - 'tabindex': '-1', + tabindex: '-1', '(focusin)': 'onFocus()' }, changeDetection: ChangeDetectionStrategy.OnPush @@ -210,7 +212,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen this.focusEditor(); } else { this.blurEditor(); - (this.host.nativeElement as HTMLElement).focus(); } } @@ -370,21 +371,22 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen 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); - } - }).afterClose - .pipe(takeUntil(this.destroy$)) + 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); + } + }) + .afterClose.pipe(takeUntil(this.destroy$)) .subscribe(() => { this.waitConfirmFromEdit = false; }); // TODO(hsuanxyz): save cursor } - cloneParagraph(position: string = 'below') { + cloneParagraph(position: string = 'below', newText?: string) { let newIndex = -1; for (let i = 0; i < this.note.paragraphs.length; i++) { if (this.note.paragraphs[i].id === this.paragraph.id) { @@ -408,12 +410,30 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen this.messageService.copyParagraph( newIndex, this.paragraph.title, - this.paragraph.text, + newText || this.paragraph.text, config, this.paragraph.settings.params ); } + runParagraphAfter(text: string) { + 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; + } + runParagraph(paragraphText?: string, propagated: boolean = false) { const text = paragraphText || this.paragraph.text; if (text && !this.isParagraphRunning) { @@ -421,25 +441,34 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen if (this.heliumService.getSpellByMagic(magic)) { this.runParagraphUsingSpell(text, magic, propagated); + this.runParagraphAfter(text); } 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(); + const check = this.ngTemplateAdapterService.preCheck(text); + if (!check) { + this.runParagraphUsingBackendInterpreter(text); + this.runParagraphAfter(text); + } else { + this.waitConfirmFromEdit = true; + this.nzModalService + .confirm({ + nzTitle: 'Do you want to migrate the Angular.js template?', + nzContent: + 'The Angular.js template has been deprecated, please upgrade to Angular template.' + + ' (<a href="https://angular.io/guide/ajs-quick-reference" target="_blank">more info</a>)', + nzOnOk: () => { + this.switchMode('command'); + this.ngTemplateAdapterService + .openMigrationDialog(check) + .pipe(takeUntil(this.destroy$)) + .subscribe(newText => { + this.cloneParagraph('below', newText); + }); + } + }) + .afterClose.pipe(takeUntil(this.destroy$)) + .subscribe(() => (this.waitConfirmFromEdit = false)); + } } - this.editorSetting.isOutputHidden = this.paragraph.config.editorSetting.editOnDblClick; } } @@ -693,125 +722,131 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen private cdr: ChangeDetectorRef, private ngZService: NgZService, private shortcutService: ShortcutService, - private host: ElementRef + private host: ElementRef, + private ngTemplateAdapterService: NgTemplateAdapterService ) { super(messageService); } ngOnInit() { const shortcutService = this.shortcutService.forkByElement(this.host.nativeElement); - const observables: Array<Observable<{ - action: ParagraphActions, - event: KeyboardEvent - }>> = []; + const observables: Array< + Observable<{ + action: ParagraphActions; + event: KeyboardEvent; + }> + > = []; Object.entries(ShortcutsMap).forEach(([action, keys]) => { const keysArr: string[] = Array.isArray(keys) ? keys : [keys]; keysArr.forEach(key => { observables.push( - shortcutService.bindShortcut({ - keybindings: key - }).pipe( - takeUntil(this.destroy$), - map(({event}) => { - return { - event, - action: action as ParagraphActions - } - })) + shortcutService + .bindShortcut({ + keybindings: key + }) + .pipe( + takeUntil(this.destroy$), + map(({ event }) => { + return { + event, + action: action as ParagraphActions + }; + }) + ) ); }); }); merge<{ - action: ParagraphActions, - event: KeyboardEvent + action: ParagraphActions; + event: KeyboardEvent; }>(...observables) .pipe(takeUntil(this.destroy$)) - .subscribe(({action, event}) => { - if (this.mode === 'command') { + .subscribe(({ action, event }) => { + if (this.mode === 'command') { + switch (action) { + case ParagraphActions.InsertAbove: + this.insertParagraph('above'); + break; + case ParagraphActions.InsertBelow: + this.insertParagraph('below'); + break; + case ParagraphActions.SwitchEditorShow: + this.setEditorHide(!this.paragraph.config.editorHide); + this.commitParagraph(); + break; + case ParagraphActions.SwitchOutputShow: + this.setTableHide(!this.paragraph.config.tableHide); + this.commitParagraph(); + break; + case ParagraphActions.SwitchTitleShow: + this.paragraph.config.title = !this.paragraph.config.title; + this.commitParagraph(); + break; + case ParagraphActions.SwitchLineNumber: + this.paragraph.config.lineNumbers = !this.paragraph.config.lineNumbers; + this.commitParagraph(); + break; + case ParagraphActions.MoveToUp: + this.moveUpParagraph(); + break; + case ParagraphActions.MoveToDown: + this.moveDownParagraph(); + break; + case ParagraphActions.SwitchEnable: + this.paragraph.config.enabled = !this.paragraph.config.enabled; + this.commitParagraph(); + break; + case ParagraphActions.ReduceWidth: + this.paragraph.config.colWidth = Math.max(1, this.paragraph.config.colWidth - 1); + this.cdr.markForCheck(); + this.changeColWidth(true); + break; + case ParagraphActions.IncreaseWidth: + this.paragraph.config.colWidth = Math.min(12, this.paragraph.config.colWidth + 1); + this.cdr.markForCheck(); + this.changeColWidth(true); + break; + case ParagraphActions.Delete: + this.removeParagraph(); + break; + case ParagraphActions.SelectAbove: + event.preventDefault(); + this.selectAtIndex.emit(this.index - 1); + break; + case ParagraphActions.SelectBelow: + event.preventDefault(); + this.selectAtIndex.emit(this.index + 1); + break; + default: + break; + } + } switch (action) { - case ParagraphActions.InsertAbove: - this.insertParagraph('above'); - break; - case ParagraphActions.InsertBelow: - this.insertParagraph('below'); - break; - case ParagraphActions.SwitchEditorShow: - this.setEditorHide(!this.paragraph.config.editorHide); - this.commitParagraph(); - break; - case ParagraphActions.SwitchOutputShow: - this.setTableHide(!this.paragraph.config.tableHide); - this.commitParagraph(); - break; - case ParagraphActions.SwitchTitleShow: - this.paragraph.config.title = !this.paragraph.config.title; - this.commitParagraph(); - break; - case ParagraphActions.SwitchLineNumber: - this.paragraph.config.lineNumbers = !this.paragraph.config.lineNumbers; - this.commitParagraph(); - break; - case ParagraphActions.MoveToUp: - this.moveUpParagraph(); - break; - case ParagraphActions.MoveToDown: - this.moveDownParagraph(); - break; - case ParagraphActions.SwitchEnable: - this.paragraph.config.enabled = !this.paragraph.config.enabled; - this.commitParagraph(); - break; - case ParagraphActions.ReduceWidth: - this.paragraph.config.colWidth = Math.max(1, this.paragraph.config.colWidth - 1); - this.cdr.markForCheck(); - this.changeColWidth(true); - break; - case ParagraphActions.IncreaseWidth: - this.paragraph.config.colWidth = Math.min(12, this.paragraph.config.colWidth + 1); - this.cdr.markForCheck(); - this.changeColWidth(true); - break; - case ParagraphActions.Delete: - this.removeParagraph(); + case ParagraphActions.EditMode: + if (this.mode === 'command') { + event.preventDefault(); + } + if (!this.paragraph.config.editorHide) { + this.switchMode('edit'); + } break; - case ParagraphActions.SelectAbove: + case ParagraphActions.Run: event.preventDefault(); - this.selectAtIndex.emit(this.index - 1); + this.runParagraph(); + break; + case ParagraphActions.RunBelow: + this.waitConfirmFromEdit = true; + this.runAllBelowAndCurrent(); break; - case ParagraphActions.SelectBelow: + case ParagraphActions.Cancel: event.preventDefault(); - this.selectAtIndex.emit(this.index + 1); + this.cancelParagraph(); break; default: break; } - } - switch (action) { - case ParagraphActions.EditMode: - if (this.mode === 'command') { - event.preventDefault(); - } - if (!this.paragraph.config.editorHide) { - this.switchMode('edit'); - } - break; - case ParagraphActions.Run: - event.preventDefault(); - this.runParagraph(); - break; - case ParagraphActions.RunBelow: - this.waitConfirmFromEdit = true; - this.runAllBelowAndCurrent(); - break; - case ParagraphActions.Cancel: - event.preventDefault(); - this.cancelParagraph(); - break; - default: - break; - } - }); + }); this.setResults(); this.originalText = this.paragraph.text; @@ -843,12 +878,16 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen ngOnChanges(changes: SimpleChanges): void { const { index, select } = changes; - if (index && index.currentValue !== index.previousValue && this.select - || select && select.currentValue === true && select.previousValue !== true) { + if ( + (index && index.currentValue !== index.previousValue && this.select) || + (select && select.currentValue === true && select.previousValue !== true) + ) { if (this.host.nativeElement) { setTimeout(() => { - (this.host.nativeElement as HTMLElement).focus(); - }) + if (this.mode === 'command') { + (this.host.nativeElement as HTMLElement).focus(); + } + }); } } } diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html index 028c3e5..fe34a37 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html @@ -65,7 +65,7 @@ zeppelinRunScripts [scriptsContent]="innerHTML" [innerHTML]="innerHTML"></div> - <div *ngSwitchCase="datasetType.TEXT" class="text-plain"><pre [innerHTML]="plainText"></pre></div> + <div *ngSwitchCase="datasetType.TEXT" class="text-plain"><pre>{{plainText}}</pre></div> <div *ngSwitchCase="datasetType.IMG" class="img"><img [src]="imgData" alt="img"></div> </ng-container> <div *ngIf="angularComponent"> diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts index 903f72b..742a9fb 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts @@ -16,7 +16,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Injector, Input, @@ -81,6 +80,7 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit, plainText: string | SafeHtml = ''; imgData: string | SafeUrl = ''; tableData = new TableData(); + frontEndError: string; // tslint:disable-next-line:no-any visualizations: any[] = [ { @@ -236,11 +236,16 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit, } renderAngular(): void { - this.runtimeCompilerService.createAndCompileTemplate(this.id, this.result.data).then(data => { - this.angularComponent = data; - // this.angularComponent.moduleFactory - this.cdr.markForCheck(); - }); + try { + this.runtimeCompilerService.createAndCompileTemplate(this.id, this.result.data).then(data => { + this.angularComponent = data; + // this.angularComponent.moduleFactory + this.cdr.markForCheck(); + }); + } catch (e) { + this.frontEndError = e.message; + console.log(e); + } } renderText(): void { diff --git a/zeppelin-web-angular/src/app/services/ng-template-adapter.service.ts b/zeppelin-web-angular/src/app/services/ng-template-adapter.service.ts new file mode 100644 index 0000000..651b709 --- /dev/null +++ b/zeppelin-web-angular/src/app/services/ng-template-adapter.service.ts @@ -0,0 +1,65 @@ +/* + * 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 { Ng1MigrationComponent } from '@zeppelin/share/ng1-migration/ng1-migration.component'; +import { NzModalService } from 'ng-zorro-antd'; +import { Observable } from 'rxjs'; + +export interface NgTemplateCheckResult { + index: number; + match: string; + magic: string; + template: string; + origin: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class NgTemplateAdapterService { + constructor(private nzModalService: NzModalService) {} + preCheck(origin: string): NgTemplateCheckResult | null { + const regexp = /(%angular)([\s\S]*<[\s\S]*>)/im; + const math = regexp.exec(origin); + if (math) { + const index = math.index; + const [output, magic, template] = math; + return { + index, + magic, + template, + origin, + match: output + }; + } + return null; + } + + openMigrationDialog(check: NgTemplateCheckResult): Observable<string> { + const modalRef = this.nzModalService.create({ + nzTitle: 'Angular.js Templates Migration Tool', + nzContent: Ng1MigrationComponent, + nzComponentParams: check, + nzFooter: null, + nzWidth: '980px', + nzStyle: { + top: '45px' + }, + nzBodyStyle: { + padding: '0' + } + }); + + return modalRef.afterClose; + } +} diff --git a/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.html b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.html new file mode 100644 index 0000000..34d948a --- /dev/null +++ b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.html @@ -0,0 +1,54 @@ +<div class="code-editor"> + <zeppelin-code-editor + (nzEditorInitialized)="onEditorInit($event)" + [nzEditorOption]="{ + language: 'html' , + minimap: { + enabled: false + } + }"> + </zeppelin-code-editor> +</div> +<div class="messages"> + + <div class="fix-bar"> + <button nz-button + [disabled]="(messageDetails.length - errorCount) === 0" + class="fix-btn" + nzSize="small" + nzType="link" + (click)="fix()"> + Quick Fix + </button> + <span class="log-counts"> + <span> + <i nz-icon class="error" nzType="stop" nzTheme="outline"></i> + {{errorCount}} + </span> + <span> + <i nz-icon class="close" nzType="issues-close" nzTheme="outline"></i> + {{messageDetails.length - errorCount}} + </span> + </span> + </div> + + <div class="message" + (click)="scrollToLine(item)" + *ngFor="let item of messageDetails"> + <i *ngIf="item.level === 0" nz-icon class="error" nzType="stop" nzTheme="outline"></i> + <i *ngIf="item.level === 2" nz-icon class="close" nzType="issues-close" nzTheme="outline"></i> + <span class="position"> ({{(item.pos.line + 1) + ',' + (item.pos.character + 1)}})</span> + {{item.message}} + <a *ngIf="item.url" [href]="item.url" target="_blank">more</a> + </div> +</div> + +<div *nzModalFooter> + <button nz-button (click)="cancel()">Cancel</button> + <button nz-button + nzType="primary" + [disabled]="this.messageDetails.length" + (click)="updateAndCopy()"> + Update and Copy + </button> +</div> diff --git a/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.less b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.less new file mode 100644 index 0000000..cb1fdc2 --- /dev/null +++ b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.less @@ -0,0 +1,77 @@ +:host { + height: 70vh; + display: flex; + + .code-editor { + flex: auto; + } + + .messages { + overflow: auto; + position: relative; + width: 240px; + border-left: 1px solid #e8e8e8; + + i { + &.error { + color: red; + } + &.close { + color: #1f8ffb; + } + } + + .fix-bar { + padding-right: 16px; + display: flex; + font-size: 12px; + border-bottom: 1px solid #e8e8e8; + height: 25px; + line-height: 25px; + .fix-btn { + flex: 0; + font-size: 12px; + } + .log-counts { + text-align: right; + flex: 1 auto; + } + } + + + .message { + font-family: Consolas, Verdana; + color: #1e1e1e; + padding: 8px 16px 8px 5px; + transition: background-color 0.3s; + word-break: break-all; + line-height: 17px; + cursor: pointer; + font-size: 12px; + .position { + color: #5d5d5d; + } + &:hover { + background-color: #ffb86c; + } + } + } +} + + +::ng-deep { + .monaco-editor { + .scroll-decoration { + box-shadow: none; + } + .decoration-link { + text-decoration-color: red; + text-decoration-line: underline; + text-decoration-style: wavy; + text-decoration-skip-ink: none; + } + .warn-content { + background: rgba(182, 182, 182, .3); + } + } +} diff --git a/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.ts b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.ts new file mode 100644 index 0000000..340330e --- /dev/null +++ b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.ts @@ -0,0 +1,174 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy } from '@angular/core'; +import { editor, IDisposable, Range } from 'monaco-editor'; +import { NzModalRef } from 'ng-zorro-antd'; +import { + defaultTemplateUpdaterRules, + LogLevel, + Message, + MessageDetail, + TemplateUpdater, + ValueChangeRule +} from 'ng1-template-updater'; +import { combineLatest, Subject } from 'rxjs'; +import IEditor = editor.IEditor; +import ITextModel = editor.ITextModel; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; + +const zeppelinFunctionChangeRule: ValueChangeRule = (expression: string, start?: number) => { + let value = expression; + const messages: Message[] = []; + const funChanges = [ + { + regexp: /z\.angularBind/gm, + replace: 'z.set' + }, + { + regexp: /z\.angularUnbind/gm, + replace: 'z.unset' + }, + { + regexp: /z\.runParagraph/gm, + replace: 'z.run' + } + ]; + + funChanges.forEach(change => { + let match = change.regexp.exec(value); + while (match !== null) { + messages.push({ + position: start + match.index, + message: `${match[0]} has been deprecated, using ${change.replace} instead`, + length: match[0].length, + // url: 'https://angular.io/guide/ajs-quick-reference', + level: LogLevel.Info + }); + match = change.regexp.exec(value); + } + value = value.replace(change.regexp, change.replace); + }); + + return { + messages, + value + }; +}; + +@Component({ + selector: 'zeppelin-ng1-migration', + templateUrl: './ng1-migration.component.html', + styleUrls: ['./ng1-migration.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class Ng1MigrationComponent implements OnDestroy { + @Input() origin: string; + @Input() index: number; + @Input() match: string; + @Input() template: string; + + messageDetails: MessageDetail[] = []; + templateUpdater: TemplateUpdater; + errorCount = 0; + decorations: string[] = []; + timeoutId = -1; + editor: IStandaloneCodeEditor; + editorModel: ITextModel; + editorInit$ = new Subject(); + editorChangeDisposable: IDisposable; + + constructor(private nzModalRef: NzModalRef, private cdr: ChangeDetectorRef) { + const updateRules = { + ...defaultTemplateUpdaterRules, + valueChangeRules: [...defaultTemplateUpdaterRules.valueChangeRules, zeppelinFunctionChangeRule] + }; + this.templateUpdater = new TemplateUpdater(updateRules); + combineLatest([this.nzModalRef.afterOpen, this.editorInit$]).subscribe(() => { + if (this.editor) { + this.editorModel = this.editor.getModel() as ITextModel; + this.editor.setValue(this.template); + this.editor.layout(); + this.bindEditorEvents(); + this.check(); + setTimeout(() => { + this.editor.focus(); + }, 150); + } + }); + } + + onEditorInit(_editor: IEditor) { + this.editorInit$.next(); + this.editorInit$.complete(); + this.editor = _editor as IStandaloneCodeEditor; + } + + bindEditorEvents() { + if (this.editorModel) { + this.editorChangeDisposable = this.editorModel.onDidChangeContent(() => { + clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(() => { + this.check(); + }, 300); + }); + } + } + + scrollToLine(failure: MessageDetail) { + const line = failure.pos.line + 1; + const character = failure.pos.character + 1; + const range = new Range(line, character, line, character + failure.length); + this.editor.revealRangeAtTop(range); + this.editor.setSelection(range); + this.editor.focus(); + } + + check() { + const code = this.editor.getValue(); + const { messages } = this.templateUpdater.parse(code); + this.messageDetails = [...messages]; + this.errorCount = messages.filter(f => f.level === LogLevel.Error).length; + this.decorations = this.editor.deltaDecorations( + this.decorations, + messages.map(failure => { + const line = failure.pos.line + 1; + const character = failure.pos.character + 1; + return { + range: new Range(line, character, line, character + failure.length), + options: { + className: failure.level === LogLevel.Error ? '' : 'warn-content', + inlineClassName: failure.level === LogLevel.Error ? 'decoration-link' : '', + stickiness: 1, + hoverMessage: { + value: failure.message + (failure.url ? ` [more](${failure.url})` : '') + } + } + }; + }) + ); + this.cdr.markForCheck(); + } + + fix() { + const code = this.editor.getValue(); + const { template } = this.templateUpdater.parse(code); + this.editor.setValue(template); + } + + updateAndCopy() { + const code = this.editor.getValue(); + const newTemplate = this.origin.replace(this.match, `%ng\n${code}`); + this.nzModalRef.close(newTemplate); + } + + cancel() { + this.nzModalRef.destroy(); + } + + ngOnDestroy(): void { + if (this.editorChangeDisposable) { + this.editorChangeDisposable.dispose(); + } + if (this.editorModel) { + this.editorModel.dispose(); + } + } +} diff --git a/zeppelin-web-angular/src/app/share/share.module.ts b/zeppelin-web-angular/src/app/share/share.module.ts index ed5d191..fcb0375 100644 --- a/zeppelin-web-angular/src/app/share/share.module.ts +++ b/zeppelin-web-angular/src/app/share/share.module.ts @@ -52,6 +52,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 { Ng1MigrationComponent } from './ng1-migration/ng1-migration.component'; import { ResizeHandleComponent } from './resize-handle'; const MODAL_LIST = [ @@ -59,7 +60,8 @@ const MODAL_LIST = [ NoteImportComponent, NoteCreateComponent, NoteRenameComponent, - FolderRenameComponent + FolderRenameComponent, + Ng1MigrationComponent ]; const EXPORT_LIST = [HeaderComponent, NodeListComponent, PageHeaderComponent, SpinComponent, ResizeHandleComponent]; const PIPES = [HumanizeBytesPipe];