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:21 UTC

[zeppelin] 04/16: [ZEPPELIN-4403] Support publishable for paragraph

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 66f4dd1489ca738bdee7757ff64a6e63b348589d
Author: Hsuan Lee <hs...@gmail.com>
AuthorDate: Fri Nov 29 17:16:34 2019 +0800

    [ZEPPELIN-4403] Support publishable for paragraph
    
    ### What is this PR for?
    A few sentences describing the overall goals of the pull request's commits.
    First time? Check out the contributing guide - https://zeppelin.apache.org/contribution/contributions.html
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    *
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-4403
    
    ### 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)
    
    ![publish](https://user-images.githubusercontent.com/22736418/69940879-4770d300-151e-11ea-9ed1-2925ce825e58.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 #3530 from hsuanxyz/paragraph-publishable and squashes the following commits:
    
    a9b974c1d [Hsuan Lee] feat: support paragraph publishable
---
 .../{public-api.ts => paragraph-base/index.ts}     |   4 +-
 .../src/app/core/paragraph-base/paragraph-base.ts  | 315 +++++++++++++++++++++
 .../app/core/{ => paragraph-base}/public-api.ts    |   5 +-
 .../{public-api.ts => paragraph-base/published.ts} |   8 +-
 zeppelin-web-angular/src/app/core/public-api.ts    |   1 +
 .../workspace/notebook/notebook-routing.module.ts  |   4 -
 .../pages/workspace/notebook/notebook.component.ts |  13 +-
 .../pages/workspace/notebook/notebook.module.ts    |  14 +-
 .../paragraph/code-editor/code-editor.component.ts |  14 -
 .../paragraph/control/control.component.ts         |  17 +-
 .../notebook/paragraph/paragraph.component.html    |   1 +
 .../notebook/paragraph/paragraph.component.ts      | 305 ++------------------
 .../published/paragraph/paragraph.component.html   |  15 +
 .../published/paragraph/paragraph.component.less   |   0
 .../published/paragraph/paragraph.component.ts     |  88 ++++++
 .../published-ruoting.module.ts}                   |  17 +-
 .../pages/workspace/published/published.module.ts  |  11 +
 .../dynamic-forms/dynamic-forms.component.html     |   0
 .../dynamic-forms/dynamic-forms.component.less     |   0
 .../dynamic-forms/dynamic-forms.component.ts       |   0
 .../workspace/share/index.ts}                      |   4 +-
 .../{core => pages/workspace/share}/public-api.ts  |   4 +-
 .../result/result.component.html                   |   6 +-
 .../result/result.component.less                   |   0
 .../paragraph => share}/result/result.component.ts |   4 +
 .../src/app/pages/workspace/share/share.module.ts  |  56 ++++
 .../pages/workspace/workspace-routing.module.ts    |   4 +
 .../app/pages/workspace/workspace.component.html   |   4 +-
 .../src/app/pages/workspace/workspace.component.ts |  11 +-
 .../src/app/services/shortcut.service.ts           |  37 +--
 30 files changed, 581 insertions(+), 381 deletions(-)

diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/core/paragraph-base/index.ts
similarity index 85%
copy from zeppelin-web-angular/src/app/core/public-api.ts
copy to zeppelin-web-angular/src/app/core/paragraph-base/index.ts
index c514103..49e4740 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/index.ts
@@ -10,6 +10,4 @@
  * limitations under the License.
  */
 
