You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2020/07/02 17:49:21 UTC

[zeppelin] branch master updated: [ZEPPELIN-4918] Add Table of Contents to Sidebar

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8b8a50a  [ZEPPELIN-4918] Add Table of Contents to Sidebar
8b8a50a is described below

commit 8b8a50a4bd99f563a50a32e284e4cb8a2d39905d
Author: Pranav Tharoor <pr...@gmail.com>
AuthorDate: Tue Jun 30 11:52:19 2020 +0530

    [ZEPPELIN-4918] Add Table of Contents to Sidebar
    
    ### What is this PR for?
    This PR adds a Table of Contents (ToC) to the new Zeppelin UI. The ToC allows quick navigation through long notebooks. The ToC is generated based on the presence of Heading tags in the results.
    
    ### What type of PR is it?
    Improvement
    
    ### Todos
    * [ ] - N/A
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-4918
    
    ### How should this be tested?
    Travis CI: https://travis-ci.com/github/pranavtharoor/zeppelin/builds/173370001
    Manual steps:
    1. Create a notebook
    2. Add different levels of Markdown headings or other inline HTML heading tags
    3. Run the paragraphs so that the result is shown
    4. Click the ToC icon in the sidebar ribbon to open the ToC. The ToC should be generated/refreshed
    5. Try clicking on the ToC rows to scroll to the respective paragraph
    
    ### Screenshots (if appropriate)
    <img width="1680" alt="Screenshot 2020-06-28 at 8 04 31 PM" src="https://user-images.githubusercontent.com/22600061/86060953-6f5bf100-ba83-11ea-8fff-8af2ce856da7.png">
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: Pranav Tharoor <pr...@gmail.com>
    
    Closes #3834 from pranavtharoor/ZEPPELIN-4918 and squashes the following commits:
    
    1b67911bf [Pranav Tharoor] [ZEPPELIN-4918] Fix breaking components and sidebar refresh on adding a new notebook
    c5c362982 [Pranav Tharoor] [ZEPPELIN-4918] Add Table of Contents to Sidebar
---
 .../workspace/notebook/notebook.component.html     |  14 ++-
 .../workspace/notebook/notebook.component.less     |  35 ++-----
 .../pages/workspace/notebook/notebook.component.ts |   6 +-
 .../pages/workspace/notebook/notebook.module.ts    |   4 +-
 .../notebook/sidebar/sidebar.component.html        |  35 +++++++
 .../notebook/sidebar/sidebar.component.less        |  67 +++++++++++++
 .../notebook/sidebar/sidebar.component.ts          |  51 ++++++++++
 .../src/app/share/note-toc/note-toc.component.html |  23 +++++
 .../src/app/share/note-toc/note-toc.component.less |  84 ++++++++++++++++
 .../src/app/share/note-toc/note-toc.component.ts   | 110 +++++++++++++++++++++
 zeppelin-web-angular/src/app/share/share.module.ts |  10 +-
 11 files changed, 397 insertions(+), 42 deletions(-)

diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
index 52b8c96..2c3a682 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
@@ -25,15 +25,13 @@
   <div class="flex-container">
     <div class="sidebar-area"
         nz-resizable
-        [nzMaxWidth]="!isSidebarOpen ? 30 : 800"
-        [nzMinWidth]="!isSidebarOpen ? 30 : 280"
+        [nzMaxWidth]="!isSidebarOpen ? 40 : 800"
+        [nzMinWidth]="!isSidebarOpen ? 40 : 280"
         (nzResize)="onResizeSidebar($event)"
