fix: As a user I want to refactor the current routing logic
This commit is contained in:
parent
79a2493c37
commit
4ca2828e0f
|
@ -17,6 +17,16 @@ const routes: Routes = [
|
|||
loadChildren: () => import('./project-overview').then(mod => mod.ProjectOverviewModule),
|
||||
canActivate: [AuthGuardService]
|
||||
},
|
||||
{
|
||||
path: Route.OBJECTIVE_OVERVIEW,
|
||||
loadChildren: () => import('./project-overview/project').then(mod => mod.ProjectModule),
|
||||
canActivate: [AuthGuardService]
|
||||
},
|
||||
{
|
||||
path: Route.PENTEST_OBJECTIVE,
|
||||
loadChildren: () => import('./pentest').then(mod => mod.PentestModule),
|
||||
canActivate: [AuthGuardService]
|
||||
},
|
||||
// ToDo: Remove after default Keycloak login mask got reworked
|
||||
/*{
|
||||
path: 'login',
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
<button nbButton hero
|
||||
status="info"
|
||||
shape="round"
|
||||
(click)="onClickExportPentestReport()">
|
||||
<fa-icon [icon]="fa.faFileExport"
|
||||
(click)="onClickGeneratePentestReport()">
|
||||
<fa-icon [icon]="fa.faFileAlt"
|
||||
class="element-icon fa-lg"></fa-icon>
|
||||
<span class="element-text">{{ 'global.action.export' | translate }}</span>
|
||||
<span class="element-text">{{ 'global.action.report' | translate }}</span>
|
||||
</button>
|
||||
</nb-action>
|
||||
</nb-actions>
|
||||
|
|
|
@ -42,7 +42,11 @@ export class ObjectiveHeaderComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (selectedProject: Project) => {
|
||||
if (selectedProject) {
|
||||
this.selectedProject$.next(selectedProject);
|
||||
} else {
|
||||
this.router.navigate([Route.PROJECT_OVERVIEW]);
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
|
@ -90,7 +94,7 @@ export class ObjectiveHeaderComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onClickExportPentestReport(): void {
|
||||
onClickGeneratePentestReport(): void {
|
||||
this.exportReportDialogService.openExportReportDialog(
|
||||
ExportReportDialogComponent,
|
||||
this.selectedProject$.getValue(),
|
||||
|
|
|
@ -23,6 +23,8 @@ import {FlexLayoutModule} from '@angular/flex-layout';
|
|||
import {CommonAppModule} from '../common-app.module';
|
||||
import {ObjectiveOverviewRoutingModule} from './objective-overview-routing.module';
|
||||
import {ExportReportDialogModule} from '@shared/modules/export-report-dialog/export-report-dialog.module';
|
||||
import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module';
|
||||
import {CommentWidgetModule} from '@shared/widgets/comment-widget/comment-widget.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -43,7 +45,6 @@ import {ExportReportDialogModule} from '@shared/modules/export-report-dialog/exp
|
|||
NbTreeGridModule,
|
||||
TranslateModule,
|
||||
StatusTagModule,
|
||||
FindigWidgetModule,
|
||||
RouterModule,
|
||||
NbMenuModule,
|
||||
FormsModule,
|
||||
|
@ -52,7 +53,11 @@ import {ExportReportDialogModule} from '@shared/modules/export-report-dialog/exp
|
|||
FlexLayoutModule,
|
||||
NbActionsModule,
|
||||
ExportReportDialogModule,
|
||||
ObjectiveOverviewRoutingModule
|
||||
ProjectDialogModule,
|
||||
ObjectiveOverviewRoutingModule,
|
||||
// Table Widgets
|
||||
FindigWidgetModule,
|
||||
CommentWidgetModule
|
||||
],
|
||||
exports: [
|
||||
ObjectiveHeaderComponent,
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
<tr nbTreeGridHeaderRow *nbTreeGridHeaderRowDef="columns"></tr>
|
||||
<tr nbTreeGridRow *nbTreeGridRowDef="let pentest; columns: columns"
|
||||
class="pentest-cell"
|
||||
routerLink="pentest"
|
||||
fragment="{{pentest.data['refNumber']}}"
|
||||
(click)="selectPentest(pentest.data)"
|
||||
[skipLocationChange]="true">
|
||||
(click)="onClickRouteToObjectivePentest(pentest.data)">
|
||||
</tr>
|
||||
<!-- Test ID -->
|
||||
<ng-container [nbTreeGridColumnDef]="columns[0]">
|
||||
|
@ -44,11 +41,14 @@
|
|||
<!-- Findings -->
|
||||
<ng-container [nbTreeGridColumnDef]="columns[3]">
|
||||
<th nbTreeGridHeaderCell *nbTreeGridHeaderCellDef>
|
||||
{{ 'pentest.findings' | translate }}
|
||||
{{ 'pentest.findings&comments' | translate }}
|
||||
</th>
|
||||
<td nbTreeGridCell *nbTreeGridCellDef="let pentest">
|
||||
<app-findig-widget [numberOfFindigs]="pentest.data['findings']"></app-findig-widget>
|
||||
<!--ToDo: Add comments {{pentest.data['comments'] || '-'}}-->
|
||||
<div fxLayout="row" fxLayoutGap="0.5rem" fxLayoutAlign="start start">
|
||||
<app-findig-widget [numberOfFindings]="pentest.data['findings']"></app-findig-widget>
|
||||
<span> / </span>
|
||||
<app-comment-widget [numberOfComments]="pentest.data['comments']"></app-comment-widget>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
</table>
|
||||
|
|
|
@ -3,13 +3,14 @@ import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@neb
|
|||
import {Pentest, ObjectiveEntry, transformPentestsToObjectiveEntries} from '@shared/models/pentest.model';
|
||||
import {PentestService} from '@shared/services/api/pentest.service';
|
||||
import {Store} from '@ngxs/store';
|
||||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||
import {PROJECT_STATE_NAME, ProjectState} from '@shared/stores/project-state/project-state';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
import {catchError, switchMap, tap} from 'rxjs/operators';
|
||||
import {BehaviorSubject, Observable, of} from 'rxjs';
|
||||
import {getTitleKeyForRefNumber} from '@shared/functions/categories/get-title-key-for-ref-number.function';
|
||||
import {Router} from '@angular/router';
|
||||
import {ChangePentest} from '@shared/stores/project-state/project-state.actions';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -20,7 +21,7 @@ import {ChangePentest} from '@shared/stores/project-state/project-state.actions'
|
|||
export class ObjectiveTableComponent implements OnInit {
|
||||
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
columns: Array<ObjectiveColumns> = [ObjectiveColumns.TEST_ID, ObjectiveColumns.TITLE, ObjectiveColumns.STATUS, ObjectiveColumns.FINDINGS];
|
||||
columns: Array<ObjectiveColumns> = [ObjectiveColumns.TEST_ID, ObjectiveColumns.TITLE, ObjectiveColumns.STATUS, ObjectiveColumns.FINDINGS_AND_COMMENTS];
|
||||
dataSource: NbTreeGridDataSource<ObjectiveEntry>;
|
||||
|
||||
private data: ObjectiveEntry[] = [];
|
||||
|
@ -36,7 +37,7 @@ export class ObjectiveTableComponent implements OnInit {
|
|||
private store: Store,
|
||||
private pentestService: PentestService,
|
||||
private dataSourceBuilder: NbTreeGridDataSourceBuilder<ObjectiveEntry>,
|
||||
private readonly router: Router
|
||||
private router: Router
|
||||
) {
|
||||
this.dataSource = dataSourceBuilder.create(this.data, this.getters);
|
||||
}
|
||||
|
@ -53,7 +54,6 @@ export class ObjectiveTableComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (pentests: Pentest[]) => {
|
||||
// ToDo: Change assignement here
|
||||
this.pentests$.next(pentests);
|
||||
this.data = transformPentestsToObjectiveEntries(pentests);
|
||||
this.dataSource.setData(this.data, this.getters);
|
||||
|
@ -66,16 +66,14 @@ export class ObjectiveTableComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
selectPentest(selectedPentest: Pentest): void {
|
||||
/* ToDo: Include again after fixing pentest route
|
||||
this.router.navigate([Route.PENTEST])
|
||||
onClickRouteToObjectivePentest(selectedPentest: Pentest): void {
|
||||
this.router.navigate([Route.PENTEST_OBJECTIVE])
|
||||
.then(
|
||||
() => this.store.reset({
|
||||
...this.store.snapshot(),
|
||||
// [PROJECT_STATE_NAME]: pentest
|
||||
})
|
||||
).finally();
|
||||
*/
|
||||
// Change Pentest State
|
||||
const statePentest: Pentest = this.pentests$.getValue().find(pentest => pentest.refNumber === selectedPentest.refNumber);
|
||||
if (statePentest) {
|
||||
this.store.dispatch(new ChangePentest(statePentest));
|
||||
|
@ -111,5 +109,5 @@ enum ObjectiveColumns {
|
|||
TEST_ID = 'testId',
|
||||
TITLE = 'title',
|
||||
STATUS = 'status',
|
||||
FINDINGS = 'findings'
|
||||
FINDINGS_AND_COMMENTS = 'findings&comments'
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</th>
|
||||
<td nbTreeGridCell *nbTreeGridCellDef="let comment" class="related-finding-cell">
|
||||
<ng-container *ngIf="comment.data['relatedFindings'].length > 0; else NoRelatedFindings">
|
||||
<app-findig-widget [numberOfFindigs]="comment.data['relatedFindings'].length"></app-findig-widget>
|
||||
<app-findig-widget [numberOfFindings]="comment.data['relatedFindings'].length"></app-findig-widget>
|
||||
</ng-container>
|
||||
<ng-template #NoRelatedFindings>
|
||||
{{ 'comment.no.relatedFindings' | translate }}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<app-pentest-findings></app-pentest-findings>
|
||||
</nb-tab>
|
||||
<nb-tab class="pentest-tabset" tabTitle="{{ 'pentest.comments' | translate }}"
|
||||
badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="info">
|
||||
badgeText="{{currentNumberOfComments$.getValue()}}" badgeStatus="control">
|
||||
<app-pentest-comments></app-pentest-comments>
|
||||
</nb-tab>
|
||||
</nb-tabset>
|
||||
|
|
|
@ -7,6 +7,8 @@ import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
|||
import {Pentest} from '@shared/models/pentest.model';
|
||||
import {PentestService} from '@shared/services/api/pentest.service';
|
||||
import {NotificationService} from '@shared/services/toaster-service/notification.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -25,6 +27,7 @@ export class PentestContentComponent implements OnInit {
|
|||
constructor(
|
||||
private readonly pentestService: PentestService,
|
||||
private notificationService: NotificationService,
|
||||
private router: Router,
|
||||
private store: Store) {
|
||||
}
|
||||
|
||||
|
@ -33,11 +36,15 @@ export class PentestContentComponent implements OnInit {
|
|||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (selectedPentest: Pentest) => {
|
||||
if (selectedPentest) {
|
||||
this.pentest$.next(selectedPentest);
|
||||
const findings = selectedPentest.findingIds ? selectedPentest.findingIds.length : 0;
|
||||
this.currentNumberOfFindings$.next(findings);
|
||||
const comments = selectedPentest.commentIds ? selectedPentest.commentIds.length : 0;
|
||||
this.currentNumberOfComments$.next(comments);
|
||||
} else {
|
||||
this.router.navigate([Route.PROJECT_OVERVIEW]);
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
|
|
|
@ -79,7 +79,7 @@ export class PentestHeaderComponent implements OnInit {
|
|||
|
||||
onClickRouteBack(): void {
|
||||
// ToDo: Change to Objective Overview after routing is fixed
|
||||
this.router.navigate([Route.PROJECT_OVERVIEW])
|
||||
this.router.navigate([Route.OBJECTIVE_OVERVIEW])
|
||||
.then(
|
||||
() => {
|
||||
this.store.dispatch(new ChangePentest(null));
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {PentestComponent} from './pentest.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PentestComponent
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {ProjectOverviewComponent} from './project-overview.component';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectOverviewComponent,
|
||||
},
|
||||
{
|
||||
path: 'id',
|
||||
path: Route.OBJECTIVE_OVERVIEW,
|
||||
loadChildren: () => import('./project').then(mod => mod.ProjectModule),
|
||||
}
|
||||
];
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
routerLink="id"
|
||||
fragment="{{project.id}}"
|
||||
class="project-link project-header"
|
||||
[state]="{selectedProject:project}">
|
||||
(click)="onClickRouteToProject(project)">
|
||||
<h4>{{project?.title}}</h4>
|
||||
</nb-card-header>
|
||||
<nb-card-body class="project-link"
|
||||
routerLink="id"
|
||||
fragment="{{project.id}}"
|
||||
[state]="{selectedProject:project}">
|
||||
(click)="onClickRouteToProject(project)">
|
||||
<p class="project-subheader">
|
||||
{{'project.client' | translate}}:
|
||||
</p>
|
||||
|
|
|
@ -9,6 +9,10 @@ import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators';
|
|||
import {DialogService} from '@shared/services/dialog-service/dialog.service';
|
||||
import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component';
|
||||
import {ProjectDialogService} from '@shared/modules/project-dialog/service/project-dialog.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
import {InitProjectState} from '@shared/stores/project-state/project-state.actions';
|
||||
import {Store} from '@ngxs/store';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
|
@ -25,6 +29,8 @@ export class ProjectOverviewComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private store: Store,
|
||||
private router: Router,
|
||||
private projectService: ProjectService,
|
||||
private dialogService: DialogService,
|
||||
private projectDialogService: ProjectDialogService) {
|
||||
|
@ -163,6 +169,18 @@ export class ProjectOverviewComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onClickRouteToProject(project): void {
|
||||
this.router.navigate([Route.OBJECTIVE_OVERVIEW]).then(() => {
|
||||
this.store.dispatch(new InitProjectState(
|
||||
project,
|
||||
[],
|
||||
[]
|
||||
)).pipe(untilDestroyed(this)).subscribe();
|
||||
}, err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
// HTML only
|
||||
isLoading(): Observable<boolean> {
|
||||
return this.loading$.asObservable();
|
||||
|
|
|
@ -11,6 +11,7 @@ import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog
|
|||
import {CommonAppModule} from '../common-app.module';
|
||||
import {ConfirmDialogModule} from '@shared/modules/confirm-dialog/confirm-dialog.module';
|
||||
import {SecurityConfirmDialogModule} from '@shared/modules/security-confirm-dialog/security-confirm-dialog.module';
|
||||
import {RouterModule} from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -20,6 +21,10 @@ import {SecurityConfirmDialogModule} from '@shared/modules/security-confirm-dial
|
|||
imports: [
|
||||
CommonModule,
|
||||
CommonAppModule,
|
||||
RouterModule.forChild([{
|
||||
path: '',
|
||||
component: ProjectOverviewComponent
|
||||
}]),
|
||||
NbCardModule,
|
||||
NbButtonModule,
|
||||
NbProgressBarModule,
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {ProjectComponent} from './project.component';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectComponent
|
||||
},
|
||||
{
|
||||
path: 'pentest',
|
||||
path: Route.PENTEST_OBJECTIVE,
|
||||
loadChildren: () => import('../../pentest').then(mod => mod.PentestModule),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {Store} from '@ngxs/store';
|
||||
import {InitProjectState} from '@shared/stores/project-state/project-state.actions';
|
||||
import {Router} from '@angular/router';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
import {Route} from '@shared/models/route.enum';
|
||||
import {ProjectState} from '@shared/stores/project-state/project-state';
|
||||
import {Project} from '@shared/models/project.model';
|
||||
|
||||
@UntilDestroy()
|
||||
|
@ -20,19 +20,23 @@ export class ProjectComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (history?.state && 'selectedProject' in history?.state) {
|
||||
this.store.select(ProjectState.project).pipe(
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
next: (selectedProject: Project) => {
|
||||
this.initProjectStore();
|
||||
} else {
|
||||
this.router.navigate([Route.PROJECT_OVERVIEW]).finally();
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initProjectStore(): void {
|
||||
const project: Project = history?.state?.selectedProject ? history?.state?.selectedProject : null;
|
||||
this.store.dispatch(new InitProjectState(
|
||||
project,
|
||||
[],
|
||||
[]
|
||||
)).pipe(untilDestroyed(this)).subscribe();
|
||||
this.router.navigate([Route.OBJECTIVE_OVERVIEW]).then(() => {
|
||||
}, err => {
|
||||
this.router.navigate([Route.PROJECT_OVERVIEW]);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -216,6 +216,7 @@
|
|||
"title": "Titel",
|
||||
"findings": "Funde",
|
||||
"comments": "Kommentare",
|
||||
"findings&comments": "Funde & Kommentare",
|
||||
"status": "Status",
|
||||
"statusText": {
|
||||
"not_started": "Nicht angefangen",
|
||||
|
|
|
@ -216,6 +216,7 @@
|
|||
"title": "Title",
|
||||
"findings": "Findings",
|
||||
"comments": "Comments",
|
||||
"findings&comments": "Findings & Comments",
|
||||
"status": "Status",
|
||||
"statusText": {
|
||||
"not_started": "Not Started",
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import {Loading, LoadingState} from '@shared/models/loading.model';
|
||||
|
||||
export function calculatePercentage<T>(download: Loading<T>, event): Loading<T> {
|
||||
return {
|
||||
progress: event.total
|
||||
? Math.round((100 * event.loaded) / event.total)
|
||||
: download.progress,
|
||||
state: LoadingState.IN_PROGRESS,
|
||||
content: null
|
||||
};
|
||||
}
|
|
@ -8,6 +8,7 @@ export function downloadFile(data: any, type: string): void {
|
|||
const url = window.URL.createObjectURL(blob);
|
||||
const pwa = window.open(url);
|
||||
if (!pwa || pwa.closed || typeof pwa.closed === 'undefined') {
|
||||
alert('Please disable your Pop-up blocker and try again.');
|
||||
// ToDo: Do not use! Alert bugs loading progress bar
|
||||
// alert('Please disable your Pop-up blocker and try again.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import {HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {distinctUntilChanged, scan} from 'rxjs/operators';
|
||||
import {calculatePercentage} from '@shared/functions/calculate-percentage';
|
||||
import {Loading, LoadingState} from '@shared/models/loading.model';
|
||||
|
||||
function isHttpResponse<T>(event: HttpEvent<T>): event is HttpResponse<T> {
|
||||
return event.type === HttpEventType.Response;
|
||||
}
|
||||
|
||||
function isHttpProgressEvent(
|
||||
event: HttpEvent<unknown>
|
||||
): event is HttpProgressEvent {
|
||||
return (
|
||||
event.type === HttpEventType.DownloadProgress ||
|
||||
event.type === HttpEventType.UploadProgress
|
||||
);
|
||||
}
|
||||
|
||||
export function loadContent<T>(
|
||||
media?: (b: T) => void
|
||||
): (source: Observable<HttpEvent<T>>) => Observable<Loading<T>> {
|
||||
return (source: Observable<HttpEvent<T>>) =>
|
||||
source.pipe(
|
||||
scan(
|
||||
(download: Loading<T>, event): Loading<T> => {
|
||||
if (isHttpProgressEvent(event)) {
|
||||
return calculatePercentage(download, event);
|
||||
}
|
||||
if (isHttpResponse(event)) {
|
||||
if (media) {
|
||||
media(event.body);
|
||||
}
|
||||
return {
|
||||
progress: 100,
|
||||
state: LoadingState.DONE,
|
||||
content: event.body
|
||||
};
|
||||
}
|
||||
return download;
|
||||
},
|
||||
{state: LoadingState.PENDING, progress: 0, content: null}
|
||||
),
|
||||
distinctUntilChanged((a, b) => a.state === b.state
|
||||
&& a.progress === b.progress
|
||||
&& a.content === b.content
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export class Loading<T = any> {
|
||||
content?: T | null;
|
||||
progress: number;
|
||||
state: LoadingState;
|
||||
}
|
||||
|
||||
export enum LoadingState {
|
||||
PENDING = 'PENDING',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
DONE = 'DONE'
|
||||
}
|
|
@ -63,6 +63,7 @@ export function transformPentestsToObjectiveEntries(pentests: Pentest[]): Object
|
|||
refNumber: value.refNumber,
|
||||
status: value.status,
|
||||
findings: value.findingIds ? value.findingIds.length : 0,
|
||||
comments: value.commentIds ? value.commentIds.length : 0,
|
||||
kind: value.childEntries ? 'dir' : 'cell',
|
||||
childEntries: value.childEntries ? value.childEntries : null,
|
||||
expanded: !!value.childEntries
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
export enum Route {
|
||||
LOGIN = 'login',
|
||||
HOME = 'home',
|
||||
PROJECT_OVERVIEW = 'projects',
|
||||
OBJECTIVE_OVERVIEW = 'project/objectives',
|
||||
PENTEST_OBJECTIVE = 'project/objectives/pentest',
|
||||
PENTEST_OVERVIEW = 'pentest'
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
{{ 'global.project' | translate }}:
|
||||
</label>
|
||||
<b class="project-title">
|
||||
{{selectedProject$.getValue()?.title}}
|
||||
{{dialogData.options[0].additionalData.title}}
|
||||
</b>
|
||||
<!--Chart Objective Component-->
|
||||
<div fxLayout="column" fxLayoutGap="2rem" fxLayoutAlign="center center">
|
||||
<app-objective-chart [projectPentestData]="selectedProject$.getValue().projectPentests"></app-objective-chart>
|
||||
<app-objective-chart
|
||||
[projectPentestData]="selectedEvaluatedProject$.getValue().projectPentests"></app-objective-chart>
|
||||
<span
|
||||
class="hint"> {{'popup.info' | translate}} {{ 'report.hint' | translate: this.completedProjectPentests$?.getValue() }}
|
||||
</span>
|
||||
|
@ -64,4 +65,4 @@
|
|||
</nb-card-footer>
|
||||
</nb-card>
|
||||
|
||||
<app-loading-spinner [isLoading$]="isLoading()" *ngIf="isLoading() | async"></app-loading-spinner>
|
||||
<app-loading-bar *ngIf="downloadPentestReport$ | async as download" [isLoading$]="isLoading()" [loadingProgress]="download?.progress"></app-loading-bar>
|
||||
|
|
|
@ -82,7 +82,7 @@ describe('ExportReportDialogComponent', () => {
|
|||
TestBed.overrideProvider(NB_DIALOG_CONFIG, {useValue: mockedExportPentestDialogData});
|
||||
fixture = TestBed.createComponent(ExportReportDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.selectedProject$.next(mockProject);
|
||||
component.selectedEvaluatedProject$.next(mockProject);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
|
|
@ -10,8 +10,10 @@ import {BehaviorSubject, Observable} from 'rxjs';
|
|||
import {NotificationService, PopupType} from '@shared/services/toaster-service/notification.service';
|
||||
import {ProjectService} from '@shared/services/api/project.service';
|
||||
import {PentestStatus} from '@shared/models/pentest-status.model';
|
||||
import {tap} from 'rxjs/operators';
|
||||
import {shareReplay, tap} from 'rxjs/operators';
|
||||
import {downloadFile} from '@shared/functions/download-file.function';
|
||||
import {Loading, LoadingState} from '@shared/models/loading.model';
|
||||
import {HttpEvent, HttpEventType} from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-export-report-dialog',
|
||||
|
@ -41,10 +43,14 @@ export class ExportReportDialogComponent implements OnInit {
|
|||
|
||||
dialogData: GenericDialogData;
|
||||
|
||||
selectedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
|
||||
selectedEvaluatedProject$: BehaviorSubject<Project> = new BehaviorSubject<Project>(null);
|
||||
completedProjectPentests$: BehaviorSubject<any> = new BehaviorSubject<any>({completedObjectivesNumber: 0});
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
// Loading Bar Property
|
||||
downloadPentestReport$: Observable<Loading<ArrayBuffer>>;
|
||||
progress = 0;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dialogData = this.data;
|
||||
this.loadEvaluatedProject();
|
||||
|
@ -61,7 +67,7 @@ export class ExportReportDialogComponent implements OnInit {
|
|||
)
|
||||
.subscribe({
|
||||
next: (project: Project) => {
|
||||
this.selectedProject$.next(project);
|
||||
this.selectedEvaluatedProject$.next(project);
|
||||
const completedPentestObjectives = project.projectPentests.filter(pentest => pentest.status === PentestStatus.COMPLETED);
|
||||
this.completedProjectPentests$.next({completedObjectivesNumber: completedPentestObjectives.length});
|
||||
this.loading$.next(false);
|
||||
|
@ -75,18 +81,26 @@ export class ExportReportDialogComponent implements OnInit {
|
|||
}
|
||||
|
||||
onClickExport(reportFormat: string, reportLanguage: string): void {
|
||||
// Get project id from dialog data
|
||||
const projectId = this.dialogData.options[0].additionalData.id;
|
||||
// Loading is true as long as there is a response from the reporting service
|
||||
this.loading$.next(true);
|
||||
// Export pentest in choosen format
|
||||
switch (reportFormat) {
|
||||
case ExportFormatOptions.PDF: {
|
||||
this.reportingService.getReportPDFforProjectById(this.selectedProject$.getValue().id).pipe(
|
||||
// @ts-ignore
|
||||
this.downloadPentestReport$ = this.reportingService.getReportPDFforProjectById(projectId)
|
||||
.pipe(
|
||||
shareReplay(),
|
||||
untilDestroyed(this)
|
||||
).subscribe({
|
||||
);
|
||||
this.downloadPentestReport$.subscribe({
|
||||
next: (response) => {
|
||||
downloadFile(response, 'application/pdf');
|
||||
if (response.state === LoadingState.DONE) {
|
||||
downloadFile(response.content, 'application/pdf');
|
||||
this.loading$.next(false);
|
||||
this.notificationService.showPopup('report.popup.generation.success', PopupType.SUCCESS);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
console.error(error);
|
||||
|
|
|
@ -10,6 +10,7 @@ import {ExportReportDialogComponent} from '@shared/modules/export-report-dialog/
|
|||
import {ExportReportDialogService} from '@shared/modules/export-report-dialog/service/export-report-dialog.service';
|
||||
import {ReportingService} from '@shared/services/reporting/reporting.service';
|
||||
import {ObjectiveChartModule} from '@shared/modules/objective-chart/objective-chart.module';
|
||||
import {LoadingBarModule} from '@shared/widgets/loading-bar/loading-bar.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -27,7 +28,8 @@ import {ObjectiveChartModule} from '@shared/modules/objective-chart/objective-ch
|
|||
TranslateModule,
|
||||
ReactiveFormsModule,
|
||||
NbRadioModule,
|
||||
ObjectiveChartModule
|
||||
ObjectiveChartModule,
|
||||
LoadingBarModule
|
||||
],
|
||||
providers: [
|
||||
ExportReportDialogService,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {Loading} from '@shared/models/loading.model';
|
||||
import { loadContent } from '@shared/functions/load-content';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -16,8 +18,31 @@ export class ReportingService {
|
|||
/**
|
||||
* Get PDF Report by project id
|
||||
*/
|
||||
public getReportPDFforProjectById(projectId: string): Observable<Loading<ArrayBuffer>> {
|
||||
return this.http.get(`${this.reportBaseURL}/${projectId}/pdf`,
|
||||
{
|
||||
// @ts-ignore
|
||||
responseType: 'arraybuffer',
|
||||
reportProgress: true,
|
||||
observe: 'events'
|
||||
}
|
||||
).pipe(loadContent<ArrayBuffer>());
|
||||
}
|
||||
|
||||
/* ToDo: Remove report function without observing report progress
|
||||
public getReportPDFforProjectById(projectId: string): Observable<ArrayBuffer> {
|
||||
// @ts-ignore
|
||||
return this.http.get<ArrayBuffer>(`${this.reportBaseURL}/${projectId}/pdf`, {responseType: 'arraybuffer'})
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
export interface ReportDownloadConfiguration {
|
||||
options: {
|
||||
headers: HttpHeaders;
|
||||
params: HttpParams;
|
||||
observe: 'events';
|
||||
reportProgress: true;
|
||||
responseType: 'arraybuffer';
|
||||
withCredentials?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<div class="comment-widget">
|
||||
<ng-container *ngIf="numberOfComments > 0; else noComments">
|
||||
<fa-icon [icon]="fa.faComment" size="lg" class="comment-icon"></fa-icon>
|
||||
<span class="comments-count">{{numberOfComments}}</span>
|
||||
</ng-container>
|
||||
<ng-template #noComments>
|
||||
{{'-'}}
|
||||
</ng-template>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
@import '../../../assets/@theme/styles/themes';
|
||||
|
||||
.comment-widget {
|
||||
align-items: center;
|
||||
|
||||
.comment-icon {
|
||||
color: nb-theme(basic-text-color);
|
||||
}
|
||||
|
||||
.comments-count {
|
||||
margin-left: 0.45rem;
|
||||
color: nb-theme(basic-text-color);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {CommentWidgetComponent} from './comment-widget.component';
|
||||
import {FontAwesomeTestingModule} from '@fortawesome/angular-fontawesome/testing';
|
||||
|
||||
describe('CommentWidgetComponent', () => {
|
||||
let component: CommentWidgetComponent;
|
||||
let fixture: ComponentFixture<CommentWidgetComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
CommentWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeTestingModule
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommentWidgetComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import * as FA from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@Component({
|
||||
selector: 'app-comment-widget',
|
||||
templateUrl: './comment-widget.component.html',
|
||||
styleUrls: ['./comment-widget.component.scss']
|
||||
})
|
||||
export class CommentWidgetComponent implements OnInit {
|
||||
@Input() numberOfComments: number;
|
||||
|
||||
// HTML only
|
||||
readonly fa = FA;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
||||
import {CommentWidgetComponent} from '@shared/widgets/comment-widget/comment-widget.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CommentWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule
|
||||
],
|
||||
exports: [
|
||||
CommentWidgetComponent
|
||||
]
|
||||
})
|
||||
export class CommentWidgetModule {
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<div class="finding-widget">
|
||||
<ng-container *ngIf="numberOfFindigs > 0; else noFindings">
|
||||
<ng-container *ngIf="numberOfFindings > 0; else noFindings">
|
||||
<fa-icon [icon]="fa.faExclamationCircle" size="lg" class="finding-icon"></fa-icon>
|
||||
<span class="findings-count">{{numberOfFindigs}}</span>
|
||||
<span class="findings-count">{{numberOfFindings}}</span>
|
||||
</ng-container>
|
||||
<ng-template #noFindings>
|
||||
{{'-'}}
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as FA from '@fortawesome/free-solid-svg-icons';
|
|||
styleUrls: ['./findig-widget.component.scss']
|
||||
})
|
||||
export class FindigWidgetComponent implements OnInit {
|
||||
@Input() numberOfFindigs: number;
|
||||
@Input() numberOfFindings: number;
|
||||
|
||||
// HTML only
|
||||
readonly fa = FA;
|
||||
|
|
|
@ -7,13 +7,13 @@ import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
|
|||
declarations: [
|
||||
FindigWidgetComponent
|
||||
],
|
||||
exports: [
|
||||
FindigWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FontAwesomeModule
|
||||
]
|
||||
],
|
||||
exports: [
|
||||
FindigWidgetComponent
|
||||
],
|
||||
})
|
||||
export class FindigWidgetModule {
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class FindigModule { }
|
|
@ -0,0 +1,7 @@
|
|||
<div fxFill fxLayout="row" fxLayoutAlign="center center" *ngIf="loading" class="loading-overlay">
|
||||
<nb-progress-bar class="progress-bar"
|
||||
status="info"
|
||||
[value]="loadingProgress"
|
||||
[displayValue]="false">
|
||||
</nb-progress-bar>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
@import '../../../assets/@theme/styles/themes';
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
|
||||
background-color: nb-theme(layout-background-color);
|
||||
opacity: .5;
|
||||
|
||||
.progress-bar {
|
||||
width: 80%;
|
||||
height: max-content;
|
||||
// Overwrites the loading overlay
|
||||
opacity: 100 !important;
|
||||
z-index: 1001 !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoadingBarComponent } from './loading-bar.component';
|
||||
|
||||
describe('LoadingBarComponent', () => {
|
||||
let component: LoadingBarComponent;
|
||||
let fixture: ComponentFixture<LoadingBarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ LoadingBarComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoadingBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import {ChangeDetectorRef, Component, Input, OnChanges, OnInit} from '@angular/core';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-loading-bar',
|
||||
templateUrl: './loading-bar.component.html',
|
||||
styleUrls: ['./loading-bar.component.scss']
|
||||
})
|
||||
export class LoadingBarComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() isLoading$: Observable<boolean> = of(false);
|
||||
@Input() loadingProgress = 0;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = false;
|
||||
this.isLoading$
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe((value: boolean): void => {
|
||||
this.loading = value;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info('Current loading progress: ', this.loadingProgress);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {LoadingBarComponent} from '@shared/widgets/loading-bar/loading-bar.component';
|
||||
import {NbProgressBarModule} from '@nebular/theme';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LoadingBarComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NbProgressBarModule,
|
||||
FlexLayoutModule
|
||||
],
|
||||
exports: [
|
||||
LoadingBarComponent
|
||||
]
|
||||
})
|
||||
export class LoadingBarModule { }
|
|
@ -1,10 +1,12 @@
|
|||
package com.securityc4po.reporting.remote.model
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class ProjectReport(
|
||||
val id: String,
|
||||
val client: String,
|
||||
val title: String,
|
||||
val createdAt: String,
|
||||
val createdAt: Date,
|
||||
val tester: String,
|
||||
val summary: String? = null,
|
||||
var projectPentestReport: MutableList<PentestReport> = mutableListOf<PentestReport>(),
|
||||
|
|
|
@ -2,6 +2,9 @@ package com.securityc4po.reporting.remote.model.api
|
|||
|
||||
import com.securityc4po.reporting.remote.model.PentestReport
|
||||
import com.securityc4po.reporting.remote.model.ProjectReport
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
|
||||
|
||||
data class Project(
|
||||
val id: String,
|
||||
|
@ -19,7 +22,8 @@ fun Project.toProjectReport(): ProjectReport {
|
|||
id = this.id,
|
||||
client = this.client,
|
||||
title = this.title,
|
||||
createdAt = this.createdAt,
|
||||
/* Use the time of report creation */
|
||||
createdAt = Date.from(Instant.now()),
|
||||
tester = this.tester,
|
||||
summary = this.summary,
|
||||
projectPentestReport = mutableListOf<PentestReport>(),
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<property name="net.sf.jasperreports.json.field.expression" value="title"/>
|
||||
<fieldDescription><![CDATA[title]]></fieldDescription>
|
||||
</field>
|
||||
<field name="createdAt" class="java.lang.String">
|
||||
<field name="createdAt" class="java.util.Date">
|
||||
<property name="net.sf.jasperreports.json.field.expression" value="createdAt"/>
|
||||
<fieldDescription><![CDATA[createdAt]]></fieldDescription>
|
||||
</field>
|
||||
|
@ -144,7 +144,7 @@
|
|||
<textElement textAlignment="Right">
|
||||
<font fontName="SansSerif" size="12" isBold="true" isItalic="true"/>
|
||||
</textElement>
|
||||
<textFieldExpression><![CDATA[$F{createdAt}]]></textFieldExpression>
|
||||
<textFieldExpression><![CDATA[(new SimpleDateFormat("dd/MM/yyyy").format($F{createdAt}))]]></textFieldExpression>
|
||||
</textField>
|
||||
</band>
|
||||
</detail>
|
||||
|
|
Loading…
Reference in New Issue