blob: a905d266177fac324cfa44d9f387844fe89575f8 [file] [log] [blame]
/*
* 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,
QueryList,
ViewChildren
} from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { isNil } from 'lodash';
import { Subject } from 'rxjs';
import { distinctUntilKeyChanged, map, startWith, takeUntil } from 'rxjs/operators';
import { NzResizeEvent } from 'ng-zorro-antd/resizable';
import { MessageListener, MessageListenersManager } from '@zeppelin/core';
import { Permissions } from '@zeppelin/interfaces';
import {
DynamicFormParams,
InterpreterBindingItem,
MessageReceiveDataTypeMap,
Note,
OP,
RevisionListItem
} from '@zeppelin/sdk';
import {
MessageService,
NgZService,
NoteStatusService,
NoteVarShareService,
SecurityService,
ThemeService,
TicketService
} from '@zeppelin/services';
import { scrollIntoViewIfNeeded } from '@zeppelin/utility';
import { NotebookParagraphComponent } from './paragraph/paragraph.component';
@Component({
selector: 'zeppelin-notebook',
templateUrl: './notebook.component.html',
styleUrls: ['./notebook.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotebookComponent extends MessageListenersManager implements OnInit, OnDestroy {
@ViewChildren(NotebookParagraphComponent) listOfNotebookParagraphComponent!: QueryList<NotebookParagraphComponent>;
private destroy$ = new Subject();
note?: Exclude<Note['note'], undefined>;
permissions?: Permissions;
selectId: string | null = null;
scrolledId: string | null = null;
isOwner = true;
noteRevisions: RevisionListItem[] = [];
currentRevision?: string;
collaborativeMode = false;
revisionView = false;
collaborativeModeUsers: string[] = [];
isNoteDirty: boolean | null = false;
isShowNoteForms = false;
saveTimer: ReturnType<typeof setTimeout> | null = null;
interpreterBindings: InterpreterBindingItem[] = [];
activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide';
sidebarWidth = 370;
sidebarAnimationFrame = -1;
isSidebarOpen = false;
@MessageListener(OP.NOTE)
getNote(data: MessageReceiveDataTypeMap[OP.NOTE]) {
const note = data.note;
if (isNil(note)) {
this.router.navigate(['/']).then();
} else {
this.removeParagraphFromNgZ();
this.note = note;
const { paragraphId } = this.activatedRoute.snapshot.params;
if (paragraphId) {
this.note = this.cleanParagraphExcept(this.note, paragraphId);
this.initializeLookAndFeel(this.note);
} else {
this.initializeLookAndFeel(this.note);
this.getInterpreterBindings(this.note);
this.getPermissions(this.note);
this.note.config.personalizedMode =
this.note.config.personalizedMode === undefined ? 'false' : this.note.config.personalizedMode;
}
if (this.note!.noteForms && this.note!.noteParams) {
this.saveNoteForms({
formsData: {
forms: this.note!.noteForms,
params: this.note!.noteParams
}
});
}
this.titleService.setTitle(this.note?.name + ' - Zeppelin');
this.themeService.updateMonacoTheme();
this.cdr.markForCheck();
}
}
@MessageListener(OP.INTERPRETER_BINDINGS)
loadInterpreterBindings(data: MessageReceiveDataTypeMap[OP.INTERPRETER_BINDINGS]) {
this.interpreterBindings = data.interpreterBindings;
if (!this.interpreterBindings.some(item => item.selected)) {
this.activatedExtension = 'interpreter';
}
this.cdr.markForCheck();
}
@MessageListener(OP.PARAGRAPH_REMOVED)
removeParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_REMOVED]) {
const { paragraphId } = this.activatedRoute.snapshot.params;
if (paragraphId || this.revisionView) {
return;
}
if (!this.note) {
return;
}
const definedNote = this.note;
const paragraphIndex = definedNote.paragraphs.findIndex(p => p.id === data.id);
definedNote.paragraphs = definedNote.paragraphs.filter((p, index) => index !== paragraphIndex);
const adjustedCursorIndex =
paragraphIndex === definedNote.paragraphs.length ? paragraphIndex - 1 : paragraphIndex + 1;
const targetParagraph = this.listOfNotebookParagraphComponent.find((_, index) => index === adjustedCursorIndex);
if (targetParagraph) {
targetParagraph.focusEditor();
}
this.cdr.markForCheck();
}
@MessageListener(OP.PARAGRAPH_ADDED)
addParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_ADDED]) {
const { paragraphId } = this.activatedRoute.snapshot.params;
if (paragraphId || this.revisionView) {
return;
}
if (!this.note) {
return;
}
const definedNote = this.note;
definedNote.paragraphs.splice(data.index, 0, data.paragraph);
const paragraphIndex = definedNote.paragraphs.findIndex(p => p.id === data.paragraph.id);
definedNote.paragraphs[paragraphIndex].focus = true;
this.cdr.markForCheck();
}
@MessageListener(OP.SAVE_NOTE_FORMS)
saveNoteForms(data: MessageReceiveDataTypeMap[OP.SAVE_NOTE_FORMS]) {
if (!this.note) {
return;
}
const definedNote = this.note;
definedNote.noteForms = data.formsData.forms;
definedNote.noteParams = data.formsData.params;
this.setNoteFormsStatus();
}
@MessageListener(OP.NOTE_REVISION)
getNoteRevision(data: MessageReceiveDataTypeMap[OP.NOTE_REVISION]) {
const note = data.note;
if (isNil(note)) {
this.router.navigate(['/']).then();
} else {
this.note = note;
this.initializeLookAndFeel(this.note);
this.cdr.markForCheck();
}
}
@MessageListener(OP.SET_NOTE_REVISION)
setNoteRevision(data: MessageReceiveDataTypeMap[OP.SET_NOTE_REVISION]) {
const { noteId } = this.activatedRoute.snapshot.params;
this.router.navigate(['/notebook', noteId]).then();
}
@MessageListener(OP.PARAGRAPH_MOVED)
moveParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_MOVED]) {
if (!this.note) {
return;
}
if (!this.revisionView) {
const movedPara = this.note.paragraphs.find(p => p.id === data.id);
if (movedPara) {
const listOfRestPara = this.note.paragraphs.filter(p => p.id !== data.id);
this.note.paragraphs = [...listOfRestPara.slice(0, data.index), movedPara, ...listOfRestPara.slice(data.index)];
const paragraphComponent = this.listOfNotebookParagraphComponent.find(e => e.paragraph.id === data.id);
this.cdr.markForCheck();
if (paragraphComponent) {
// Call when next tick
setTimeout(() => {
scrollIntoViewIfNeeded(paragraphComponent.getElement());
paragraphComponent.focusEditor();
});
}
}
}
}
@MessageListener(OP.COLLABORATIVE_MODE_STATUS)
getCollaborativeModeStatus(data: MessageReceiveDataTypeMap[OP.COLLABORATIVE_MODE_STATUS]) {
this.collaborativeMode = Boolean(data.status);
this.collaborativeModeUsers = data.users;
this.cdr.markForCheck();
}
@MessageListener(OP.PATCH_PARAGRAPH)
patchParagraph(data: MessageReceiveDataTypeMap[OP.PATCH_PARAGRAPH]) {
this.collaborativeMode = true;
this.cdr.markForCheck();
}
@MessageListener(OP.NOTE_UPDATED)
noteUpdated(data: MessageReceiveDataTypeMap[OP.NOTE_UPDATED]) {
if (!this.note) {
return;
}
if (data.name !== this.note.name) {
this.note.name = data.name;
}
this.note.config = data.config;
this.note.info = data.info;
this.initializeLookAndFeel(this.note);
this.cdr.markForCheck();
}
@MessageListener(OP.LIST_REVISION_HISTORY)
listRevisionHistory(data: MessageReceiveDataTypeMap[OP.LIST_REVISION_HISTORY]) {
this.noteRevisions = data.revisionList;
if (this.noteRevisions) {
if (this.noteRevisions.length === 0 || this.noteRevisions[0].id !== 'Head') {
this.noteRevisions.splice(0, 0, { id: 'Head', message: 'Head' });
}
const { revisionId } = this.activatedRoute.snapshot.params;
if (revisionId) {
const revisionItemFound = this.noteRevisions.find(r => r.id === revisionId);
if (!revisionItemFound) {
throw new Error(`Revision ${revisionId} not found`);
}
this.currentRevision = revisionItemFound.message;
} else {
this.currentRevision = 'Head';
}
}
this.cdr.markForCheck();
}
onParagraphSearch(term: string) {
this.listOfNotebookParagraphComponent.forEach(comp => comp.highlightMatches(term || ''));
}
saveParagraph(id: string) {
const paragraphFound = this.listOfNotebookParagraphComponent.toArray().find(p => p.paragraph.id === id);
if (!paragraphFound) {
throw new Error(`Paragraph ${id} not found`);
}
paragraphFound.saveParagraph();
}
killSaveTimer() {
if (this.saveTimer) {
clearTimeout(this.saveTimer);
this.saveTimer = null;
}
}
startSaveTimer() {
this.killSaveTimer();
this.isNoteDirty = true;
this.saveTimer = setTimeout(() => {
this.saveNote();
}, 10000);
}
onParagraphSelect(id: string | null) {
this.selectId = id;
}
onParagraphScrolled(id: string | null) {
this.scrolledId = id;
}
onSelectAtIndex(index: number) {
if (!this.note) {
throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
}
const scopeIndex = Math.min(this.note.paragraphs.length, Math.max(0, index));
if (this.note.paragraphs[scopeIndex]) {
this.selectId = this.note.paragraphs[scopeIndex].id;
}
}
saveNote() {
if (this.note && this.note.paragraphs && this.listOfNotebookParagraphComponent) {
this.listOfNotebookParagraphComponent.toArray().forEach(p => {
p.saveParagraph();
});
this.isNoteDirty = null;
this.cdr.markForCheck();
}
}
getInterpreterBindings(note: Exclude<Note['note'], undefined>) {
this.messageService.getInterpreterBindings(note.id);
}
getPermissions(note: Exclude<Note['note'], undefined>) {
this.securityService.getPermissions(note.id).subscribe(data => {
this.permissions = data;
this.isOwner = !(
this.permissions.owners.length && this.permissions.owners.indexOf(this.ticketService.ticket.principal) < 0
);
this.cdr.markForCheck();
});
}
get viewOnly(): boolean {
if (!this.note) {
return false;
}
return this.noteStatusService.viewOnly(this.note);
}
initializeLookAndFeel(note: Exclude<Note['note'], undefined>) {
note.config.looknfeel = note.config.looknfeel || 'default';
if (note.paragraphs && note.paragraphs[0]) {
note.paragraphs[0].focus = true;
}
}
cleanParagraphExcept(note: Exclude<Note['note'], undefined>, paragraphId: string) {
const targetParagraph = note.paragraphs.find(p => p.id === paragraphId);
if (!targetParagraph) {
throw new Error(`Paragraph ${paragraphId} not found`);
}
const config = targetParagraph.config || {};
config.editorHide = true;
config.tableHide = false;
const paragraphs = [{ ...targetParagraph, config }];
return { ...note, paragraphs };
}
setAllParagraphTableHide(tableHide: boolean) {
this.listOfNotebookParagraphComponent.forEach(p => p.setTableHide(tableHide));
}
setAllParagraphEditorHide(editorHide: boolean) {
this.listOfNotebookParagraphComponent.forEach(p => p.setEditorHide(editorHide));
}
onNoteFormChange(noteParams: DynamicFormParams) {
if (!this.note) {
throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
}
this.messageService.saveNoteForms({
noteParams,
id: this.note.id
});
}
onFormNameRemove(formName: string) {
if (!this.note) {
throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
}
this.messageService.removeNoteForms(this.note, formName);
}
onNoteTitleChange(noteFormTitle: string) {
if (!this.note) {
throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
}
this.messageService.updateNote(this.note.id, this.note.name, {
...this.note.config,
noteFormTitle
});
}
setNoteFormsStatus() {
this.isShowNoteForms = !!this.note && this.note.noteForms && Object.keys(this.note.noteForms).length !== 0;
this.cdr.markForCheck();
}
onSidebarOpenChange(isSidebarOpen: boolean) {
this.isSidebarOpen = isSidebarOpen;
}
onResizeSidebar({ width }: NzResizeEvent): void {
cancelAnimationFrame(this.sidebarAnimationFrame);
this.sidebarAnimationFrame = requestAnimationFrame(() => {
this.sidebarWidth = width!;
});
}
constructor(
public messageService: MessageService,
protected ngZService: NgZService,
private activatedRoute: ActivatedRoute,
private cdr: ChangeDetectorRef,
private noteStatusService: NoteStatusService,
private noteVarShareService: NoteVarShareService,
private ticketService: TicketService,
private securityService: SecurityService,
private router: Router,
private titleService: Title,
private themeService: ThemeService
) {
super(messageService);
}
ngOnInit() {
this.activatedRoute.queryParamMap
.pipe(
startWith(this.activatedRoute.snapshot.queryParamMap),
takeUntil(this.destroy$),
map(data => data.get('paragraph'))
)
.subscribe(id => {
this.onParagraphSelect(id);
this.onParagraphScrolled(id);
});
this.activatedRoute.params.pipe(takeUntil(this.destroy$), distinctUntilKeyChanged('noteId')).subscribe(() => {
this.noteVarShareService.clear();
});
this.activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe(param => {
const { noteId, revisionId } = param;
if (revisionId) {
this.messageService.noteRevision(noteId, revisionId);
} else {
this.messageService.getNote(noteId);
}
this.revisionView = !!revisionId;
this.cdr.markForCheck();
this.messageService.listRevisionHistory(noteId);
// TODO(hsuanxyz) scroll to current paragraph
});
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();
this.saveNote();
this.destroy$.next();
this.destroy$.complete();
this.titleService.setTitle('Zeppelin');
}
}