diff --git a/angular/src/main/resources/interpreter-setting.json b/angular/src/main/resources/interpreter-setting.json index 723348d25e9..957295f8322 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 85ad2a6251f..cd5fb2fd43f 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 63b6be17d43..9d9aa934a3f 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 4d382f2b82f..d543e670d49 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 @@ --> + [class.focused]="focus" + [class.dirty]="dirty" + (nzEditorInitialized)="initializedEditor($event)"> 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 72a1f689dc8..8f61bd5b93f 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 0711e816542..916afefd9e3 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 5b959539d92..bda003d8687 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 1dde62dae65..f5b61e8006f 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.' + + ' (more info)', + 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> = []; + 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 028c3e50089..fe34a372d69 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"> -
+
{{plainText}}
img
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 903f72bccc5..742a9fb29a8 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 00000000000..651b709019c --- /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 { + 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 00000000000..34d948ab9f7 --- /dev/null +++ b/zeppelin-web-angular/src/app/share/ng1-migration/ng1-migration.component.html @@ -0,0 +1,54 @@ +
+ + +
+
+ +
+ + + + + {{errorCount}} + + + + {{messageDetails.length - errorCount}} + + +
+ +
+ + + ({{(item.pos.line + 1) + ',' + (item.pos.character + 1)}}) + {{item.message}} + more +
+
+ +
+ + +
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 00000000000..cb1fdc26721 --- /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 00000000000..340330e92ff --- /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 ed5d191736a..fcb03751092 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];