Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/frontend/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ export const routes: Routes = [
},
{ path: 'roadmap', component: ReleaseRoadmapComponent },
{ path: 'cve-overview', component: CveOverviewComponent },
{ path: 'cve-overview/:cveId', component: CveOverviewComponent },
{
path: 'vulnerabilities/manage',
component: VulnerabilityImpactManageComponent,
canActivate: [FrankFrameworkMemberGuard],
},
{
path: 'vulnerabilities/manage/:cveId',
component: VulnerabilityImpactManageComponent,
canActivate: [FrankFrameworkMemberGuard],
},
{
path: 'release-manage/:id',
canActivate: [FrankFrameworkMemberGuard],
children: [
{ path: '', component: ReleaseManageComponent },
{ path: 'business-values', component: BusinessValueManageComponent },
{ path: 'business-values/:businessValueId', component: BusinessValueManageComponent },
],
},
{ path: 'not-found', component: NotFoundComponent },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

@for (detail of vulnerabilityDetails; track detail.vulnerability.cveId) {
<app-cve-overview-item
[id]="'cve-item-' + detail.vulnerability.cveId"
[detail]="detail"
[isSelected]="detail.vulnerability.cveId === selectedId"
[branchMaintainedMap]="branchMaintainedMap"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
<a
class="manage-impact-btn"
title="Edit the Frank!Framework impact for this CVE"
[routerLink]="['/vulnerabilities/manage']"
[queryParams]="{ cve: vulnerabilityDetail.vulnerability.cveId }"
[routerLink]="['/vulnerabilities/manage', vulnerabilityDetail.vulnerability.cveId]"
>
<span class="manage-icon" aria-hidden="true"></span>
<span>Manage Impact</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('CveOverviewRightPanelComponent', () => {

expect(manageButton).toBeTruthy();
expect(manageButton.getAttribute('href')).toContain('/vulnerabilities/manage');
expect(manageButton.getAttribute('href')).toContain('cve=CVE-2024-0001');
expect(manageButton.getAttribute('href')).toContain('/CVE-2024-0001');
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, OnInit, OnDestroy, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CommonModule, Location } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { Observable, Subject, catchError, debounceTime, distinctUntilChanged, finalize, of, takeUntil } from 'rxjs';
import {
Vulnerability,
Expand Down Expand Up @@ -94,11 +95,14 @@ export class CveOverviewComponent implements OnInit, OnDestroy {
private readonly releaseService = inject(ReleaseService);
private readonly authService = inject(AuthService);
private readonly impactLabelPipe = inject(ImpactLabelPipe);
private readonly location = inject(Location);
private readonly route = inject(ActivatedRoute);
private readonly destroy$ = new Subject<void>();
private readonly searchSubject = new Subject<string>();
private readonly branchStartDates = signal<Map<string, Date>>(new Map());

private isBulkLoading = false;
private pendingCveId: string | null = null;

private static emptyPage(page: number): VulnerabilityPage {
return {
Expand Down Expand Up @@ -175,8 +179,11 @@ export class CveOverviewComponent implements OnInit, OnDestroy {
this.resetAndFetch();
});

this.pendingCveId = this.route.snapshot.paramMap.get('cveId');
this.loadBranchStartDates();
this.fetchPage(0);
this.fetchPage(0, () => {
if (this.pendingCveId) this.findAndSelectCve(this.pendingCveId);
});
}

ngOnDestroy(): void {
Expand All @@ -187,6 +194,7 @@ export class CveOverviewComponent implements OnInit, OnDestroy {
public onSelectCve(vulnerability: Vulnerability): void {
this.selectedCveId.set(vulnerability.cveId);
this.showFilterPanel.set(false);
this.location.go(`/cve-overview/${vulnerability.cveId}`);
}

public onSearch(query: string): void {
Expand All @@ -208,11 +216,16 @@ export class CveOverviewComponent implements OnInit, OnDestroy {
this.showFilterPanel.update((isVisible) => !isVisible);
}

public fetchPage(page: number): void {
public fetchPage(page: number, onComplete?: () => void): void {
if (this.isLoading()) return;
this.isLoading.set(true);
this.requestPage(page)
.pipe(finalize(() => this.isLoading.set(false)))
.pipe(
finalize(() => {
this.isLoading.set(false);
onComplete?.();
}),
)
.subscribe((data) => {
this.allDetails.update((existing) => (page === 0 ? data.content : [...existing, ...data.content]));
this.currentPage.set(data.number);
Expand All @@ -234,6 +247,23 @@ export class CveOverviewComponent implements OnInit, OnDestroy {
this.fetchPage(0);
}

private findAndSelectCve(cveId: string): void {
const match = this.allDetails().find((detail) => detail.vulnerability.cveId === cveId);

if (match) {
this.selectedCveId.set(cveId);
this.pendingCveId = null;
setTimeout(() => {
document.querySelector(`#cve-item-${cveId}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
return;
}

if (this.isLastPage()) return;

this.fetchPage(this.currentPage() + 1, () => this.findAndSelectCve(cveId));
}

private loadBranchStartDates(): void {
this.releaseService
.getAllReleases()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { of, throwError } from 'rxjs';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
Expand Down Expand Up @@ -68,7 +67,7 @@ describe('BusinessValueManageComponent', () => {
let mockBusinessValueService: jasmine.SpyObj<BusinessValueService>;
let mockIssueService: jasmine.SpyObj<IssueService>;
let mockReleaseService: jasmine.SpyObj<ReleaseService>;
let mockLocation: jasmine.SpyObj<Location>;
let mockRouter: jasmine.SpyObj<Router>;

beforeEach(async () => {
mockBusinessValueService = jasmine.createSpyObj('BusinessValueService', [
Expand All @@ -79,7 +78,7 @@ describe('BusinessValueManageComponent', () => {
]);
mockIssueService = jasmine.createSpyObj('IssueService', ['getIssuesByReleaseId']);
mockReleaseService = jasmine.createSpyObj('ReleaseService', ['getReleaseById', 'getAllReleases']);
mockLocation = jasmine.createSpyObj('Location', ['back']);
mockRouter = jasmine.createSpyObj('Router', ['navigate']);

mockBusinessValueService.getBusinessValuesByReleaseId.and.returnValue(of(mockBusinessValues));
mockIssueService.getIssuesByReleaseId.and.returnValue(of(mockIssues));
Expand All @@ -92,11 +91,11 @@ describe('BusinessValueManageComponent', () => {
{ provide: BusinessValueService, useValue: mockBusinessValueService },
{ provide: IssueService, useValue: mockIssueService },
{ provide: ReleaseService, useValue: mockReleaseService },
{ provide: Location, useValue: mockLocation },
{ provide: Router, useValue: mockRouter },
{
provide: ActivatedRoute,
useValue: {
snapshot: { paramMap: { get: () => 'release-123' } },
snapshot: { paramMap: { get: (key: string) => key === 'id' ? 'release-123' : null } },
},
},
provideHttpClient(),
Expand Down Expand Up @@ -304,10 +303,10 @@ describe('BusinessValueManageComponent', () => {
expect(component.showCreateForm()).toBeTrue();
});

it('should go back using location service', () => {
it('should go back to parent location with router', () => {
component.goBack();

expect(mockLocation.back).toHaveBeenCalledWith();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/release-manage', 'release-123']);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit, inject, signal, computed } from '@angular/core';
import { CommonModule, Location } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { BusinessValue, BusinessValueService } from '../../../services/business-value.service';
import { Issue, IssueService } from '../../../services/issue.service';
import { Release, ReleaseService } from '../../../services/release.service';
Expand All @@ -15,6 +15,7 @@ import {
IssueWithSelection,
} from './business-value-issue-panel/business-value-issue-panel.component';
import { LoaderComponent } from '../../../components/loader/loader.component';
import { ReleaseManageComponent } from '../release-manage.component';

@Component({
selector: 'app-business-value-manage',
Expand Down Expand Up @@ -56,23 +57,26 @@ export class BusinessValueManageComponent implements OnInit {
public hasChanges = computed(() => this.hasIssueChanges());

private route = inject(ActivatedRoute);
private location = inject(Location);
private businessValueService = inject(BusinessValueService);
private issueService = inject(IssueService);
private releaseService = inject(ReleaseService);
private router = inject(Router);

private originalSelectedIssueIds = signal<Set<string>>(new Set());
private pendingBusinessValueId: string | null = null;
private readonly businessValuesPath = 'business-values';

ngOnInit(): void {
const releaseId = this.route.snapshot.paramMap.get('id');
if (releaseId) {
this.releaseId.set(releaseId);
this.pendingBusinessValueId = this.route.snapshot.paramMap.get('businessValueId');
this.fetchData(releaseId);
}
}

public goBack(): void {
this.location.back();
this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId()]);
}

public toggleCreateForm(): void {
Expand Down Expand Up @@ -126,6 +130,7 @@ export class BusinessValueManageComponent implements OnInit {
if (this.selectedBusinessValue()?.id === deletedId) {
this.selectedBusinessValue.set(null);
this.resetIssueSelection();
this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId(), this.businessValuesPath]);
}

this.closeDeleteModal();
Expand All @@ -149,8 +154,16 @@ export class BusinessValueManageComponent implements OnInit {
if (this.selectedBusinessValue()?.id === businessValue.id) {
this.selectedBusinessValue.set(null);
this.resetIssueSelection();
this.router.navigate([ReleaseManageComponent.releaseManagePath, this.releaseId(), this.businessValuesPath]);
} else {
this.selectedBusinessValue.set(businessValue);
this.router.navigate([
ReleaseManageComponent.releaseManagePath,
this.releaseId(),
this.businessValuesPath,
businessValue.id,
]);

this.businessValueService.getBusinessValueById(businessValue.id).subscribe({
next: (detailedBV) => {
const updatedList = this.businessValues().map((bv) => (bv.id === detailedBV.id ? detailedBV : bv));
Expand Down Expand Up @@ -239,7 +252,12 @@ export class BusinessValueManageComponent implements OnInit {
release: this.releaseService.getReleaseById(releaseId).pipe(catchError(() => of(null))),
allReleases: this.releaseService.getAllReleases().pipe(catchError(() => of([]))),
})
.pipe(finalize(() => this.isLoading.set(false)))
.pipe(
finalize(() => {
this.isLoading.set(false);
this.applyDeepLinkedBusinessValue();
}),
)
.subscribe(({ businessValues, issues, release, allReleases }) => {
this.businessValues.set(businessValues);
this.allIssues.set(issues ?? []);
Expand All @@ -265,6 +283,17 @@ export class BusinessValueManageComponent implements OnInit {
});
}

private applyDeepLinkedBusinessValue(): void {
if (!this.pendingBusinessValueId) return;

const match = this.businessValues().find((businessValue) => businessValue.id === this.pendingBusinessValueId);

if (match) {
this.pendingBusinessValueId = null;
this.selectBusinessValue(match);
}
}

private updateIssueSelection(currentBusinessValue: BusinessValue): void {
const currentConnectedIds = new Set(currentBusinessValue.issues?.map((issue) => issue.id));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ <h3>General Management</h3>
<p class="section-group-subtitle">Applies to all releases</p>
</div>
<div class="management-sections">
<a class="section-card vulnerabilities-card" [routerLink]="vulnerabilitiesLink()">
<a
class="section-card vulnerabilities-card"
[routerLink]="vulnerabilitiesLink()"
[queryParams]="vulnerabilitiesLinkQueryParams()"
>
<div class="card-icon-wrapper">
<svg
class="card-icon"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { BusinessValue, BusinessValueService } from '../../services/business-val
styleUrl: './release-manage.component.scss',
})
export class ReleaseManageComponent implements OnInit {
public static readonly releaseManagePath = '/release-manage';

public authService = inject(AuthService);
public release = signal<Release | null>(null);
public releaseIssues = signal<Issue[] | null>([]);
Expand Down Expand Up @@ -61,6 +63,10 @@ export class ReleaseManageComponent implements OnInit {
return '/vulnerabilities/manage';
}

public vulnerabilitiesLinkQueryParams(): { releaseId: string | null } {
return { releaseId: this.release()?.id ?? null };
}

public closeSection(): void {
this.activeSection.set(null);
}
Expand Down
Loading
Loading