From 4e5f2d4b8dd137afca25d5b941a6a7fda2ad319a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 31 Oct 2025 16:11:44 -0400 Subject: [PATCH] feat(risk-insights): enrich newApplications$ observable with full metrics - Update newApplications$ to return NewApplicationDetail[] instead of string[] - Combine applicationData (for reviewedDate filter) with reportData (for metrics) - Include atRiskPasswordCount, passwordCount, and atRiskMemberCount per application - Filter out applications without matching report details - Update distinctUntilChanged to compare all application properties - Provides complete data at source, eliminates need for downstream filtering Part 2 of 5: Orchestrator now returns enriched new application data. --- .../risk-insights-orchestrator.service.ts | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) 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 650726628c6..90521429648 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 @@ -52,6 +52,7 @@ import { RiskInsightsEnrichedData } from "../../models/report-data-service.types import { CipherHealthReport, MemberDetails, + NewApplicationDetail, OrganizationReportApplication, OrganizationReportSummary, ReportStatus, @@ -98,18 +99,52 @@ export class RiskInsightsOrchestratorService { enrichedReportData$ = this._enrichedReportDataSubject.asObservable(); // New applications that haven't been reviewed (reviewedDate === null) - newApplications$: Observable = this.rawReportData$.pipe( + // Returns full application details for display in the new applications dialog + newApplications$: Observable = this.rawReportData$.pipe( map((reportState) => { - if (!reportState.data?.applicationData) { + if (!reportState.data?.applicationData || !reportState.data?.reportData) { return []; } - return reportState.data.applicationData - .filter((app) => app.reviewedDate === null) - .map((app) => app.applicationName); + + // Get applications that haven't been reviewed + const unreviewedApps = reportState.data.applicationData.filter( + (app) => app.reviewedDate === null, + ); + + // Map to NewApplicationDetail with full data from reportData + return unreviewedApps + .map((app) => { + // Find matching report data for this application + const reportDetail = reportState.data!.reportData.find( + (report) => report.applicationName === app.applicationName, + ); + + // Skip if no matching report detail found + if (!reportDetail) { + return null; + } + + return { + applicationName: app.applicationName, + atRiskPasswordCount: reportDetail.atRiskPasswordCount, + passwordCount: reportDetail.passwordCount, + atRiskMemberCount: reportDetail.atRiskMemberCount, + } as NewApplicationDetail; + }) + .filter((app): app is NewApplicationDetail => app !== null); + }), + distinctUntilChanged((prev, curr) => { + if (prev.length !== curr.length) { + return false; + } + return prev.every( + (prevApp, i) => + prevApp.applicationName === curr[i].applicationName && + prevApp.atRiskPasswordCount === curr[i].atRiskPasswordCount && + prevApp.passwordCount === curr[i].passwordCount && + prevApp.atRiskMemberCount === curr[i].atRiskMemberCount, + ); }), - distinctUntilChanged( - (prev, curr) => prev.length === curr.length && prev.every((app, i) => app === curr[i]), - ), shareReplay({ bufferSize: 1, refCount: true }), );