-export * from './message-listener';
-export * from './destroy-hook';
-export * from './copy-text';
+export * from './public-api';
diff --git a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
new file mode 100644
index 0000000..f8f9a1b
--- /dev/null
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
@@ -0,0 +1,315 @@
+/*
+ * 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 { ChangeDetectorRef, QueryList } from '@angular/core';
+
+import {
+  AngularObjectRemove,
+  AngularObjectUpdate,
+  GraphConfig,
+  MessageReceiveDataTypeMap,
+  OP,
+  ParagraphConfig,
+  ParagraphEditorSetting,
+  ParagraphItem,
+  ParagraphIResultsMsgItem
+} from '@zeppelin/sdk';
+
+import { MessageService } from '@zeppelin/services/message.service';
+import { NgZService } from '@zeppelin/services/ng-z.service';
+import { NoteStatusService, ParagraphStatus } from '@zeppelin/services/note-status.service';
+
+import DiffMatchPatch from 'diff-match-patch';
+import { isEmpty, isEqual } from 'lodash';
+
+import { NotebookParagraphResultComponent } from '@zeppelin/pages/workspace/share/result/result.component';
+import { MessageListener, MessageListenersManager } from '../message-listener/message-listener';
+
+export abstract class ParagraphBase extends MessageListenersManager {
+  paragraph: ParagraphItem;
+  dirtyText: string;
+  originalText: string;
+  isEntireNoteRunning = false;
+  revisionView = false;
+  diffMatchPatch = new DiffMatchPatch();
+  isParagraphRunning = false;
+  results = [];
+  configs = {};
+  progress = 0;
+  colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
+  editorSetting: ParagraphEditorSetting = {};
+
+  notebookParagraphResultComponents: QueryList<NotebookParagraphResultComponent>;
+
+  constructor(
+    public messageService: MessageService,
+    protected noteStatusService: NoteStatusService,
+    protected ngZService: NgZService,
+    protected cdr: ChangeDetectorRef
+  ) {
+    super(messageService);
+  }
+
+  abstract changeColWidth(needCommit: boolean, updateResult?: boolean): void;
+
+  @MessageListener(OP.PROGRESS)
+  onProgress(data: MessageReceiveDataTypeMap[OP.PROGRESS]) {
+    if (data.id === this.paragraph.id) {
+      this.progress = data.progress;
+      this.cdr.markForCheck();
+    }
+  }
+
+  @MessageListener(OP.NOTE_RUNNING_STATUS)
+  noteRunningStatusChange(data: MessageReceiveDataTypeMap[OP.NOTE_RUNNING_STATUS]) {
+    this.isEntireNoteRunning = data.status;
+    this.cdr.markForCheck();
+  }
+
+  @MessageListener(OP.PARAS_INFO)
+  updateParaInfos(data: MessageReceiveDataTypeMap[OP.PARAS_INFO]) {
+    if (this.paragraph.id === data.id) {
+      this.paragraph.runtimeInfos = data.infos;
+      this.cdr.markForCheck();
+    }
+  }
+
+  @MessageListener(OP.EDITOR_SETTING)
+  getEditorSetting(data: MessageReceiveDataTypeMap[OP.EDITOR_SETTING]) {
+    if (this.paragraph.id === data.paragraphId) {
+      this.paragraph.config.editorSetting = { ...this.paragraph.config.editorSetting, ...data.editor };
+      this.cdr.markForCheck();
+    }
+  }
+
+  @MessageListener(OP.PARAGRAPH)
+  paragraphData(data: MessageReceiveDataTypeMap[OP.PARAGRAPH]) {
+    const oldPara = this.paragraph;
+    const newPara = data.paragraph;
+    if (this.isUpdateRequired(oldPara, newPara)) {
+      this.updateParagraph(oldPara, newPara, () => {
+        if (newPara.results && newPara.results.msg) {
+          // tslint:disable-next-line:no-for-in-array
+          for (const i in newPara.results.msg) {
+            if (newPara.results.msg[i]) {
+              const newResult = newPara.results.msg ? newPara.results.msg[i] : new ParagraphIResultsMsgItem();
+              const oldResult =
+                oldPara.results && oldPara.results.msg ? oldPara.results.msg[i] : new ParagraphIResultsMsgItem();
+              const newConfig = newPara.config.results ? newPara.config.results[i] : { graph: new GraphConfig() };
+              const oldConfig = oldPara.config.results ? oldPara.config.results[i] : { graph: new GraphConfig() };
+              if (!isEqual(newResult, oldResult) || !isEqual(newConfig, oldConfig)) {
+                const resultComponent = this.notebookParagraphResultComponents.toArray()[i];
+                if (resultComponent) {
+                  resultComponent.updateResult(newConfig, newResult);
+                }
+              }
+            }
+          }
+        }
+        this.cdr.markForCheck();
+      });
+      this.cdr.markForCheck();
+    }
+  }
+
+  @MessageListener(OP.PATCH_PARAGRAPH)
+  patchParagraph(data: MessageReceiveDataTypeMap[OP.PATCH_PARAGRAPH]) {
+    if (data.paragraphId === this.paragraph.id) {
+      let patch = data.patch;
+      patch = this.diffMatchPatch.patch_fromText(patch);
+      if (!this.paragraph.text) {
+        this.paragraph.text = '';
+      }
+      this.paragraph.text = this.diffMatchPatch.patch_apply(patch, this.paragraph.text)[0];
+      this.originalText = this.paragraph.text;
+      this.cdr.markForCheck();
+    }
+  }
+
+  @MessageListener(OP.ANGULAR_OBJECT_UPDATE)
+  angularObjectUpdate(data: AngularObjectUpdate) {
+    if (data.paragraphId === this.paragraph.id) {
+      const { name, object } = data.angularObject;
+      this.ngZService.setContextValue(name, object, data.paragraphId, false);
+    }
+  }
+
+  @MessageListener(OP.ANGULAR_OBJECT_REMOVE)
+  angularObjectRemove(data: AngularObjectRemove) {
+    if (data.paragraphId === this.paragraph.id) {
+      this.ngZService.unsetContextValue(data.name, data.paragraphId, false);
+    }
+  }
+
+  updateParagraph(oldPara: ParagraphItem, newPara: ParagraphItem, updateCallback: () => void) {
+    // 1. can't update on revision view
+    if (!this.revisionView) {
+      // 2. get status, refreshed
+      const statusChanged = newPara.status !== oldPara.status;
+      const resultRefreshed =
+        newPara.dateFinished !== oldPara.dateFinished ||
+        isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
+        newPara.status === ParagraphStatus.ERROR ||
+        (newPara.status === ParagraphStatus.FINISHED && statusChanged);
+
+      // 3. update texts managed by paragraph
+      this.updateAllScopeTexts(oldPara, newPara);
+      // 4. execute callback to update result
+      updateCallback();
+
+      // 5. update remaining paragraph objects
+      this.updateParagraphObjectWhenUpdated(newPara);
+
+      // 6. handle scroll down by key properly if new paragraph is added
+      if (statusChanged || resultRefreshed) {
+        // when last paragraph runs, zeppelin automatically appends new paragraph.
+        // this broadcast will focus to the newly inserted paragraph
+        // TODO(hsuanxyz)
+      }
+      this.cdr.markForCheck();
+    }
+  }
+
+  isUpdateRequired(oldPara: ParagraphItem, newPara: ParagraphItem): boolean {
+    return (
+      newPara.id === oldPara.id &&
+      (newPara.dateCreated !== oldPara.dateCreated ||
+        newPara.text !== oldPara.text ||
+        newPara.dateFinished !== oldPara.dateFinished ||
+        newPara.dateStarted !== oldPara.dateStarted ||
+        newPara.dateUpdated !== oldPara.dateUpdated ||
+        newPara.status !== oldPara.status ||
+        newPara.jobName !== oldPara.jobName ||
+        newPara.title !== oldPara.title ||
+        isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
+        newPara.errorMessage !== oldPara.errorMessage ||
+        !isEqual(newPara.settings, oldPara.settings) ||
+        !isEqual(newPara.config, oldPara.config) ||
+        !isEqual(newPara.runtimeInfos, oldPara.runtimeInfos))
+    );
+  }
+
+  updateAllScopeTexts(oldPara: ParagraphItem, newPara: ParagraphItem) {
+    if (oldPara.text !== newPara.text) {
+      if (this.dirtyText) {
+        // check if editor has local update
+        if (this.dirtyText === newPara.text) {
+          // when local update is the same from remote, clear local update
+          this.paragraph.text = newPara.text;
+          this.dirtyText = undefined;
+          this.originalText = newPara.text;
+        } else {
+          // if there're local update, keep it.
+          this.paragraph.text = newPara.text;
+        }
+      } else {
+        this.paragraph.text = newPara.text;
+        this.originalText = newPara.text;
+      }
+    }
+    this.cdr.markForCheck();
+  }
+
+  updateParagraphObjectWhenUpdated(newPara: ParagraphItem) {
+    if (this.paragraph.config.colWidth !== newPara.config.colWidth) {
+      this.changeColWidth(false);
+    }
+    this.paragraph.aborted = newPara.aborted;
+    this.paragraph.user = newPara.user;
+    this.paragraph.dateUpdated = newPara.dateUpdated;
+    this.paragraph.dateCreated = newPara.dateCreated;
+    this.paragraph.dateFinished = newPara.dateFinished;
+    this.paragraph.dateStarted = newPara.dateStarted;
+    this.paragraph.errorMessage = newPara.errorMessage;
+    this.paragraph.jobName = newPara.jobName;
+    this.paragraph.title = newPara.title;
+    this.paragraph.lineNumbers = newPara.lineNumbers;
+    this.paragraph.status = newPara.status;
+    this.paragraph.fontSize = newPara.fontSize;
+    if (newPara.status !== ParagraphStatus.RUNNING) {
+      this.paragraph.results = newPara.results;
+    }
+    this.paragraph.settings = newPara.settings;
+    this.paragraph.runtimeInfos = newPara.runtimeInfos;
+    this.isParagraphRunning = this.noteStatusService.isParagraphRunning(newPara);
+    this.paragraph.config = newPara.config;
+    this.initializeDefault(this.paragraph.config);
+    this.setResults();
+    this.cdr.markForCheck();
+  }
+
+  setResults() {
+    if (this.paragraph.results) {
+      this.results = this.paragraph.results.msg;
+      this.configs = this.paragraph.config.results;
+    }
+    if (!this.paragraph.config) {
+      this.paragraph.config = {};
+    }
+  }
+
+  initializeDefault(config: ParagraphConfig) {
+    const forms = this.paragraph.settings.forms;
+
+    if (!config.colWidth) {
+      config.colWidth = 12;
+    }
+
+    if (!config.fontSize) {
+      config.fontSize = 9;
+    }
+
+    if (config.enabled === undefined) {
+      config.enabled = true;
+    }
+
+    for (const idx in forms) {
+      if (forms[idx]) {
+        if (forms[idx].options) {
+          if (config.runOnSelectionChange === undefined) {
+            config.runOnSelectionChange = true;
+          }
+        }
+      }
+    }
+
+    if (!config.results) {
+      config.results = {};
+    }
+
+    if (!config.editorSetting) {
+      config.editorSetting = {};
+    } else if (config.editorSetting.editOnDblClick) {
+      this.editorSetting.isOutputHidden = config.editorSetting.editOnDblClick;
+    }
+  }
+
+  runParagraphUsingSpell(paragraphText: string, magic: string, propagated: boolean) {
+    // TODO(hsuanxyz)
+  }
+
+  runParagraphUsingBackendInterpreter(paragraphText: string) {
+    this.messageService.runParagraph(
+      this.paragraph.id,
+      this.paragraph.title,
+      paragraphText,
+      this.paragraph.config,
+      this.paragraph.settings.params
+    );
+  }
+
+  cancelParagraph() {
+    if (!this.isEntireNoteRunning) {
+      this.messageService.cancelParagraph(this.paragraph.id);
+    }
+  }
+}
diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/core/paragraph-base/public-api.ts
similarity index 85%
copy from zeppelin-web-angular/src/app/core/public-api.ts
copy to zeppelin-web-angular/src/app/core/paragraph-base/public-api.ts
index c514103..a6ba532 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/public-api.ts
@@ -10,6 +10,5 @@
  * limitations under the License.
  */
 