-        [style.min-width.px]="!isSidebarOpen ? 30 : sidebarWidth">
-      <button class="sidebar-toggle" (click)="toggleSidebar()">
-        <i *ngIf="!isSidebarOpen" nz-icon nzType="folder" nzTheme="outline"></i>
-        <i *ngIf="isSidebarOpen" nz-icon nzType="folder-open" nzTheme="outline"></i>
-      </button>
-      <zeppelin-node-list *ngIf="isSidebarOpen"></zeppelin-node-list>
+        [style.min-width.px]="!isSidebarOpen ? 40 : sidebarWidth">
+      <zeppelin-notebook-sidebar [note]="note"
+                                (isSidebarOpenChange)="onSidebarOpenChange($event)"
+                                (scrollToParagraph)="onParagraphScrolled($event)"></zeppelin-notebook-sidebar>
       <nz-resize-handle  *ngIf="isSidebarOpen" nzDirection="right"><div class="sidebar-resize"></div></nz-resize-handle>
     </div>
     <div class="notebook-area">
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.less
index 4c0e7cd..6938ce4 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.less
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.less
@@ -32,30 +32,15 @@
 
   .sidebar-area {
     box-shadow: 4px 0px 2px 0 rgba(0, 0, 0, 0.06);
-    border-right: solid thin @component-background;;
-    overflow-y: auto;
-    overflow-x: hidden;
+    border-right: solid thin @component-background;
+    overflow: hidden;
     background: @component-background;
-    padding: 20px;
     z-index: 25;
     position: sticky;
     top: 0px;
     height: 100vh;
   }
 
-  .sidebar-toggle {
-    width: 40px;
-    position: absolute;
-    top: 0;
-    right: 0;
-    background: none;
-    border: none;
-    cursor: pointer;
-    padding: 10px 0;
-    font-size: 20px;
-    z-index: 30;
-  }
-
   .sidebar-resize {
     height: 100%;
     width: 5px;
@@ -68,24 +53,15 @@
 
   .simple .sidebar-area {
     box-shadow: none;
-
-    .sidebar-toggle {
-      opacity: 0;
-      transition: opacity 0.2s ease;
-    }
-
+    
     &:hover {
       border-right: solid thin rgba(0, 0, 0, 0.06);
-
-      .sidebar-toggle {
-        opacity: 1;
-      }
     }
-
   }
-
+  
   .notebook-area {
     overflow: hidden;
+    width: 100%;
   }
 
   .extension-area {
@@ -96,6 +72,7 @@
     top: 0px;
     z-index: 20;
   }
+
   .paragraph-area {
     padding: 0 5px;
   }
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 e672251..1b04d30 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
@@ -72,7 +72,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
   saveTimer = null;
   interpreterBindings: InterpreterBindingItem[] = [];
   activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide';
-  sidebarWidth = 330;
+  sidebarWidth = 370;
   sidebarAnimationFrame = -1;
   isSidebarOpen = false;
 
@@ -340,8 +340,8 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     this.cdr.markForCheck();
   }
 
