diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mocks/mock-data.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mocks/mock-data.ts index 022b0aa42fb..33dd8676223 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mocks/mock-data.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/mocks/mock-data.ts @@ -92,8 +92,8 @@ export const mockApplicationData: OrganizationReportApplication[] = [ ]; export const mockEnrichedReportData: ApplicationHealthReportDetailEnriched[] = [ - { ...mockApplication1, isMarkedAsCritical: true, ciphers: [] }, - { ...mockApplication2, isMarkedAsCritical: false, ciphers: [] }, + { ...mockApplication1, isMarkedAsCritical: true }, + { ...mockApplication2, isMarkedAsCritical: false }, ]; export const mockCipherViews: CipherView[] = [ diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts index 6196c788ecd..77a3cea419b 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-data-service.types.ts @@ -1,5 +1,3 @@ -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - import { ApplicationHealthReportDetail, OrganizationReportApplication, @@ -8,7 +6,6 @@ import { export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & { isMarkedAsCritical: boolean; - ciphers: CipherView[]; }; export interface RiskInsightsEnrichedData { reportData: ApplicationHealthReportDetailEnriched[]; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index c0fdbb7e022..a2c61e05913 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -84,6 +84,9 @@ export class RiskInsightsOrchestratorService { // ------------------------- Cipher data ------------------------- private _ciphersSubject = new BehaviorSubject(null); private _ciphers$ = this._ciphersSubject.asObservable(); + get ciphers$(): Observable { + return this._ciphers$; + } private _hasCiphersSubject$ = new BehaviorSubject(null); hasCiphers$ = this._hasCiphersSubject$.asObservable(); @@ -235,7 +238,6 @@ export class RiskInsightsOrchestratorService { application, updatedApplicationData, ), - ciphers: [], // explicitly ignore ciphers at this stage }), ); @@ -368,7 +370,6 @@ export class RiskInsightsOrchestratorService { application, updatedApplicationData, ), - ciphers: [], // explicitly ignore ciphers at this stage }), ); @@ -504,7 +505,6 @@ export class RiskInsightsOrchestratorService { application, updatedApplicationData, ), - ciphers: [], // explicitly ignore ciphers at this stage }), ); // For now, merge the report with the critical marking flag to make the enriched type @@ -662,7 +662,6 @@ export class RiskInsightsOrchestratorService { application, updatedApplicationData, ), - ciphers: [], // explicitly ignore ciphers at this stage }), ); @@ -956,8 +955,8 @@ export class RiskInsightsOrchestratorService { */ private _setupEnrichedReportData() { // Setup the enriched report data pipeline - const enrichmentSubscription = combineLatest([this.rawReportData$, this._ciphers$]).pipe( - switchMap(([rawReportData, ciphers]) => { + const enrichmentSubscription = combineLatest([this.rawReportData$]).pipe( + switchMap(([rawReportData]) => { this.logService.debug( "[RiskInsightsOrchestratorService] Enriching report data with ciphers and critical app status", ); @@ -968,7 +967,6 @@ export class RiskInsightsOrchestratorService { const enrichedReports: ApplicationHealthReportDetailEnriched[] = rawReports.map((app) => ({ ...app, isMarkedAsCritical: this.reportService.isCriticalApplication(app, criticalAppsData), - ciphers: ciphers?.filter((cipher) => app.cipherIds.includes(cipher.id)) ?? [], })); const enrichedData = { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts index cdfdbe740a0..2ea5a16c93a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, firstValueFrom, Observable, of, Subject } from "rxjs"; import { distinctUntilChanged, map } from "rxjs/operators"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { getAtRiskApplicationList, getAtRiskMemberList } from "../../helpers"; import { @@ -38,6 +39,7 @@ export class RiskInsightsDataService { readonly isGeneratingReport$: Observable = of(false); readonly criticalReportResults$: Observable = of(null); readonly hasCiphers$: Observable = of(null); + readonly ciphers$: Observable = of([]); // New applications that need review (reviewedDate === null) readonly newApplications$: Observable = of([]); @@ -64,6 +66,7 @@ export class RiskInsightsDataService { this.newApplications$ = this.orchestrator.newApplications$; this.hasCiphers$ = this.orchestrator.hasCiphers$.pipe(distinctUntilChanged()); + this.ciphers$ = this.orchestrator.ciphers$; // Expose the loading state this.reportStatus$ = this.reportState$.pipe( diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts index bf1294a34ce..ff238e2636a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts @@ -215,7 +215,6 @@ export class NewApplicationsDialogComponent { isMarkedAsCritical: updatedStateApplicationData.some( (a) => a.applicationName == application.applicationName && a.isCritical, ), - ciphers: [], // explicitly ignore ciphers at this stage }), ) || []; return from( diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts index b4e2bf466b9..abe03f716b4 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts @@ -1,8 +1,8 @@ -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { debounceTime } from "rxjs"; +import { combineLatest, debounceTime, of, switchMap } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { @@ -16,6 +16,7 @@ import { } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IconButtonModule, NoItemsModule, @@ -31,9 +32,8 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "dirt-all-applications", templateUrl: "./all-applications.component.html", imports: [ @@ -49,7 +49,9 @@ import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.co ], }) export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource< + ApplicationHealthReportDetailEnriched & { ciphers: CipherView[] } + >(); protected selectedUrls: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); protected organization = new Organization(); @@ -74,15 +76,31 @@ export class AllApplicationsComponent implements OnInit { } async ngOnInit() { - this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (report) => { - this.applicationSummary = report?.summaryData ?? createNewSummaryData(); - this.dataSource.data = report?.reportData ?? []; - }, - error: () => { - this.dataSource.data = []; - }, - }); + combineLatest([this.dataService.enrichedReportData$, this.dataService.ciphers$]) + .pipe( + switchMap(([report, ciphers]) => { + if (!report) { + return null; + } + + // Map ciphers to each application + const reportWithCiphers = report.reportData.map((app) => ({ + ...app, + ciphers: ciphers.filter((cipher) => app.cipherIds.includes(cipher.id)), + })); + return of({ ...report, reportData: reportWithCiphers }); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (report) => { + this.applicationSummary = report?.summaryData ?? createNewSummaryData(); + this.dataSource.data = report?.reportData ?? []; + }, + error: () => { + this.dataSource.data = []; + }, + }); } isMarkedAsCriticalItem(applicationName: string) { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts index 794df90da53..6f666271bb0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts @@ -1,10 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { debounceTime, EMPTY, map, switchMap } from "rxjs"; +import { combineLatest, debounceTime, EMPTY, map, switchMap } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { @@ -28,9 +28,8 @@ import { RiskInsightsTabType } from "../models/risk-insights.models"; import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "dirt-critical-applications", templateUrl: "./critical-applications.component.html", imports: [ @@ -72,18 +71,34 @@ export class CriticalApplicationsComponent implements OnInit { } async ngOnInit() { - this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (criticalReport) => { - this.dataSource.data = criticalReport?.reportData ?? []; - this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData(); - this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0; - }, - error: () => { - this.dataSource.data = []; - this.applicationSummary = createNewSummaryData(); - this.enableRequestPasswordChange = false; - }, - }); + combineLatest([this.dataService.criticalReportResults$, this.dataService.ciphers$]) + .pipe( + map(([report, ciphers]) => { + if (!report) { + return null; + } + const reportWithCiphers = (report?.reportData ?? []).map((app) => ({ + ...app, + ciphers: ciphers.filter((cipher) => app.cipherIds.includes(cipher.id)), + })); + return { ...report, reportData: reportWithCiphers }; + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe({ + next: (criticalReport) => { + this.dataSource.data = criticalReport?.reportData ?? []; + this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData(); + this.enableRequestPasswordChange = + criticalReport?.summaryData?.totalAtRiskMemberCount > 0; + }, + error: () => { + this.dataSource.data = []; + this.applicationSummary = createNewSummaryData(); + this.enableRequestPasswordChange = false; + }, + }); + this.activatedRoute.paramMap .pipe( takeUntilDestroyed(this.destroyRef),