-export * from './message-listener';
-export * from './destroy-hook';
-export * from './copy-text';
+export * from './paragraph-base';
+export * from './published';
diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/core/paragraph-base/published.ts
similarity index 82%
copy from zeppelin-web-angular/src/app/core/public-api.ts
copy to zeppelin-web-angular/src/app/core/paragraph-base/published.ts
index c514103..0f41577 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/published.ts
@@ -10,6 +10,8 @@
  * limitations under the License.
  */
 
-export * from './message-listener';
-export * from './destroy-hook';
-export * from './copy-text';
+export const publishedSymbol = Symbol('published');
+
+export interface Published {
+  readonly [publishedSymbol]: true;
+}
diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/core/public-api.ts
index c514103..3bcd355 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/core/public-api.ts
@@ -13,3 +13,4 @@
 export * from './message-listener';
 export * from './destroy-hook';
 export * from './copy-text';
+export * from './paragraph-base';
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts
index 6c177b6..321b788 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts
@@ -21,10 +21,6 @@ const routes: Routes = [
     component: NotebookComponent
   },
   {
-    path: ':noteId/paragraph/:paragraphId',
-    component: NotebookComponent
-  },
-  {
     path: ':noteId/revision/:revisionId',
     component: NotebookComponent
   }
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index 97479a7..e0c17a1 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -21,7 +21,7 @@ import {
 } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { isNil } from 'lodash';
-import { Subject} from 'rxjs';
+import { Subject } from 'rxjs';
 import { distinctUntilKeyChanged, takeUntil } from 'rxjs/operators';
 
 import { MessageListener, MessageListenersManager } from '@zeppelin/core';
