You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2020/01/12 06:38:20 UTC

[zeppelin] 03/16: [ZEPPELIN-4450] Provide Angular.js Template Migration Tool

This is an automated email from the ASF dual-hosted git repository.

zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git

commit 1e0177d132f8458aa6beff570c380b7b4740399f
Author: Hsuan Lee <hs...@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)
    
    ![ng1-template](https://user-images.githubusercontent.com/22736418/69597301-78de3000-1040-11ea-85a6-830d573f1305.gif)
    
    ### Questions:
    * Does the licenses files need update? NO
    * Is there breaking changes for older versions? NO
    * Does this needs documentation? NO
    
    Author: Hsuan Lee <hs...@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];