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/10 02:33:01 UTC

[zeppelin] branch web_angular updated: [ZEPPELIN-4548] Support search notebook

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

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


The following commit(s) were added to refs/heads/web_angular by this push:
     new d95b73e  [ZEPPELIN-4548] Support search notebook
d95b73e is described below

commit d95b73efeaaf247aa07599d5f52a04c911a5a679
Author: Hsuan Lee <hs...@gmail.com>
AuthorDate: Wed Jan 8 16:23:25 2020 +0800

    [ZEPPELIN-4548] Support search notebook
    
    ### What is this PR for?
    
    support search notebook
    
    ### What type of PR is it?
    [Feature]
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-4548
    
    ### How should this be tested?
    
    N/A
    
    ### Screenshots (if appropriate)
    
    ![image](https://user-images.githubusercontent.com/22736418/71962091-ad462e00-3233-11ea-9d85-5b579f280953.png)
    
    ### 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 #3592 from hsuanxyz/feat/search and squashes the following commits:
    
    6e41f3e36 [Hsuan Lee] feat: support search notebook
---
 .../{public-api.ts => notebook-search.ts}          |  14 +-
 .../src/app/interfaces/public-api.ts               |   1 +
 .../notebook-search-routing.module.ts}             |  24 ++-
 .../notebook-search/notebook-search.component.html |  17 +++
 .../notebook-search.component.less}                |  19 ++-
 .../notebook-search/notebook-search.component.ts   |  58 ++++++++
 .../notebook-search/notebook-search.module.ts      |  29 ++++
 .../result-item/result-item.component.html         |  22 +++
 .../result-item/result-item.component.less}        |  14 +-
 .../result-item/result-item.component.ts           | 162 +++++++++++++++++++++
 .../paragraph/code-editor/code-editor.component.ts |   4 +-
 .../pages/workspace/workspace-routing.module.ts    |   5 +
 .../src/app/services/notebook-search.service.ts    |  47 ++++++
 .../src/app/share/header/header.component.html     |   6 +-
 .../src/app/share/header/header.component.ts       |  15 ++
 .../src/app/utility/get-keyword-positions.ts       |  43 ++++++
 zeppelin-web-angular/src/app/utility/line-map.ts   |  60 ++++++++
 .../src/app/visualizations/g2.config.ts            |   1 -
 18 files changed, 510 insertions(+), 31 deletions(-)

diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/interfaces/notebook-search.ts
similarity index 71%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to zeppelin-web-angular/src/app/interfaces/notebook-search.ts
index 8c54e3d..267ee94 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/interfaces/notebook-search.ts
@@ -10,10 +10,10 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
-export * from './credential';
-export * from './notebook-repo';
+export interface NotebookSearchResultItem {
+  id: string;
+  name: string;
+  snippet: string;
+  text: string;
+  header: string;
+}
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/interfaces/public-api.ts
index 8c54e3d..e762a5c 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/interfaces/public-api.ts
@@ -17,3 +17,4 @@ export * from './message-interceptor';
 export * from './security';
 export * from './credential';
 export * from './notebook-repo';