@@ -29,6 +29,7 @@ import { Permissions } from '@zeppelin/interfaces';
 import { InterpreterBindingItem, MessageReceiveDataTypeMap, Note, OP, RevisionListItem } from '@zeppelin/sdk';
 import {
   MessageService,
+  NgZService,
   NoteStatusService,
   NoteVarShareService,
   SecurityService,
@@ -66,6 +67,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     if (isNil(note)) {
       this.router.navigate(['/']).then();
     } else {
+      this.removeParagraphFromNgZ();
       this.note = note;
       const { paragraphId } = this.activatedRoute.snapshot.params;
       if (paragraphId) {
@@ -289,6 +291,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     private ticketService: TicketService,
     private securityService: SecurityService,
     private router: Router,
+    protected ngZService: NgZService
   ) {
     super(messageService);
   }
@@ -317,6 +320,14 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     this.revisionView = !!this.activatedRoute.snapshot.params.revisionId;
   }
 
+  removeParagraphFromNgZ(): void {
+    if (this.note && Array.isArray(this.note.paragraphs)) {
+      this.note.paragraphs.forEach(p => {
+        this.ngZService.removeParagraph(p.id);
+      });
+    }
+  }
+
   ngOnDestroy(): void {
     super.ngOnDestroy();
     this.killSaveTimer();
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.module.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.module.ts
index ccdd4de..0258bbe 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.module.ts
@@ -18,7 +18,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
 import {
   NzButtonModule,
-  NzCheckboxModule,
   NzDividerModule,
   NzDropDownModule,
   NzFormModule,
@@ -35,23 +34,20 @@ import {
   NzToolTipModule
 } from 'ng-zorro-antd';
 import { NzCodeEditorModule } from 'ng-zorro-antd/code-editor';
-import { NzResizableModule } from 'ng-zorro-antd/resizable';
 
 import { ShareModule } from '@zeppelin/share';
 
-import { VisualizationModule } from 'src/app/visualizations/visualization.module';
 import { NotebookAddParagraphComponent } from './add-paragraph/add-paragraph.component';
 import { NotebookInterpreterBindingComponent } from './interpreter-binding/interpreter-binding.component';
 import { NotebookParagraphCodeEditorComponent } from './paragraph/code-editor/code-editor.component';
 import { NotebookParagraphControlComponent } from './paragraph/control/control.component';
-import { NotebookParagraphDynamicFormsComponent } from './paragraph/dynamic-forms/dynamic-forms.component';
 import { NotebookParagraphFooterComponent } from './paragraph/footer/footer.component';
 import { NotebookParagraphComponent } from './paragraph/paragraph.component';
 import { NotebookParagraphProgressComponent } from './paragraph/progress/progress.component';
-import { NotebookParagraphResultComponent } from './paragraph/result/result.component';
 import { NotebookPermissionsComponent } from './permissions/permissions.component';
 import { NotebookRevisionsComparatorComponent } from './revisions-comparator/revisions-comparator.component';
 
+import { WorkspaceShareModule } from '../../workspace/share/share.module';
 import { NotebookActionBarComponent } from './action-bar/action-bar.component';
 import { NotebookRoutingModule } from './notebook-routing.module';
 import { NotebookComponent } from './notebook.component';
@@ -67,18 +63,16 @@ import { NotebookShareModule } from './share/share.module';
     NotebookParagraphComponent,
     NotebookAddParagraphComponent,
     NotebookParagraphCodeEditorComponent,
-    NotebookParagraphResultComponent,
     NotebookParagraphProgressComponent,
     NotebookParagraphFooterComponent,
-    NotebookParagraphControlComponent,
-    NotebookParagraphDynamicFormsComponent
+    NotebookParagraphControlComponent
   ],
   imports: [
     CommonModule,
     PortalModule,
+    WorkspaceShareModule,
     NotebookRoutingModule,
     ShareModule,
-    VisualizationModule,
     NotebookShareModule,
     NzButtonModule,
     NzIconModule,
@@ -92,14 +86,12 @@ import { NotebookShareModule } from './share/share.module';
     FormsModule,
     ReactiveFormsModule,
     NzDividerModule,
-    NzCheckboxModule,
     NzProgressModule,
     NzSwitchModule,
     NzSelectModule,
     NzGridModule,
     NzRadioModule,
     DragDropModule,
-    NzResizableModule,
     NzCodeEditorModule
   ]
 })
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 916afef..8cf4bd7 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
@@ -104,20 +104,6 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
 
   initializedEditor(editor: IEditor) {
     this.editor = editor as IStandaloneCodeEditor;
-    if (this.paragraphControl) {
-      this.paragraphControl.listOfMenu.forEach((item, index) => {
-        this.editor.addAction({
-          id: item.icon,
-          label: item.label,
-          precondition: null,
-          keybindingContext: null,
-          contextMenuGroupId: 'navigation',
-          contextMenuOrder: index,
-          run: () => item.trigger()
-        });
-      });
-    }
-
     this.editor.addCommand(
       monaco.KeyCode.Escape,
       () => {
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 bda003d..eacf2da 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
@@ -72,6 +72,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
   @Output() readonly runAllBelowAndCurrent = new EventEmitter<void>();
   @Output() readonly cloneParagraph = new EventEmitter<void>();
   @Output() readonly removeParagraph = new EventEmitter<void>();
+  @Output() readonly openSingleParagraph = new EventEmitter<string>();
   fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
   dropdownVisible = false;
   isMac = navigator.appVersion.indexOf('Mac') !== -1;
@@ -115,8 +116,10 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
         show: true,
         disabled: false,
         icon: 'export',
-        trigger: () => this.goToSingleParagraph(),
-        shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+W`
+        trigger: () => {
+          this.openSingleParagraph.emit(this.pid);
+        },
+        shortCut: this.isMac ? '⌥+⌘+T' : 'Alt+Ctrl+T'
       },
       {
         label: 'Clear output',
@@ -225,13 +228,6 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
     }
   }
 
-  goToSingleParagraph() {
-    // TODO(hsuanxyz) asIframe
-    const { noteId } = this.activatedRoute.snapshot.params;
-    const redirectToUrl = `${location.protocol}//${location.host}${location.pathname}#/notebook/${noteId}/paragraph/${this.pid}`;
-    window.open(redirectToUrl);
-  }
-
   changeColWidth(colWidth: number) {
     this.colWidth = +colWidth;
     this.colWidthChange.emit(this.colWidth);
@@ -269,8 +265,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
     private cdr: ChangeDetectorRef,
     private nzMessageService: NzMessageService,
     private activatedRoute: ActivatedRoute,
-    private messageService: MessageService,
-    private nzModalService: NzModalService
+    private messageService: MessageService
   ) {}
 
   ngOnInit() {
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
index 1c42f87..e286627 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
@@ -48,6 +48,7 @@
                                          (editorHideChange)="commitParagraph()"
                                          (enabledChange)="commitParagraph()"
                                          (titleChange)="commitParagraph()"
+                                         (openSingleParagraph)="openSingleParagraph($event)"
                                          (runOnSelectionChangeChange)="commitParagraph()"
                                          (runParagraph)="runParagraph()"
                                          (moveUp)="moveUpParagraph()"
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 f5b61e8..246d09e 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
@@ -29,25 +29,10 @@ import {
 import { merge, Observable, Subject } from 'rxjs';
 import { map, takeUntil } from 'rxjs/operators';
 
-import DiffMatchPatch from 'diff-match-patch';
-import { isEmpty, isEqual } from 'lodash';
 import { NzModalService } from 'ng-zorro-antd/modal';
 
-import { MessageListener, MessageListenersManager } from '@zeppelin/core';
-import {
-  AngularObjectRemove,
-  AngularObjectUpdate,
-  GraphConfig,
-  InterpreterBindingItem,
-  MessageReceiveDataTypeMap,
-  Note,
-  OP,
-  ParagraphConfig,
-  ParagraphConfigResult,
-  ParagraphEditorSetting,
-  ParagraphItem,
-  ParagraphIResultsMsgItem
-} from '@zeppelin/sdk';
+import { ParagraphBase } from '@zeppelin/core';
+import { InterpreterBindingItem, Note, ParagraphConfigResult, ParagraphItem } from '@zeppelin/sdk';
 import {
   HeliumService,
   MessageService,
@@ -55,7 +40,6 @@ import {
   NoteStatusService,
   NoteVarShareService,
   ParagraphActions,
-  ParagraphStatus,
   ShortcutsMap,
   ShortcutService
 } from '@zeppelin/services';
@@ -63,8 +47,8 @@ import { SpellResult } from '@zeppelin/spell/spell-result';
 
 import { NgTemplateAdapterService } from '@zeppelin/services/ng-template-adapter.service';
 import { NzResizeEvent } from 'ng-zorro-antd/resizable';
+import { NotebookParagraphResultComponent } from '../../share/result/result.component';
 import { NotebookParagraphCodeEditorComponent } from './code-editor/code-editor.component';
-import { NotebookParagraphResultComponent } from './result/result.component';
 
 type Mode = 'edit' | 'command';
 
@@ -78,7 +62,7 @@ type Mode = 'edit' | 'command';
   },
   changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy {
+export class NotebookParagraphComponent extends ParagraphBase implements OnInit, OnChanges, OnDestroy {
   @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
   notebookParagraphCodeEditorComponent: NotebookParagraphCodeEditorComponent;
   @ViewChildren(NotebookParagraphResultComponent) notebookParagraphResultComponents: QueryList<
@@ -103,105 +87,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
   private destroy$ = new Subject();
   private mode: Mode = 'command';
   waitConfirmFromEdit = false;
-  dirtyText: string;
-  originalText: string;
-  isEntireNoteRunning = false;
-  diffMatchPatch = new DiffMatchPatch();
-  isParagraphRunning = false;
-  results = [];
-  configs = {};
-  progress = 0;
-  colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
-  editorSetting: ParagraphEditorSetting = {};
-
-  @MessageListener(OP.PROGRESS)
-  onProgress(data: MessageReceiveDataTypeMap[OP.PROGRESS]) {
-    if (data.id === this.paragraph.id) {
-      this.progress = data.progress;
-      this.cdr.markForCheck();
-    }
-  }
-
-  @MessageListener(OP.NOTE_RUNNING_STATUS)
-  noteRunningStatusChange(data: MessageReceiveDataTypeMap[OP.NOTE_RUNNING_STATUS]) {
-    this.isEntireNoteRunning = data.status;
-    this.cdr.markForCheck();
-  }
-
-  @MessageListener(OP.PARAS_INFO)
-  updateParaInfos(data: MessageReceiveDataTypeMap[OP.PARAS_INFO]) {
-    if (this.paragraph.id === data.id) {
-      this.paragraph.runtimeInfos = data.infos;
-      this.cdr.markForCheck();
-    }
-  }
-
-  @MessageListener(OP.EDITOR_SETTING)
-  getEditorSetting(data: MessageReceiveDataTypeMap[OP.EDITOR_SETTING]) {
-    if (this.paragraph.id === data.paragraphId) {
-      this.paragraph.config.editorSetting = { ...this.paragraph.config.editorSetting, ...data.editor };
-      this.cdr.markForCheck();
-    }
-  }
-
-  @MessageListener(OP.PARAGRAPH)
-  paragraphData(data: MessageReceiveDataTypeMap[OP.PARAGRAPH]) {
-    const oldPara = this.paragraph;
-    const newPara = data.paragraph;
-    if (this.isUpdateRequired(oldPara, newPara)) {
-      this.updateParagraph(oldPara, newPara, () => {
-        if (newPara.results && newPara.results.msg) {
-          // tslint:disable-next-line:no-for-in-array
-          for (const i in newPara.results.msg) {
-            if (newPara.results.msg[i]) {
-              const newResult = newPara.results.msg ? newPara.results.msg[i] : new ParagraphIResultsMsgItem();
-              const oldResult =
-                oldPara.results && oldPara.results.msg ? oldPara.results.msg[i] : new ParagraphIResultsMsgItem();
-              const newConfig = newPara.config.results ? newPara.config.results[i] : { graph: new GraphConfig() };
-              const oldConfig = oldPara.config.results ? oldPara.config.results[i] : { graph: new GraphConfig() };
-              if (!isEqual(newResult, oldResult) || !isEqual(newConfig, oldConfig)) {
-                const resultComponent = this.notebookParagraphResultComponents.toArray()[i];
-                if (resultComponent) {
-                  resultComponent.updateResult(newConfig, newResult);
-                }
-              }
-            }
-          }
-        }
-        this.cdr.markForCheck();
-      });
-      this.cdr.markForCheck();
-    }
-  }
-
-  @MessageListener(OP.PATCH_PARAGRAPH)
-  patchParagraph(data: MessageReceiveDataTypeMap[OP.PATCH_PARAGRAPH]) {
-    if (data.paragraphId === this.paragraph.id) {
-      let patch = data.patch;
-      patch = this.diffMatchPatch.patch_fromText(patch);
-      if (!this.paragraph.text) {
-        this.paragraph.text = '';
-      }
-      this.paragraph.text = this.diffMatchPatch.patch_apply(patch, this.paragraph.text)[0];
-      this.originalText = this.paragraph.text;
-      this.cdr.markForCheck();
-    }
-  }
-
-  @MessageListener(OP.ANGULAR_OBJECT_UPDATE)
-  angularObjectUpdate(data: AngularObjectUpdate) {
-    if (data.paragraphId === this.paragraph.id) {
-      const { name, object } = data.angularObject;
-      this.ngZService.setContextValue(name, object, data.paragraphId, false);
-    }
-  }
-
-  @MessageListener(OP.ANGULAR_OBJECT_REMOVE)
-  angularObjectRemove(data: AngularObjectRemove) {
-    if (data.paragraphId === this.paragraph.id) {
-      this.ngZService.unsetContextValue(data.name, data.paragraphId, false);
-    }
-  }
 
   switchMode(mode: Mode): void {
     if (mode === this.mode) {
@@ -215,35 +100,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     }
   }
 
-  updateParagraph(oldPara: ParagraphItem, newPara: ParagraphItem, updateCallback: () => void) {
-    // 1. can't update on revision view
-    if (!this.revisionView) {
-      // 2. get status, refreshed
-      const statusChanged = newPara.status !== oldPara.status;
-      const resultRefreshed =
-        newPara.dateFinished !== oldPara.dateFinished ||
-        isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
-        newPara.status === ParagraphStatus.ERROR ||
-        (newPara.status === ParagraphStatus.FINISHED && statusChanged);
-
-      // 3. update texts managed by paragraph
-      this.updateAllScopeTexts(oldPara, newPara);
-      // 4. execute callback to update result
-      updateCallback();
-
-      // 5. update remaining paragraph objects
-      this.updateParagraphObjectWhenUpdated(newPara);
-
-      // 6. handle scroll down by key properly if new paragraph is added
-      if (statusChanged || resultRefreshed) {
-        // when last paragraph runs, zeppelin automatically appends new paragraph.
-        // this broadcast will focus to the newly inserted paragraph
-        // TODO(hsuanxyz)
-      }
-      this.cdr.markForCheck();
-    }
-  }
-
   textChanged(text: string) {
     this.dirtyText = text;
     this.paragraph.text = text;
@@ -472,94 +328,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     }
   }
 
-  runParagraphUsingSpell(paragraphText: string, magic: string, propagated: boolean) {
-    // TODO(hsuanxyz)
-  }
-
-  runParagraphUsingBackendInterpreter(paragraphText: string) {
-    this.messageService.runParagraph(
-      this.paragraph.id,
-      this.paragraph.title,
-      paragraphText,
-      this.paragraph.config,
-      this.paragraph.settings.params
-    );
-  }
-
-  cancelParagraph() {
-    if (!this.isEntireNoteRunning) {
-      this.messageService.cancelParagraph(this.paragraph.id);
-    }
-  }
-
-  updateAllScopeTexts(oldPara: ParagraphItem, newPara: ParagraphItem) {
-    if (oldPara.text !== newPara.text) {
-      if (this.dirtyText) {
-        // check if editor has local update
-        if (this.dirtyText === newPara.text) {
-          // when local update is the same from remote, clear local update
-          this.paragraph.text = newPara.text;
-          this.dirtyText = undefined;
-          this.originalText = newPara.text;
-        } else {
-          // if there're local update, keep it.
-          this.paragraph.text = newPara.text;
-        }
-      } else {
-        this.paragraph.text = newPara.text;
-        this.originalText = newPara.text;
-      }
-    }
-    this.cdr.markForCheck();
-  }
-
-  updateParagraphObjectWhenUpdated(newPara: ParagraphItem) {
-    if (this.paragraph.config.colWidth !== newPara.config.colWidth) {
-      this.changeColWidth(false);
-    }
-    this.paragraph.aborted = newPara.aborted;
-    this.paragraph.user = newPara.user;
-    this.paragraph.dateUpdated = newPara.dateUpdated;
-    this.paragraph.dateCreated = newPara.dateCreated;
-    this.paragraph.dateFinished = newPara.dateFinished;
-    this.paragraph.dateStarted = newPara.dateStarted;
-    this.paragraph.errorMessage = newPara.errorMessage;
-    this.paragraph.jobName = newPara.jobName;
-    this.paragraph.title = newPara.title;
-    this.paragraph.lineNumbers = newPara.lineNumbers;
-    this.paragraph.status = newPara.status;
-    this.paragraph.fontSize = newPara.fontSize;
-    if (newPara.status !== ParagraphStatus.RUNNING) {
-      this.paragraph.results = newPara.results;
-    }
-    this.paragraph.settings = newPara.settings;
-    this.paragraph.runtimeInfos = newPara.runtimeInfos;
-    this.isParagraphRunning = this.noteStatusService.isParagraphRunning(newPara);
-    this.paragraph.config = newPara.config;
-    this.initializeDefault(this.paragraph.config);
-    this.setResults();
-    this.cdr.markForCheck();
-  }
-
-  isUpdateRequired(oldPara: ParagraphItem, newPara: ParagraphItem): boolean {
-    return (
-      newPara.id === oldPara.id &&
-      (newPara.dateCreated !== oldPara.dateCreated ||
-        newPara.text !== oldPara.text ||
-        newPara.dateFinished !== oldPara.dateFinished ||
-        newPara.dateStarted !== oldPara.dateStarted ||
-        newPara.dateUpdated !== oldPara.dateUpdated ||
-        newPara.status !== oldPara.status ||
-        newPara.jobName !== oldPara.jobName ||
-        newPara.title !== oldPara.title ||
-        isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
-        newPara.errorMessage !== oldPara.errorMessage ||
-        !isEqual(newPara.settings, oldPara.settings) ||
-        !isEqual(newPara.config, oldPara.config) ||
-        !isEqual(newPara.runtimeInfos, oldPara.runtimeInfos))
-    );
-  }
-
   insertParagraph(position: string) {
     if (this.revisionView === true) {
       return;
@@ -584,16 +352,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     this.cdr.markForCheck();
   }
 
-  setResults() {
-    if (this.paragraph.results) {
-      this.results = this.paragraph.results.msg;
-      this.configs = this.paragraph.config.results;
-    }
-    if (!this.paragraph.config) {
-      this.paragraph.config = {};
-    }
-  }
-
   setTitle(title: string) {
     this.paragraph.title = title;
     this.commitParagraph();
@@ -611,42 +369,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     this.cdr.markForCheck();
   }
 
-  initializeDefault(config: ParagraphConfig) {
-    const forms = this.paragraph.settings.forms;
-
-    if (!config.colWidth) {
-      config.colWidth = 12;
-    }
-
-    if (!config.fontSize) {
-      config.fontSize = 9;
-    }
-
-    if (config.enabled === undefined) {
-      config.enabled = true;
-    }
-
-    for (const idx in forms) {
-      if (forms[idx]) {
-        if (forms[idx].options) {
-          if (config.runOnSelectionChange === undefined) {
-            config.runOnSelectionChange = true;
-          }
-        }
-      }
-    }
-
-    if (!config.results) {
-      config.results = {};
-    }
-
-    if (!config.editorSetting) {
-      config.editorSetting = {};
-    } else if (config.editorSetting.editOnDblClick) {
-      this.editorSetting.isOutputHidden = config.editorSetting.editOnDblClick;
-    }
-  }
-
   moveUpParagraph() {
     const newIndex = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id) - 1;
     if (newIndex < 0 || newIndex >= this.note.paragraphs.length) {
@@ -709,23 +431,29 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     this.cdr.markForCheck();
   }
 
+  openSingleParagraph(paragraphId: string): void {
+    const noteId = this.note.id;
+    const redirectToUrl = `${location.protocol}//${location.host}${location.pathname}#/notebook/${noteId}/paragraph/${paragraphId}`;
+    window.open(redirectToUrl);
+  }
+
   trackByIndexFn(index: number) {
     return index;
   }
 
   constructor(
+    noteStatusService: NoteStatusService,
+    cdr: ChangeDetectorRef,
+    ngZService: NgZService,
     private heliumService: HeliumService,
-    private noteStatusService: NoteStatusService,
     public messageService: MessageService,
     private nzModalService: NzModalService,
     private noteVarShareService: NoteVarShareService,
-    private cdr: ChangeDetectorRef,
-    private ngZService: NgZService,
     private shortcutService: ShortcutService,
     private host: ElementRef,
     private ngTemplateAdapterService: NgTemplateAdapterService
   ) {
-    super(messageService);
+    super(messageService, noteStatusService, ngZService, cdr);
   }
 
   ngOnInit() {
@@ -823,6 +551,9 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
           }
         }
         switch (action) {
+          case ParagraphActions.Link:
+            this.openSingleParagraph(this.paragraph.id);
+            break;
           case ParagraphActions.EditMode:
             if (this.mode === 'command') {
               event.preventDefault();
@@ -847,7 +578,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
             break;
         }
       });
-
     this.setResults();
     this.originalText = this.paragraph.text;
     this.isEntireNoteRunning = this.noteStatusService.isEntireNoteRunning(this.note);
@@ -894,6 +624,5 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
 
   ngOnDestroy(): void {
     super.ngOnDestroy();
-    this.ngZService.removeParagraph(this.paragraph.id);
   }
 }
diff --git a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
new file mode 100644
index 0000000..d69a0f4
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.html
@@ -0,0 +1,15 @@
+<zeppelin-notebook-paragraph-dynamic-forms
+  *ngIf="paragraph"
+  [disable]="paragraph.status == 'RUNNING' || paragraph.status == 'PENDING'"
+  [paramDefs]="paragraph.settings.params"
+  [formDefs]="paragraph.settings.forms"
+  [runOnChange]="paragraph.config.runOnSelectionChange"
+  (formChange)="runParagraph()">
+</zeppelin-notebook-paragraph-dynamic-forms>
+<zeppelin-notebook-paragraph-result *ngFor="let result of results; index as i; trackBy: trackByIndexFn"
+                                    [id]="paragraph.id"
+                                    [published]="true"
+                                    [currentCol]="paragraph.config.colWidth"
+                                    [config]="configs[i]"
+                                    [result]="result">
+</zeppelin-notebook-paragraph-result>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.less b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.less
new file mode 100644
index 0000000..e69de29
diff --git a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
new file mode 100644
index 0000000..12e10de
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
@@ -0,0 +1,88 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, QueryList, ViewChildren } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { MessageListener, ParagraphBase } from '@zeppelin/core';
+import { publishedSymbol, Published } from '@zeppelin/core/paragraph-base/published';
+import { NotebookParagraphResultComponent } from '@zeppelin/pages/workspace/share/result/result.component';
+import { MessageReceiveDataTypeMap, Note, OP } from '@zeppelin/sdk';
+import { HeliumService, MessageService, NgZService, NoteStatusService } from '@zeppelin/services';
+import { SpellResult } from '@zeppelin/spell/spell-result';
+import { isNil } from 'lodash';
+
+@Component({
+  selector: 'zeppelin-publish-paragraph',
+  templateUrl: './paragraph.component.html',
+  styleUrls: ['./paragraph.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class PublishedParagraphComponent extends ParagraphBase implements Published, OnInit {
+  readonly [publishedSymbol] = true;
+
+  noteId: string;
+  paragraphId: string;
+
+  @ViewChildren(NotebookParagraphResultComponent) notebookParagraphResultComponents: QueryList<
+    NotebookParagraphResultComponent
+  >;
+
+  constructor(
+    public messageService: MessageService,
+    noteStatusService: NoteStatusService,
+    ngZService: NgZService,
+    cdr: ChangeDetectorRef,
+    private activatedRoute: ActivatedRoute,
+    private heliumService: HeliumService
+  ) {
+    super(messageService, noteStatusService, ngZService, cdr);
+    this.activatedRoute.params.subscribe(params => {
+      this.noteId = params.noteId;
+      this.paragraphId = params.paragraphId;
+      this.messageService.getNote(this.noteId);
+    });
+  }
+
+  ngOnInit() {}
+
+  @MessageListener(OP.NOTE)
+  getNote(data: MessageReceiveDataTypeMap[OP.NOTE]) {
+    const note = data.note;
+    if (!isNil(note)) {
+      this.paragraph = (note as Note['note']).paragraphs.find(p => p.id === this.paragraphId);
+      if (this.paragraph) {
+        this.setResults();
+        this.originalText = this.paragraph.text;
+        this.initializeDefault(this.paragraph.config);
+      }
+    }
+    this.cdr.markForCheck();
+  }
+
+  trackByIndexFn(index: number) {
+    return index;
+  }
+
+  setResults() {
+    if (this.paragraph.results) {
+      this.results = this.paragraph.results.msg;
+      this.configs = this.paragraph.config.results;
+    }
+    if (!this.paragraph.config) {
+      this.paragraph.config = {};
+    }
+  }
+
+  changeColWidth(needCommit: boolean, updateResult?: boolean): void {
+    // noop
+  }
+
+  runParagraph(): void {
+    const text = this.paragraph.text;
+    if (text && !this.isParagraphRunning) {
+      const magic = SpellResult.extractMagic(this.paragraph.text);
+      if (this.heliumService.getSpellByMagic(magic)) {
+        this.runParagraphUsingSpell(text, magic, false);
+      } else {
+        this.runParagraphUsingBackendInterpreter(text);
+      }
+    }
+  }
+}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts b/zeppelin-web-angular/src/app/pages/workspace/published/published-ruoting.module.ts
similarity index 70%
copy from zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts
copy to zeppelin-web-angular/src/app/pages/workspace/published/published-ruoting.module.ts
index 6c177b6..eaf001f 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook-routing.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/published/published-ruoting.module.ts
@@ -12,21 +12,12 @@
 
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
-
-import { NotebookComponent } from './notebook.component';
+import { PublishedParagraphComponent } from './paragraph/paragraph.component';
 
 const routes: Routes = [
   {
-    path: ':noteId',
-    component: NotebookComponent
-  },
-  {
-    path: ':noteId/paragraph/:paragraphId',
-    component: NotebookComponent
-  },
-  {
-    path: ':noteId/revision/:revisionId',
-    component: NotebookComponent
+    path: ':paragraphId',
+    component: PublishedParagraphComponent
   }
 ];
 
@@ -34,4 +25,4 @@ const routes: Routes = [
   imports: [RouterModule.forChild(routes)],
   exports: [RouterModule]
 })
-export class NotebookRoutingModule {}
+export class PublishedRoutingModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts b/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
new file mode 100644
index 0000000..f6474d9
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/published/published.module.ts
@@ -0,0 +1,11 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { WorkspaceShareModule } from '../../workspace/share/share.module';
+import { PublishedParagraphComponent } from './paragraph/paragraph.component';
+import { PublishedRoutingModule } from './published-ruoting.module';
+
+@NgModule({
+  declarations: [PublishedParagraphComponent],
+  imports: [CommonModule, WorkspaceShareModule, PublishedRoutingModule]
+})
+export class PublishedModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
similarity index 100%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.html
rename to zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.html
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.less
similarity index 100%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.less
rename to zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.less
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts b/zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
similarity index 100%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/dynamic-forms/dynamic-forms.component.ts
rename to zeppelin-web-angular/src/app/pages/workspace/share/dynamic-forms/dynamic-forms.component.ts
diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/pages/workspace/share/index.ts
similarity index 85%
copy from zeppelin-web-angular/src/app/core/public-api.ts
copy to zeppelin-web-angular/src/app/pages/workspace/share/index.ts
index c514103..49e4740 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/share/index.ts
@@ -10,6 +10,4 @@
  * limitations under the License.
  */
 
-export * from './message-listener';
-export * from './destroy-hook';
-export * from './copy-text';
+export * from './public-api';
diff --git a/zeppelin-web-angular/src/app/core/public-api.ts b/zeppelin-web-angular/src/app/pages/workspace/share/public-api.ts
similarity index 85%
copy from zeppelin-web-angular/src/app/core/public-api.ts
copy to zeppelin-web-angular/src/app/pages/workspace/share/public-api.ts
index c514103..e865360 100644
--- a/zeppelin-web-angular/src/app/core/public-api.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/share/public-api.ts
@@ -10,6 +10,4 @@
  * limitations under the License.
  */
 
-export * from './message-listener';
-export * from './destroy-hook';
-export * from './copy-text';
+export * from './share.module';
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html
similarity index 95%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html
rename to zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html
index fe34a37..0b246b0 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html
@@ -10,7 +10,7 @@
   ~ limitations under the License.
   -->
 
-<div class="setting-bar" *ngIf="result.type === datasetType.TABLE">
+<div class="setting-bar" *ngIf="result.type === datasetType.TABLE && !published">
   <div class="visualization-selector">
     <nz-radio-group [(ngModel)]="config?.graph.mode" (ngModelChange)="switchMode($event)" nzButtonStyle="solid">
       <label *ngFor="let item of visualizations"
@@ -54,7 +54,7 @@
      [nzGridColumnCount]="12"
      [nzMinColumn]="1"
      nzBounds="window">
-  <nz-resize-handle nzDirection="bottomRight">
+  <nz-resize-handle nzDirection="bottomRight" *ngIf="!published">
     <zeppelin-resize-handle></zeppelin-resize-handle>
   </nz-resize-handle>
   <ng-template cdkPortalOutlet></ng-template>
@@ -65,7 +65,7 @@
          zeppelinRunScripts
          [scriptsContent]="innerHTML"
          [innerHTML]="innerHTML"></div>
-    <div *ngSwitchCase="datasetType.TEXT" class="text-plain"><pre>{{plainText}}</pre></div>
+    <div *ngSwitchCase="datasetType.TEXT" class="text-plain"><pre [innerHTML]="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.less b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.less
similarity index 100%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.less
rename to zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.less
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts
similarity index 99%
rename from zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
rename to zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts
index 742a9fb..86d94cc 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts
@@ -68,6 +68,7 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit,
   @Input() result: ParagraphIResultsMsgItem;
   @Input() config: ParagraphConfigResult;
   @Input() id: string;
+  @Input() published = false;
   @Input() currentCol = 12;
   @Output() readonly configChange = new EventEmitter<ParagraphConfigResult>();
   @Output() readonly sizeChange = new EventEmitter<NzResizeEvent>();
@@ -223,6 +224,9 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit,
         break;
     }
     this.cdr.markForCheck();
+    if (this.published) {
+      this.cdr.detectChanges();
+    }
   }
 
   renderHTML(): void {
diff --git a/zeppelin-web-angular/src/app/pages/workspace/share/share.module.ts b/zeppelin-web-angular/src/app/pages/workspace/share/share.module.ts
new file mode 100644
index 0000000..4c7ef1d
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/share/share.module.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { PortalModule } from '@angular/cdk/portal';
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import {
+  NzButtonModule,
+  NzCheckboxModule,
+  NzDropDownModule,
+  NzIconModule,
+  NzRadioModule,
+  NzSelectModule,
+  NzSwitchModule,
+  NzToolTipModule
+} from 'ng-zorro-antd';
+import { NzResizableModule } from 'ng-zorro-antd/resizable';
+
+import { ShareModule } from '@zeppelin/share';
+import { VisualizationModule } from '@zeppelin/visualizations/visualization.module';
+
+import { NotebookParagraphDynamicFormsComponent } from './dynamic-forms/dynamic-forms.component';
+import { NotebookParagraphResultComponent } from './result/result.component';
+
+@NgModule({
+  exports: [NotebookParagraphResultComponent, NotebookParagraphDynamicFormsComponent],
+  declarations: [NotebookParagraphResultComponent, NotebookParagraphDynamicFormsComponent],
+  imports: [
+    CommonModule,
+    ShareModule,
+    PortalModule,
+    VisualizationModule,
+    FormsModule,
+    NzButtonModule,
+    NzDropDownModule,
+    NzRadioModule,
+    NzResizableModule,
+    NzToolTipModule,
+    NzIconModule,
+    NzCheckboxModule,
+    NzSelectModule,
+    NzSwitchModule
+  ]
+})
+export class WorkspaceShareModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts b/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
index 0340a8d..9315443 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
@@ -31,6 +31,10 @@ const routes: Routes = [
         loadChildren: () => import('@zeppelin/pages/workspace/notebook/notebook.module').then(m => m.NotebookModule)
       },
       {
+        path: 'notebook/:noteId/paragraph',
+        loadChildren: () => import('@zeppelin/pages/workspace/published/published.module').then(m => m.PublishedModule)
+      },
+      {
         path: 'jobmanager',
         loadChildren: () =>
           import('@zeppelin/pages/workspace/job-manager/job-manager.module').then(m => m.JobManagerModule)
diff --git a/zeppelin-web-angular/src/app/pages/workspace/workspace.component.html b/zeppelin-web-angular/src/app/pages/workspace/workspace.component.html
index 6bcae47..c41cf78 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/workspace.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/workspace.component.html
@@ -11,7 +11,7 @@
   -->
 
 <div class="content" [class.blur]="!websocketConnected">
-  <zeppelin-header></zeppelin-header>
-  <router-outlet></router-outlet>
+  <zeppelin-header *ngIf="!publishMode"></zeppelin-header>
+  <router-outlet (activate)="onActivate($event)"></router-outlet>
 </div>
 <zeppelin-spin *ngIf="!websocketConnected" [transparent]="true">Connecting WebSocket ...</zeppelin-spin>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/workspace.component.ts b/zeppelin-web-angular/src/app/pages/workspace/workspace.component.ts
index 03c5b9b..c89d287e 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/workspace.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/workspace.component.ts
@@ -12,10 +12,13 @@
 
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
+import { filter, map, startWith, takeUntil, tap } from 'rxjs/operators';
 
+import { ActivatedRoute, NavigationEnd, Route, Router } from '@angular/router';
+import { publishedSymbol, Published } from '@zeppelin/core/paragraph-base/published';
 import { HeliumManagerService } from '@zeppelin/helium-manager';
 import { MessageService } from '@zeppelin/services';
+import { log } from 'ng-zorro-antd';
 
 @Component({
   selector: 'zeppelin-workspace',
@@ -26,6 +29,7 @@ import { MessageService } from '@zeppelin/services';
 export class WorkspaceComponent implements OnInit, OnDestroy {
   private destroy$ = new Subject();
   websocketConnected = false;
+  publishMode = false;
 
   constructor(
     public messageService: MessageService,
@@ -33,6 +37,11 @@ export class WorkspaceComponent implements OnInit, OnDestroy {
     private heliumManagerService: HeliumManagerService
   ) {}
 
+  onActivate(e) {
+    this.publishMode = e && e[publishedSymbol];
+    this.cdr.markForCheck();
+  }
+
   ngOnInit() {
     this.messageService.connectedStatus$.pipe(takeUntil(this.destroy$)).subscribe(data => {
       this.websocketConnected = data;
diff --git a/zeppelin-web-angular/src/app/services/shortcut.service.ts b/zeppelin-web-angular/src/app/services/shortcut.service.ts
index b6d6a3a..4b2a626 100644
--- a/zeppelin-web-angular/src/app/services/shortcut.service.ts
+++ b/zeppelin-web-angular/src/app/services/shortcut.service.ts
@@ -1,7 +1,7 @@
-import {DOCUMENT} from "@angular/common";
-import {Inject, Injectable} from '@angular/core';
-import {EventManager} from "@angular/platform-browser";
-import {Observable} from "rxjs";
+import { DOCUMENT } from '@angular/common';
+import { Inject, Injectable } from '@angular/core';
+import { EventManager } from '@angular/platform-browser';
+import { Observable } from 'rxjs';
 
 export enum ParagraphActions {
   EditMode = 'Paragraph:EditMode',
@@ -23,7 +23,8 @@ export enum ParagraphActions {
   SwitchTitleShow = 'Paragraph:SwitchTitleShow',
   SwitchOutputShow = 'Paragraph:SwitchOutputShow',
   SwitchEditorShow = 'Paragraph:SwitchEditorShow',
-  SwitchEnable = 'Paragraph:SwitchEnable'
+  SwitchEnable = 'Paragraph:SwitchEnable',
+  Link = 'Paragraph:Link'
 }
 
 export const ShortcutsMap = {
@@ -34,6 +35,8 @@ export const ShortcutsMap = {
   [ParagraphActions.Cancel]: 'shift.ctrlCmd.c',
   // Need register special character `¬` in MacOS
   [ParagraphActions.Clear]: ['alt.ctrlCmd.l', 'alt.ctrlCmd.¬'],
+  // Need register special character `†` in MacOS
+  [ParagraphActions.Link]: ['alt.ctrlCmd.t', 'alt.ctrlCmd.†'],
   // Need register special character `®` in MacOS
   [ParagraphActions.SwitchEnable]: ['alt.ctrlCmd.r', 'alt.ctrlCmd.®'],
   // Need register special character `–` in MacOS
@@ -54,28 +57,27 @@ export const ShortcutsMap = {
 };
 
 export interface ShortcutEvent {
-  event: KeyboardEvent
+  event: KeyboardEvent;
   keybindings: string;
 }
 
 export interface ShortcutOption {
-  scope?: HTMLElement,
-  keybindings: string
+  scope?: HTMLElement;
+  keybindings: string;
 }
 
 function isMacOS() {
-  return navigator.platform.indexOf('Mac') > -1
+  return navigator.platform.indexOf('Mac') > -1;
 }
 
 @Injectable({
   providedIn: 'root'
 })
 export class ShortcutService {
-
   private element: HTMLElement;
 
-  constructor(private eventManager: EventManager,
-              @Inject(DOCUMENT) _document: any) {
+  // tslint:disable-next-line:no-any
+  constructor(private eventManager: EventManager, @Inject(DOCUMENT) _document: any) {
     this.element = _document;
   }
 
@@ -86,9 +88,9 @@ export class ShortcutService {
   bindShortcut(option: ShortcutOption): Observable<ShortcutEvent> {
     const host = option.scope || this.element;
     // `ctrlCmd` is special symbol, will be replaced `meta` in MacOS, 'control' in Windows/Linux
-    const keybindings = option.keybindings
-      .replace(/ctrlCmd/g, isMacOS() ? 'meta' : 'control');
-    const event = `keydown.${keybindings}`;
+    const keybindings = option.keybindings.replace(/ctrlCmd/g, isMacOS() ? 'meta' : 'control');
+    const eventName = `keydown.${keybindings}`;
+    // tslint:disable-next-line:ban-types
     let dispose: Function;
     return new Observable<ShortcutEvent>(observer => {
       const handler = event => {
@@ -98,12 +100,11 @@ export class ShortcutService {
         });
       };
 
-      dispose = this.eventManager.addEventListener(host, event, handler);
+      dispose = this.eventManager.addEventListener(host, eventName, handler);
 
       return () => {
         dispose();
       };
-    })
+    });
   }
-
 }