-  toggleSidebar() {
-    this.isSidebarOpen = !this.isSidebarOpen;
+  onSidebarOpenChange(isSidebarOpen: boolean) {
+    this.isSidebarOpen = isSidebarOpen;
   }
 
   onResizeSidebar({ width }: NzResizeEvent): void {
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 a7e8ef9..306196d 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
@@ -53,6 +53,7 @@ import { NoteFormBlockComponent } from './note-form-block/note-form-block.compon
 import { NotebookRoutingModule } from './notebook-routing.module';
 import { NotebookComponent } from './notebook.component';
 import { NotebookShareModule } from './share/share.module';
+import { NotebookSidebarComponent } from './sidebar/sidebar.component';
 
 @NgModule({
   declarations: [
@@ -67,7 +68,8 @@ import { NotebookShareModule } from './share/share.module';
     NotebookParagraphProgressComponent,
     NotebookParagraphFooterComponent,
     NotebookParagraphControlComponent,
-    NoteFormBlockComponent
+    NoteFormBlockComponent,
+    NotebookSidebarComponent
   ],
   imports: [
     CommonModule,
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.html
new file mode 100644
index 0000000..66e2bbd
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.html
@@ -0,0 +1,35 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="sidebar" [class.simple]="note.config.looknfeel !== 'default'">
+  <div class="sidebar-nav">
+    <button [class.sidebar-button-active]="sidebarState === SidebarState.TOC"
+            class="sidebar-button"
+            (click)="setOrToggleSidebarState(SidebarState.TOC)">
+      <i nz-icon nzType="unordered-list" nzTheme="outline"></i>
+    </button>
+    <button [class.sidebar-button-active]="sidebarState === SidebarState.FILE_TREE"
+            class="sidebar-button"
+            (click)="setOrToggleSidebarState(SidebarState.FILE_TREE)">
+      <i nz-icon nzType="folder" nzTheme="outline"></i>
+    </button>
+  </div>
+  <button class="sidebar-button sidebar-close" (click)="setOrToggleSidebarState(SidebarState.CLOSED)">
+    <i *ngIf="sidebarState !== SidebarState.CLOSED" nz-icon nzType="close" nzTheme="outline"></i>
+  </button>
+  <div class="sidebar-main" *ngIf="sidebarState !== SidebarState.CLOSED">
+    <zeppelin-node-list *ngIf="sidebarState === SidebarState.FILE_TREE"></zeppelin-node-list>
+    <zeppelin-note-toc *ngIf="sidebarState === SidebarState.TOC"
+                      [note]="note"
+                      (scrollToParagraph)="onScrollToParagraph($event)"></zeppelin-note-toc>
+  </div>
+<div>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.less
new file mode 100644
index 0000000..d49a110
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.less
@@ -0,0 +1,67 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "theme-mixin";
+
+.themeMixin({
+  .sidebar {
+    height: 100%;
+    width: 100%;
+    display: flex;
+  }
+
+  .sidebar-nav {
+    display: flex;
+    flex-direction: column;
+    border-right: solid thin rgba(0, 0, 0, 0.06);
+  }
+
+  .sidebar-button {
+    width: 40px;
+    background: none;
+    border: none;
+    cursor: pointer;
+    padding: 10px 0;
+    font-size: 20px;
+    z-index: 30;
+    transition: all 0.2s ease;
+
+    &-active, &:hover {
+      color: #3071A9;
+    }
+  }
+
+  .sidebar-close {
+    position: absolute;
+    top: 3px;
+    right: 3px;
+  }
+
+  .simple {
+    .sidebar-button, .sidebar-nav {
+      opacity: 0;
+      transition: opacity 0.2s ease;
+    }
+
+    &:hover {
+      .sidebar-button, .sidebar-nav {
+        opacity: 1;
+      }
+    }
+  }
+
+  .sidebar-main {
+    padding: 40px 30px 20px;
+    width: calc(100% - 40px);
+    overflow-y: auto;
+  }
+});
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.ts
new file mode 100644
index 0000000..cfb270e
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/sidebar/sidebar.component.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+import { Note } from '@zeppelin/sdk';
+
+enum SidebarState {
+  CLOSED = 'CLOSED',
+  FILE_TREE = 'FILE_TREE',
+  TOC = 'TOC'
+}
+
+@Component({
+  selector: 'zeppelin-notebook-sidebar',
+  templateUrl: './sidebar.component.html',
+  styleUrls: ['./sidebar.component.less']
+})
+export class NotebookSidebarComponent {
+  @Input() note: Note['note'];
+  @Output() readonly isSidebarOpenChange = new EventEmitter<boolean>();
+  @Output() readonly scrollToParagraph = new EventEmitter<string>();
+  sidebarState = SidebarState.CLOSED;
+  SidebarState = SidebarState;
+
+  setOrToggleSidebarState(sidebarState: SidebarState) {
+    if (this.sidebarState === sidebarState) {
+      this.sidebarState = SidebarState.CLOSED;
+    } else {
+      this.sidebarState = sidebarState;
+    }
+    if (this.sidebarState === SidebarState.CLOSED) {
+      this.isSidebarOpenChange.emit(false);
+    } else {
+      this.isSidebarOpenChange.emit(true);
+    }
+  }
+
+  onScrollToParagraph(event: string) {
+    this.scrollToParagraph.emit(event);
+  }
+}
diff --git a/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.html b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.html
new file mode 100644
index 0000000..6f62ea5
--- /dev/null
+++ b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.html
@@ -0,0 +1,23 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="note-toc" [class.simple]="note.config.looknfeel !== 'default'">
+  <div class="toc-heading">Table of Contents</div>
+  <div class="toc-placeholder" *ngIf="rows.length === 0">
+    <i nz-icon nzType="unordered-list" nzTheme="outline"></i>
+    <span class="toc-message">Headings in the output show up here</span>
+  </div>
+  <div class="toc-row" *ngFor="let row of rows" (click)="onRowClick(row.paragraphId)">
+    <div *ngFor="let _ of Arr(row.level)" class="toc-indent">&nbsp;</div> 
+    <span [class.toc-top-level-heading]="row.level === 1">{{ row.title }}</span>
+  </div>
+</div>
diff --git a/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.less b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.less
new file mode 100644
index 0000000..3d59ffa
--- /dev/null
+++ b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.less
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import 'theme-mixin';
+
+:host {
+  display: block;
+}
+
+.themeMixin({
+  .note-toc {
+    width: 100%;
+    position: relative;
+
+    &:not(.simple) .toc-heading {
+      border-bottom: solid 1px #3071A9;
+    }
+  }
+
+  .toc-heading {
+    font-size: 20px;
+    font-weight: 500;
+    margin-bottom: 10px;
+    padding-bottom: 15px;
+  }
+
+  .toc-placeholder {
+    font-size: 80px;
+    color: #eeeeee;
+    padding: 40px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+  }
+
+  .toc-message {
+    font-size: 12px;
+    color: #cccccc;
+    text-align: center;
+    margin-top: 30px;
+  }
+
+  .toc-row {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    cursor: pointer;
+    transition: all 0.1s ease;
+
+    &:hover {
+      color: #3071A9;
+    }
+  }
+
+  .toc-indent {
+    display: inline-block;
+    min-width: 20px;
+    padding: 10px;
+
+    &:first-child {
+      min-width: 0;
+      padding: 10px 0;
+    }
+
+    & + span:not(.toc-top-level-heading) {
+      padding: 10px;
+    }
+  }
+
+  .toc-top-level-heading {
+    font-weight: 500;
+    font-size: 16px;
+  }
+});
diff --git a/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.ts b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.ts
new file mode 100644
index 0000000..03931aa
--- /dev/null
+++ b/zeppelin-web-angular/src/app/share/note-toc/note-toc.component.ts
@@ -0,0 +1,110 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, DoCheck, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { DatasetType, Note } from '@zeppelin/sdk';
+
+interface TocResult {
+  paragraphId: string;
+  resultData: string;
+  resultType: DatasetType;
+}
+
+interface TocRow {
+  paragraphId: string;
+  level: number;
+  title: string;
+}
+
+@Component({
+  selector: 'zeppelin-note-toc',
+  templateUrl: './note-toc.component.html',
+  styleUrls: ['./note-toc.component.less']
+})
+export class NoteTocComponent implements OnInit, DoCheck {
+  @Input() note: Note['note'];
+  @Output() readonly scrollToParagraph = new EventEmitter<string>();
+  Arr = Array;
+  rows = [];
+  oldNote: Note['note'];
+
+  onRowClick(id: string) {
+    this.scrollToParagraph.emit(id);
+  }
+
+  getResults(note: Note['note']): TocResult[] {
+    const results = note.paragraphs.reduce((allResults: TocResult[], paragraph) => {
+      const newResults = [];
+      if (paragraph.results && paragraph.results.msg) {
+        paragraph.results.msg.forEach(result =>
+          newResults.push({
+            paragraphId: paragraph.id,
+            resultData: result.data,
+            resultType: result.type
+          })
+        );
+      }
+      return [...allResults, ...newResults];
+    }, []);
+    return results.filter(result => result.resultType === DatasetType.HTML);
+  }
+
+  unpackNodes(element: Element) {
+    element.querySelectorAll('*').forEach(subElements => (subElements.outerHTML = subElements.innerHTML));
+    return element.innerHTML;
+  }
+
+  computeRows() {
+    const htmlResults = this.getResults(this.note);
+    const rows: TocRow[] = htmlResults.reduce((allRows: TocRow[], result) => {
+      const parser = new DOMParser();
+      const resultDOM = parser.parseFromString(result.resultData, 'text/html');
+      const headings = Array.from(resultDOM.querySelectorAll('h1, h2, h3, h4, h5, h6'));
+      const newRows = headings.map(heading => ({
+        level: parseInt(heading.nodeName[1], 10),
+        title: this.unpackNodes(heading),
+        paragraphId: result.paragraphId
+      }));
+      return [...allRows, ...newRows];
+    }, []);
+    const levelsSet: Set<number> = new Set();
+    rows.forEach(row => levelsSet.add(row.level));
+    const levels = Array.from(levelsSet).sort();
+    const headingLevelToTocLevelMap = {};
+    levels.forEach((level, index) => (headingLevelToTocLevelMap[level] = index + 1));
+    this.rows = rows.map(heading => ({ ...heading, level: headingLevelToTocLevelMap[heading.level] }));
+  }
+
+  shouldRecomputeRows() {
+    if (this.note.id !== this.oldNote.id) {
+      return true;
+    }
+    const oldResult = this.getResults(this.oldNote);
+    return this.getResults(this.note).reduce(
+      (hasParagraphUpdated, result, resultIndex) =>
+        hasParagraphUpdated || !oldResult[resultIndex] || result.resultData !== oldResult[resultIndex].resultData,
+      false
+    );
+  }
+
+  ngOnInit() {
+    this.computeRows();
+    this.oldNote = this.note;
+  }
+
+  ngDoCheck() {
+    if (this.shouldRecomputeRows()) {
+      this.computeRows();
+      this.oldNote = this.note;
+    }
+  }
+}
diff --git a/zeppelin-web-angular/src/app/share/share.module.ts b/zeppelin-web-angular/src/app/share/share.module.ts
index 26047e4..ce75d65 100644
--- a/zeppelin-web-angular/src/app/share/share.module.ts
+++ b/zeppelin-web-angular/src/app/share/share.module.ts
@@ -47,6 +47,7 @@ import { NodeListComponent } from '@zeppelin/share/node-list/node-list.component
 import { NoteCreateComponent } from '@zeppelin/share/note-create/note-create.component';
 import { NoteImportComponent } from '@zeppelin/share/note-import/note-import.component';
 import { NoteRenameComponent } from '@zeppelin/share/note-rename/note-rename.component';
+import { NoteTocComponent } from '@zeppelin/share/note-toc/note-toc.component';
 import { PageHeaderComponent } from '@zeppelin/share/page-header/page-header.component';
 import { HumanizeBytesPipe } from '@zeppelin/share/pipes';
 import { RunScriptsDirective } from '@zeppelin/share/run-scripts/run-scripts.directive';
@@ -62,7 +63,14 @@ const MODAL_LIST = [
   FolderRenameComponent,
   Ng1MigrationComponent
 ];
-const EXPORT_LIST = [HeaderComponent, NodeListComponent, PageHeaderComponent, SpinComponent, ResizeHandleComponent];
+const EXPORT_LIST = [
+  HeaderComponent,
+  NodeListComponent,
+  NoteTocComponent,
+  PageHeaderComponent,
+  SpinComponent,
+  ResizeHandleComponent
+];
 const PIPES = [HumanizeBytesPipe];
 
 @NgModule({