import {Component, OnInit} from '@angular/core'; import {BehaviorSubject, Observable} from 'rxjs'; import {Pentest} from '@shared/models/pentest.model'; import * as FA from '@fortawesome/free-solid-svg-icons'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; import {filter, tap} from 'rxjs/operators'; import { Comment, CommentEntry, transformCommentsToObjectiveEntries } from '@shared/models/comment.model'; import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {ProjectState} from '@shared/stores/project-state/project-state'; import {Store} from '@ngxs/store'; import {PentestStatus} from '@shared/models/pentest-status.model'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service'; import {CommentService} from '@shared/services/api/comment.service'; import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions'; import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component'; import {Finding} from '@shared/models/finding.model'; import {FindingService} from '@shared/services/api/finding.service'; @UntilDestroy() @Component({ selector: 'app-pentest-comments', templateUrl: './pentest-comments.component.html', styleUrls: ['./pentest-comments.component.scss'] }) export class PentestCommentsComponent implements OnInit { // HTML only readonly fa = FA; // HTML only for button enabling inProgressStatus: PentestStatus = PentestStatus.IN_PROGRESS; pentestInfo$: BehaviorSubject = new BehaviorSubject(null); // comments$: BehaviorSubject = new BehaviorSubject(null); loading$: BehaviorSubject = new BehaviorSubject(true); columns: Array = [ CommentColumns.TITLE, CommentColumns.DESCRIPTION, CommentColumns.ACTIONS ]; dataSource: NbTreeGridDataSource; data: CommentEntry[] = []; getters: NbGetters = { dataGetter: (node: CommentEntry) => node, childrenGetter: (node: CommentEntry) => node.childEntries || undefined, expandedGetter: (node: CommentEntry) => !!node.expanded, }; constructor(private readonly commentService: CommentService, private readonly findingService: FindingService, private dataSourceBuilder: NbTreeGridDataSourceBuilder, private notificationService: NotificationService, private dialogService: DialogService, private commentDialogService: CommentDialogService, private store: Store) { this.dataSource = dataSourceBuilder.create(this.data, this.getters); } ngOnInit(): void { this.store.select(ProjectState.pentest).pipe( untilDestroyed(this) ).subscribe({ next: (selectedPentest: Pentest) => { this.pentestInfo$.next(selectedPentest); this.loadCommentsData(); this.requestFindingsData(selectedPentest.id); }, error: err => { console.error(err); } }); } loadCommentsData(): void { this.commentService.getCommentsByPentestId(this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '') .pipe( untilDestroyed(this), /*filter(isNotNullOrUndefined),*/ tap(() => this.loading$.next(true)) ) .subscribe({ next: (comments: Comment[]) => { if (comments) { this.data = transformCommentsToObjectiveEntries(comments); } else { this.data = []; } this.dataSource.setData(this.data, this.getters); this.loading$.next(false); }, error: err => { console.error(err); // ToDo: Implement again after proper lazy loading and routing // this.notificationService.showPopup('comment.popup.not.found', PopupType.FAILURE); this.loading$.next(false); } }); } onClickAddComment(): void { this.commentDialogService.openCommentDialog( CommentDialogComponent, this.pentestInfo$.getValue().findingIds, null, { closeOnEsc: false, hasScroll: false, autoFocus: true, closeOnBackdropClick: false }, this.pentestInfo$.getValue() ).pipe( untilDestroyed(this) ).subscribe({ next: (newComment: Comment) => { this.loadCommentsData(); } }); } onClickEditComment(commentEntry): void { this.commentService.getCommentById(commentEntry.data.commentId).pipe( filter(isNotNullOrUndefined), untilDestroyed(this) ).subscribe({ next: (existingComment: Comment) => { if (existingComment) { this.commentDialogService.openCommentDialog( CommentDialogComponent, this.pentestInfo$.getValue().findingIds, existingComment, { closeOnEsc: false, hasScroll: false, autoFocus: true, closeOnBackdropClick: false }, this.pentestInfo$.getValue() ).pipe( untilDestroyed(this) ).subscribe({ next: (updatedComment: Comment) => { this.loadCommentsData(); } }); } else { this.notificationService.showPopup('comment.popup.not.available', PopupType.INFO); } }, error: err => { console.error(err); } }); } requestFindingsData(pentestId: string): void { this.findingService.getFindingsByPentestId(pentestId).pipe( untilDestroyed(this) ).subscribe({ next: (findings: Finding[]) => { // findings.forEach(finding => this.objectiveFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption)); }, error: err => { console.error(err); } }); } onClickDeleteComment(commentEntry): void { const message = { title: 'comment.delete.title', key: 'comment.delete.key', data: {name: commentEntry.data.title}, }; this.dialogService.openConfirmDialog( message ).onClose.pipe( untilDestroyed(this) ).subscribe({ next: () => { this.deleteComment(commentEntry); } }); } // HTML only isLoading(): Observable { return this.loading$.asObservable(); } private deleteComment(commentEntry): void { this.commentService.deleteCommentByPentestAndCommentId( this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', commentEntry.data.commentId) .pipe( untilDestroyed(this) ).subscribe({ next: (deletedComment: any) => { this.store.dispatch(new UpdatePentestComments(deletedComment.id)); this.loadCommentsData(); this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS); }, error: error => { console.error(error); this.onRequestFailed(commentEntry); this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE); } }); } private onRequestFailed(retryParameter: any): void { this.dialogService.openRetryDialog({key: 'global.retry.dialog', data: null}).onClose .pipe( untilDestroyed(this) ) .subscribe((ref) => { if (ref.retry) { this.deleteComment(retryParameter); } }); } } enum CommentColumns { COMMENT_ID = 'commentId', TITLE = 'title', DESCRIPTION = 'description', RELATED_FINDINGS = 'relatedFindings', ACTIONS = 'actions' }