+export * from './notebook-search';
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search-routing.module.ts
similarity index 58%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search-routing.module.ts
index 8c54e3d..9127792 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search-routing.module.ts
@@ -10,10 +10,20 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
-export * from './credential';
-export * from './notebook-repo';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { NotebookSearchComponent } from './notebook-search.component';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: NotebookSearchComponent
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class NotebookSearchRoutingModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.html
new file mode 100644
index 0000000..e1db6d9
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.html
@@ -0,0 +1,17 @@
+<!--
+  ~ 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="main">
+  <zeppelin-notebook-search-result-item *ngFor="let item of results"
+                                        [result]="item">
+  </zeppelin-notebook-search-result-item>
+</div>
+
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.less
similarity index 71%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.less
index 8c54e3d..108b1a4 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.less
@@ -10,10 +10,15 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
-export * from './credential';
-export * from './notebook-repo';
+@import 'theme-mixin';
+
+.themeMixin({
+  .main {
+    padding: @card-padding-base / 2;
+  }
+
+  zeppelin-notebook-search-result-item {
+    margin-bottom: 16px;
+    display: block;
+  }
+});
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.ts
new file mode 100644
index 0000000..2036d23
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.component.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { NotebookSearchResultItem } from '@zeppelin/interfaces';
+import { NotebookSearchService } from '@zeppelin/services/notebook-search.service';
+import { Subject } from 'rxjs';
+import { filter, map, switchMap, takeUntil, tap } from 'rxjs/operators';
+
+@Component({
+  selector: 'zeppelin-notebook-search',
+  templateUrl: './notebook-search.component.html',
+  styleUrls: ['./notebook-search.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NotebookSearchComponent implements OnInit, OnDestroy {
+  private destroy$ = new Subject();
+  private searchAction$ = this.router.params.pipe(
+    takeUntil(this.destroy$),
+    map(params => params.queryStr),
+    filter(queryStr => typeof queryStr === 'string' && !!queryStr.trim()),
+    tap(() => (this.searching = true)),
+    switchMap(queryStr => this.notebookSearchService.search(queryStr))
+  );
+
+  results: NotebookSearchResultItem[] = [];
+  searching = false;
+
+  constructor(
+    private cdr: ChangeDetectorRef,
+    private router: ActivatedRoute,
+    private notebookSearchService: NotebookSearchService
+  ) {}
+
+  ngOnInit() {
+    this.searchAction$.subscribe(results => {
+      this.results = results;
+      this.searching = false;
+      this.cdr.markForCheck();
+    });
+  }
+
+  ngOnDestroy(): void {
+    this.notebookSearchService.clear();
+    this.destroy$.next();
+    this.destroy$.complete();
+  }
+}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.module.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.module.ts
new file mode 100644
index 0000000..69dafa8
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/notebook-search.module.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+import { NzCardModule } from 'ng-zorro-antd/card';
+
+import { ShareModule } from '@zeppelin/share';
+
+import { NotebookSearchRoutingModule } from './notebook-search-routing.module';
+import { NotebookSearchComponent } from './notebook-search.component';
+import { NotebookSearchResultItemComponent } from './result-item/result-item.component';
+
+@NgModule({
+  declarations: [NotebookSearchComponent, NotebookSearchResultItemComponent],
+  imports: [CommonModule, NotebookSearchRoutingModule, ShareModule, NzCardModule, FormsModule]
+})
+export class NotebookSearchModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.html
new file mode 100644
index 0000000..dcd78dc
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.html
@@ -0,0 +1,22 @@
+<!--
+  ~ 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.
+  -->
+
+<nz-card [nzTitle]="titleTemplateRef">
+  <ng-template #titleTemplateRef>
+    <a [routerLink]="routerLink">{{displayName}}</a>
+  </ng-template>
+  <zeppelin-code-editor
+    [style.height.px]="height"
+    [nzEditorOption]="editorOption"
+    (nzEditorInitialized)="initializedEditor($event)">
+  </zeppelin-code-editor>
+</nz-card>
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.less
similarity index 71%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.less
index 8c54e3d..cb24d4e 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.less
@@ -10,10 +10,10 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
-export * from './credential';
-export * from './notebook-repo';
+::ng-deep {
+  .monaco-editor {
+    .mark {
+      background: #fdf733;
+    }
+  }
+}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.ts
new file mode 100644
index 0000000..e911a66
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook-search/result-item/result-item.component.ts
@@ -0,0 +1,162 @@
+/*
+ * 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 {
+  ChangeDetectionStrategy,
+  ChangeDetectorRef,
+  Component,
+  Input,
+  NgZone,
+  OnChanges,
+  OnDestroy,
+  SimpleChanges
+} from '@angular/core';
+import { NotebookSearchResultItem } from '@zeppelin/interfaces';
+import { getKeywordPositions, KeywordPosition } from '@zeppelin/utility/get-keyword-positions';
+import { editor, Range } from 'monaco-editor';
+import IEditor = editor.IEditor;
+import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
+
+@Component({
+  selector: 'zeppelin-notebook-search-result-item',
+  templateUrl: './result-item.component.html',
+  styleUrls: ['./result-item.component.less'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NotebookSearchResultItemComponent implements OnChanges, OnDestroy {
+  @Input() result: NotebookSearchResultItem;
+
+  displayName = '';
+  routerLink = '';
+  mergedStr: string;
+  keywords: string[] = [];
+  highlightPositions: KeywordPosition[] = [];
+  editor: IStandaloneCodeEditor;
+  height = 0;
+  decorations: string[] = [];
+  editorOption = {
+    readOnly: true,
+    fontSize: 12,
+    renderLineHighlight: 'none',
+    minimap: { enabled: false },
+    lineNumbers: 'off',
+    glyphMargin: false,
+    scrollBeyondLastLine: false,
+    contextmenu: false
+  };
+
+  constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}
+
+  setDisplayNameAndRouterLink(): void {
+    const noteId = this.result.id.split('/', 2)[0];
+    this.displayName = this.result.name ? this.result.name : `Note ${noteId}`;
+
+    this.routerLink = `/notebook/${noteId}`;
+  }
+
+  setHighlightKeyword(): void {
+    let mergedStr = this.result.header ? `${this.result.header}\n\n${this.result.snippet}` : this.result.snippet;
+
+    const regexp = /<B>(.+?)<\/B>/g;
+    const matches = [];
+    let match = regexp.exec(mergedStr);
+
+    while (match !== null) {
+      if (match[1]) {
+        matches.push(match[1].toLocaleLowerCase());
+      }
+      match = regexp.exec(mergedStr);
+    }
+
+    mergedStr = mergedStr.replace(regexp, '$1');
+    this.mergedStr = mergedStr;
+    const keywords = [...new Set(matches)];
+    this.highlightPositions = getKeywordPositions(keywords, mergedStr);
+  }
+
+  applyHighlight() {
+    if (this.editor) {
+      this.decorations = this.editor.deltaDecorations(
+        this.decorations,
+        this.highlightPositions.map(highlight => {
+          const line = highlight.line + 1;
+          const character = highlight.character + 1;
+          return {
+            range: new Range(line, character, line, character + highlight.length),
+            options: {
+              className: 'mark',
+              stickiness: 1
+            }
+          };
+        })
+      );
+      this.cdr.markForCheck();
+    }
+  }
+
+  setLanguage() {
+    const editorModes = {
+      scala: /^%(\w*\.)?(spark|flink)/,
+      python: /^%(\w*\.)?(pyspark|python)/,
+      html: /^%(\w*\.)?(angular|ng)/,
+      r: /^%(\w*\.)?(r|sparkr|knitr)/,
+      sql: /^%(\w*\.)?\wql/,
+      yaml: /^%(\w*\.)?\wconf/,
+      markdown: /^%md/,
+      shell: /^%sh/
+    };
+    let mode = 'text';
+    const model = this.editor.getModel();
+    const keys = Object.keys(editorModes);
+    for (let i = 0; i < keys.length; i++) {
+      if (editorModes[keys[i]].test(this.result.snippet)) {
+        mode = keys[i];
+        break;
+      }
+    }
+    editor.setModelLanguage(model, mode);
+  }
+
+  autoAdjustEditorHeight() {
+    this.ngZone.run(() => {
+      setTimeout(() => {
+        if (this.editor) {
+          this.height =
+            this.editor.getTopForLineNumber(Number.MAX_SAFE_INTEGER) + this.editor.getConfiguration().lineHeight * 2;
+          this.editor.layout();
+          this.cdr.markForCheck();
+        }
+      });
+    });
+  }
+
+  initializedEditor(editorInstance: IEditor) {
+    this.editor = editorInstance as IStandaloneCodeEditor;
+    this.editor.setValue(this.mergedStr);
+    this.setLanguage();
+    this.autoAdjustEditorHeight();
+    this.applyHighlight();
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.result) {
+      this.setDisplayNameAndRouterLink();
+      this.setHighlightKeyword();
+      this.autoAdjustEditorHeight();
+      this.applyHighlight();
+    }
+  }
+
+  ngOnDestroy(): void {
+    this.editor.dispose();
+  }
+}
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 8cf4bd7..bfae433 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
@@ -145,7 +145,9 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
         lineNumbers: this.lineNumbers ? 'on' : 'off',
         glyphMargin: false,
         folding: false,
-        scrollBeyondLastLine: false
+        scrollBeyondLastLine: false,
+        contextmenu: false,
+        matchBrackets: false
       });
     }
   }
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 16928a3..d4faf20 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
@@ -35,6 +35,11 @@ const routes: Routes = [
         loadChildren: () => import('@zeppelin/pages/workspace/published/published.module').then(m => m.PublishedModule)
       },
       {
+        path: 'search/:queryStr',
+        loadChildren: () =>
+          import('@zeppelin/pages/workspace/notebook-search/notebook-search.module').then(m => m.NotebookSearchModule)
+      },
+      {
         path: 'jobmanager',
         loadChildren: () =>
           import('@zeppelin/pages/workspace/job-manager/job-manager.module').then(m => m.JobManagerModule)
diff --git a/zeppelin-web-angular/src/app/services/notebook-search.service.ts b/zeppelin-web-angular/src/app/services/notebook-search.service.ts
new file mode 100644
index 0000000..7371eec
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/notebook-search.service.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { NotebookSearchResultItem } from '@zeppelin/interfaces';
+import { BaseRest } from '@zeppelin/services/base-rest';
+import { BaseUrlService } from '@zeppelin/services/base-url.service';
+import { BehaviorSubject } from 'rxjs';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class NotebookSearchService extends BaseRest {
+  private queryStr$ = new BehaviorSubject<string | null>(null);
+
+  constructor(baseUrlService: BaseUrlService, private http: HttpClient) {
+    super(baseUrlService);
+  }
+
+  queried() {
+    return this.queryStr$.asObservable();
+  }
+
+  clear() {
+    this.queryStr$.next(null);
+  }
+
+  search(query: string) {
+    this.queryStr$.next(query);
+    return this.http.get<NotebookSearchResultItem[]>(this.restUrl`/notebook/search`, {
+      params: {
+        q: query
+      }
+    });
+  }
+}
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.html b/zeppelin-web-angular/src/app/share/header/header.component.html
index 33d8ecd..76f93c2 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.html
+++ b/zeppelin-web-angular/src/app/share/header/header.component.html
@@ -73,7 +73,11 @@
   </div>
   <div class="search">
     <nz-input-group [nzPrefixIcon]="'search'">
-      <input type="text" nz-input placeholder="Search"/>
+      <input type="text"
+             nz-input
+             placeholder="Search"
+             (keyup.enter)="onSearch()"
+             [(ngModel)]="queryStr"/>
     </nz-input-group>
   </div>
 </div>
diff --git a/zeppelin-web-angular/src/app/share/header/header.component.ts b/zeppelin-web-angular/src/app/share/header/header.component.ts
index e69b89e..b4c20c2 100644
--- a/zeppelin-web-angular/src/app/share/header/header.component.ts
+++ b/zeppelin-web-angular/src/app/share/header/header.component.ts
@@ -20,6 +20,7 @@ import { filter, takeUntil } from 'rxjs/operators';
 import { MessageListener, MessageListenersManager } from '@zeppelin/core';
 import { MessageReceiveDataTypeMap, OP } from '@zeppelin/sdk';
 import { MessageService, TicketService } from '@zeppelin/services';
+import { NotebookSearchService } from '@zeppelin/services/notebook-search.service';
 import { AboutZeppelinComponent } from '@zeppelin/share/about-zeppelin/about-zeppelin.component';
 
 @Component({
@@ -32,6 +33,7 @@ export class HeaderComponent extends MessageListenersManager implements OnInit,
   private destroy$ = new Subject();
   connectStatus = 'error';
   noteListVisible = false;
+  queryStr: string | null = null;
 
   about() {
     this.nzModalService.create({
@@ -46,6 +48,13 @@ export class HeaderComponent extends MessageListenersManager implements OnInit,
     this.ticketService.logout().subscribe();
   }
 
+  onSearch() {
+    this.queryStr = this.queryStr.trim();
+    if (this.queryStr) {
+      this.router.navigate(['/search', this.queryStr]);
+    }
+  }
+
   @MessageListener(OP.CONFIGURATIONS_INFO)
   getConfiguration(data: MessageReceiveDataTypeMap[OP.CONFIGURATIONS_INFO]) {
     this.ticketService.setConfiguration(data);
@@ -56,6 +65,7 @@ export class HeaderComponent extends MessageListenersManager implements OnInit,
     private nzModalService: NzModalService,
     public messageService: MessageService,
     private router: Router,
+    private notebookSearchService: NotebookSearchService,
     private cdr: ChangeDetectorRef
   ) {
     super(messageService);
@@ -76,6 +86,11 @@ export class HeaderComponent extends MessageListenersManager implements OnInit,
         this.noteListVisible = false;
         this.cdr.markForCheck();
       });
+
+    this.notebookSearchService
+      .queried()
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(queryStr => (this.queryStr = queryStr));
   }
 
   ngOnDestroy() {
diff --git a/zeppelin-web-angular/src/app/utility/get-keyword-positions.ts b/zeppelin-web-angular/src/app/utility/get-keyword-positions.ts
new file mode 100644
index 0000000..c2eb7aa
--- /dev/null
+++ b/zeppelin-web-angular/src/app/utility/get-keyword-positions.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { computeLineStartsMap, getLineAndCharacterFromPosition } from '@zeppelin/utility/line-map';
+
+export interface KeywordPosition {
+  line: number;
+  character: number;
+  length: number;
+}
+
+export function getKeywordPositions(keywords: string[], str: string): KeywordPosition[] {
+  const highlightPositions = [];
+  const lineMap = computeLineStartsMap(str);
+
+  keywords.forEach((keyword: string) => {
+    const positions = [];
+    const keywordReg = new RegExp(keyword, 'ig');
+    let posMatch = keywordReg.exec(str);
+
+    while (posMatch !== null) {
+      const { line, character } = getLineAndCharacterFromPosition(lineMap, posMatch.index);
+      positions.push({
+        line,
+        character,
+        length: keyword.length
+      });
+      posMatch = keywordReg.exec(str);
+    }
+    highlightPositions.push(...positions);
+  });
+
+  return highlightPositions;
+}
diff --git a/zeppelin-web-angular/src/app/utility/line-map.ts b/zeppelin-web-angular/src/app/utility/line-map.ts
new file mode 100644
index 0000000..30c4079
--- /dev/null
+++ b/zeppelin-web-angular/src/app/utility/line-map.ts
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+const LF_CHAR = 10;
+const CR_CHAR = 13;
+const LINE_SEP_CHAR = 8232;
+const PARAGRAPH_CHAR = 8233;
+
+export function computeLineStartsMap(text) {
+  const result = [0];
+  let pos = 0;
+  while (pos < text.length) {
+    const char = text.charCodeAt(pos++);
+    // Handles the "CRLF" line break. In that case we peek the character
+    // after the "CR" and check if it is a line feed.
+    if (char === CR_CHAR) {
+      if (text.charCodeAt(pos) === LF_CHAR) {
+        pos++;
+      }
+      result.push(pos);
+    } else if (char === LF_CHAR || char === LINE_SEP_CHAR || char === PARAGRAPH_CHAR) {
+      result.push(pos);
+    }
+  }
+  result.push(pos);
+  return result;
+}
+
+function findClosestLineStartPosition(linesMap, position, low = 0, high = linesMap.length - 1) {
+  let _low = low;
+  let _high = high;
+  while (_low <= _high) {
+    const pivotIdx = Math.floor((_low + _high) / 2);
+    const pivotEl = linesMap[pivotIdx];
+    if (pivotEl === position) {
+      return pivotIdx;
+    } else if (position > pivotEl) {
+      _low = pivotIdx + 1;
+    } else {
+      _high = pivotIdx - 1;
+    }
+  }
+  // In case there was no exact match, return the closest "lower" line index. We also
+  // subtract the index by one because want the index of the previous line start.
+  return _low - 1;
+}
+
+export function getLineAndCharacterFromPosition(lineStartsMap, position) {
+  const lineIndex = findClosestLineStartPosition(lineStartsMap, position);
+  return { character: position - lineStartsMap[lineIndex], line: lineIndex };
+}
diff --git a/zeppelin-web-angular/src/app/visualizations/g2.config.ts b/zeppelin-web-angular/src/app/visualizations/g2.config.ts
index aa5a4e9..622d25d 100644
--- a/zeppelin-web-angular/src/app/visualizations/g2.config.ts
+++ b/zeppelin-web-angular/src/app/visualizations/g2.config.ts
@@ -124,7 +124,6 @@ const zeppelinTheme = {
 
 export function setTheme() {
   const theme = G2.Util.deepMix(G2.Global, zeppelinTheme);
-  console.log(zeppelinTheme);
   // tslint:disable-next-line:no-any
   (G2.Global as any).setTheme(theme);
 }