1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-26 13:13:22 +00:00

Add progress tracking to risk insights report generation (#17199)

* Add progress tracking to risk insights report generation

* added skeleton page loader
This commit is contained in:
Maximilian Power
2025-11-07 15:03:58 +01:00
committed by GitHub
parent 0debc17c1f
commit a0bba3957b
9 changed files with 302 additions and 48 deletions

View File

@@ -107,6 +107,17 @@ export const ReportStatus = Object.freeze({
export type ReportStatus = (typeof ReportStatus)[keyof typeof ReportStatus];
export const ReportProgress = Object.freeze({
FetchingMembers: 1,
AnalyzingPasswords: 2,
CalculatingRisks: 3,
GeneratingReport: 4,
Saving: 5,
Complete: 6,
} as const);
export type ReportProgress = (typeof ReportProgress)[keyof typeof ReportProgress];
export interface RiskInsightsData {
id: OrganizationReportId;
creationDate: Date;

View File

@@ -56,6 +56,7 @@ import {
OrganizationReportSummary,
ReportStatus,
ReportState,
ReportProgress,
ApplicationHealthReportDetail,
} from "../../models/report-models";
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
@@ -128,6 +129,10 @@ export class RiskInsightsOrchestratorService {
private _generateReportTriggerSubject = new BehaviorSubject<boolean>(false);
generatingReport$ = this._generateReportTriggerSubject.asObservable();
// Report generation progress
private _reportProgressSubject = new BehaviorSubject<ReportProgress | null>(null);
reportProgress$ = this._reportProgressSubject.asObservable();
// --------------------------- Critical Application data ---------------------
criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
@@ -631,19 +636,33 @@ export class RiskInsightsOrchestratorService {
organizationId: OrganizationId,
userId: UserId,
): Observable<ReportState> {
// Generate the report
// Reset progress at the start
this._reportProgressSubject.next(null);
this.logService.debug("[RiskInsightsOrchestratorService] Fetching member cipher details");
this._reportProgressSubject.next(ReportProgress.FetchingMembers);
// Generate the report - fetch member ciphers and org ciphers in parallel
const memberCiphers$ = from(
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
).pipe(map((memberCiphers) => flattenMemberDetails(memberCiphers)));
return forkJoin([this._ciphers$.pipe(take(1)), memberCiphers$]).pipe(
tap(() => {
this.logService.debug("[RiskInsightsOrchestratorService] Generating new report");
// Start the generation pipeline
const reportGeneration$ = forkJoin([this._ciphers$.pipe(take(1)), memberCiphers$]).pipe(
switchMap(([ciphers, memberCiphers]) => {
this.logService.debug("[RiskInsightsOrchestratorService] Analyzing password health");
this._reportProgressSubject.next(ReportProgress.AnalyzingPasswords);
return this._getCipherHealth(ciphers ?? [], memberCiphers);
}),
map((cipherHealthReports) => {
this.logService.debug("[RiskInsightsOrchestratorService] Calculating risk scores");
this._reportProgressSubject.next(ReportProgress.CalculatingRisks);
return this.reportService.generateApplicationsReport(cipherHealthReports);
}),
tap(() => {
this.logService.debug("[RiskInsightsOrchestratorService] Generating report data");
this._reportProgressSubject.next(ReportProgress.GeneratingReport);
}),
switchMap(([ciphers, memberCiphers]) => this._getCipherHealth(ciphers ?? [], memberCiphers)),
map((cipherHealthReports) =>
this.reportService.generateApplicationsReport(cipherHealthReports),
),
withLatestFrom(this.rawReportData$),
map(([report, previousReport]) => {
// Update the application data
@@ -680,6 +699,8 @@ export class RiskInsightsOrchestratorService {
};
}),
switchMap(({ report, summary, applications, metrics }) => {
this.logService.debug("[RiskInsightsOrchestratorService] Saving report");
this._reportProgressSubject.next(ReportProgress.Saving);
return this.reportService
.saveRiskInsightsReport$(report, summary, applications, metrics, {
organizationId,
@@ -696,6 +717,10 @@ export class RiskInsightsOrchestratorService {
);
}),
// Update the running state
tap(() => {
this.logService.debug("[RiskInsightsOrchestratorService] Report generation complete");
this._reportProgressSubject.next(ReportProgress.Complete);
}),
map((mappedResult): ReportState => {
const { id, report, summary, applications, contentEncryptionKey } = mappedResult;
return {
@@ -723,7 +748,9 @@ export class RiskInsightsOrchestratorService {
error: null,
data: null,
}),
);
) as Observable<ReportState>;
return reportGeneration$;
}
// Calculates the metrics for a report

View File

@@ -10,6 +10,7 @@ import {
DrawerType,
RiskInsightsEnrichedData,
ReportStatus,
ReportProgress,
ApplicationHealthReportDetail,
OrganizationReportApplication,
} from "../../models";
@@ -38,6 +39,7 @@ export class RiskInsightsDataService {
readonly isGeneratingReport$: Observable<boolean> = of(false);
readonly criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
readonly hasCiphers$: Observable<boolean | null> = of(null);
readonly reportProgress$: Observable<ReportProgress | null> = of(null);
// New applications that need review (reviewedDate === null)
readonly newApplications$: Observable<ApplicationHealthReportDetail[]> = of([]);
@@ -62,6 +64,7 @@ export class RiskInsightsDataService {
this.enrichedReportData$ = this.orchestrator.enrichedReportData$;
this.criticalReportResults$ = this.orchestrator.criticalReportResults$;
this.newApplications$ = this.orchestrator.newApplications$;
this.reportProgress$ = this.orchestrator.reportProgress$;
this.hasCiphers$ = this.orchestrator.hasCiphers$.pipe(distinctUntilChanged());