mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +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:
@@ -4446,6 +4446,30 @@
|
||||
"generatingYourAccessIntelligence": {
|
||||
"message": "Generating your Access Intelligence..."
|
||||
},
|
||||
"fetchingMemberData": {
|
||||
"message": "Fetching member data..."
|
||||
},
|
||||
"analyzingPasswordHealth": {
|
||||
"message": "Analyzing password health..."
|
||||
},
|
||||
"calculatingRiskScores": {
|
||||
"message": "Calculating risk scores..."
|
||||
},
|
||||
"generatingReportData": {
|
||||
"message": "Generating report data..."
|
||||
},
|
||||
"savingReport": {
|
||||
"message": "Saving report..."
|
||||
},
|
||||
"compilingInsights": {
|
||||
"message": "Compiling insights..."
|
||||
},
|
||||
"loadingProgress": {
|
||||
"message": "Loading progress"
|
||||
},
|
||||
"thisMightTakeFewMinutes": {
|
||||
"message": "This might take a few minutes."
|
||||
},
|
||||
"riskInsightsRunReport": {
|
||||
"message": "Run report"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<ng-container>
|
||||
@let status = dataService.reportStatus$ | async;
|
||||
@let hasCiphers = dataService.hasCiphers$ | async;
|
||||
@let isGeneratingReport = dataService.isGeneratingReport$ | async;
|
||||
@if (status == ReportStatusEnum.Initializing || hasCiphers === null) {
|
||||
<!-- Show loading state when initializing risk insights -->
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
<!-- Show page loading state when initializing risk insights (quick page load) -->
|
||||
<dirt-page-loading></dirt-page-loading>
|
||||
} @else {
|
||||
<!-- Check final states after initial calls have been completed -->
|
||||
@if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
|
||||
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
||||
<!-- Show empty state only when feature flag is enabled and there's no report data -->
|
||||
<div class="tw-flex tw-justify-center tw-items-center tw-min-h-[70vh] tw-w-full">
|
||||
<div @fadeIn class="tw-flex tw-justify-center tw-items-center tw-min-h-[70vh] tw-w-full">
|
||||
@if (!hasCiphers) {
|
||||
<!-- Show Empty state when there are no applications (no ciphers to make reports on) -->
|
||||
<empty-state-card
|
||||
@@ -36,7 +37,7 @@
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Show screen when there is report data OR when feature flag is disabled (show tabs even without data) -->
|
||||
<div class="tw-min-h-screen tw-flex tw-flex-col">
|
||||
<div @fadeIn class="tw-min-h-screen tw-flex tw-flex-col">
|
||||
<div>
|
||||
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
||||
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="appsCount > 0">
|
||||
@@ -78,28 +79,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex-1 tw-flex tw-flex-col">
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
|
||||
@if (isRiskInsightsActivityTabFeatureEnabled) {
|
||||
<bit-tab label="{{ 'activity' | i18n }}">
|
||||
<dirt-all-activity></dirt-all-activity>
|
||||
@if (status == ReportStatusEnum.Loading && isGeneratingReport) {
|
||||
<!-- Show detailed progress when generating report (longer operation) -->
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
<div class="tw-flex-1 tw-flex tw-flex-col">
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
|
||||
@if (isRiskInsightsActivityTabFeatureEnabled) {
|
||||
<bit-tab label="{{ 'activity' | i18n }}">
|
||||
<dirt-all-activity></dirt-all-activity>
|
||||
</bit-tab>
|
||||
}
|
||||
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
|
||||
<dirt-all-applications></dirt-all-applications>
|
||||
</bit-tab>
|
||||
}
|
||||
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
|
||||
<dirt-all-applications></dirt-all-applications>
|
||||
</bit-tab>
|
||||
<bit-tab>
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-star"></i>
|
||||
{{
|
||||
"criticalApplicationsWithCount"
|
||||
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
|
||||
}}
|
||||
</ng-template>
|
||||
<dirt-critical-applications></dirt-critical-applications>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
</div>
|
||||
<bit-tab>
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-star"></i>
|
||||
{{
|
||||
"criticalApplicationsWithCount"
|
||||
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
|
||||
}}
|
||||
</ng-template>
|
||||
<dirt-critical-applications></dirt-critical-applications>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { animate, style, transition, trigger } from "@angular/animations";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
@@ -34,6 +35,7 @@ import { AllApplicationsComponent } from "./all-applications/all-applications.co
|
||||
import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component";
|
||||
import { EmptyStateCardComponent } from "./empty-state-card.component";
|
||||
import { RiskInsightsTabType } from "./models/risk-insights.models";
|
||||
import { PageLoadingComponent } from "./shared/page-loading.component";
|
||||
import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -55,6 +57,15 @@ import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.com
|
||||
DrawerHeaderComponent,
|
||||
AllActivityComponent,
|
||||
ApplicationsLoadingComponent,
|
||||
PageLoadingComponent,
|
||||
],
|
||||
animations: [
|
||||
trigger("fadeIn", [
|
||||
transition(":enter", [
|
||||
style({ opacity: 0 }),
|
||||
animate("300ms 100ms ease-in", style({ opacity: 1 })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { animate, style, transition, trigger } from "@angular/animations";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
CardComponent as BitCardComponent,
|
||||
SkeletonComponent,
|
||||
SkeletonGroupComponent,
|
||||
SkeletonTextComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// Page loading component for quick initial loads
|
||||
// Uses skeleton animations to match the full page layout including header, tabs, and widget cards
|
||||
// Includes smooth fade-out transition when loading completes
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "dirt-page-loading",
|
||||
imports: [
|
||||
JslibModule,
|
||||
BitCardComponent,
|
||||
SkeletonComponent,
|
||||
SkeletonGroupComponent,
|
||||
SkeletonTextComponent,
|
||||
],
|
||||
animations: [
|
||||
trigger("fadeOut", [transition(":leave", [animate("300ms ease-out", style({ opacity: 0 }))])]),
|
||||
],
|
||||
template: `
|
||||
<div class="tw-sr-only" role="status">{{ "loading" | i18n }}</div>
|
||||
|
||||
<div @fadeOut class="tw-min-h-screen tw-flex tw-flex-col">
|
||||
<!-- Header Section -->
|
||||
<header>
|
||||
<!-- Page Title -->
|
||||
<bit-skeleton edgeShape="box" class="tw-h-10 tw-w-48 tw-mb-2"></bit-skeleton>
|
||||
|
||||
<!-- Description -->
|
||||
<bit-skeleton edgeShape="box" class="tw-h-5 tw-w-96 tw-mb-2"></bit-skeleton>
|
||||
|
||||
<!-- Info Banner -->
|
||||
<div
|
||||
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center tw-gap-4"
|
||||
>
|
||||
<bit-skeleton edgeShape="box" class="tw-size-5"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-4 tw-flex-1 tw-max-w-md"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-8 tw-w-32"></bit-skeleton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tabs Section -->
|
||||
<div class="tw-flex-1 tw-flex tw-flex-col">
|
||||
<!-- Tab Headers -->
|
||||
<div class="tw-flex tw-gap-6 tw-mb-6">
|
||||
<bit-skeleton edgeShape="box" class="tw-h-10 tw-w-24"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-10 tw-w-32"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-10 tw-w-40"></bit-skeleton>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Activity Cards Grid -->
|
||||
<ul
|
||||
class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none"
|
||||
>
|
||||
<!-- Password Change Metric skeleton -->
|
||||
<li class="tw-col-span-1">
|
||||
<bit-card class="tw-h-56">
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-48 tw-mb-2"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-9 tw-w-full tw-mb-2"></bit-skeleton>
|
||||
<bit-skeleton-text [lines]="2" class="tw-w-3/4"></bit-skeleton-text>
|
||||
</div>
|
||||
</bit-card>
|
||||
</li>
|
||||
|
||||
<!-- Activity Card 1: At Risk Members -->
|
||||
<li class="tw-col-span-1">
|
||||
<bit-card class="tw-h-56">
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-32 tw-mb-4"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-9 tw-w-24 tw-mb-4"></bit-skeleton>
|
||||
<bit-skeleton-text [lines]="2" class="tw-w-full tw-mb-4"></bit-skeleton-text>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-40"></bit-skeleton>
|
||||
</div>
|
||||
</bit-card>
|
||||
</li>
|
||||
|
||||
<!-- Activity Card 2: Critical Applications -->
|
||||
<li class="tw-col-span-1">
|
||||
<bit-card class="tw-h-56">
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-36 tw-mb-4"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-9 tw-w-32 tw-mb-4"></bit-skeleton>
|
||||
<bit-skeleton-text [lines]="2" class="tw-w-full tw-mb-4"></bit-skeleton-text>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-44"></bit-skeleton>
|
||||
</div>
|
||||
</bit-card>
|
||||
</li>
|
||||
|
||||
<!-- Activity Card 3: Applications Needing Review -->
|
||||
<li class="tw-col-span-1">
|
||||
<bit-card class="tw-h-56">
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<bit-skeleton edgeShape="box" class="tw-h-6 tw-w-44 tw-mb-4"></bit-skeleton>
|
||||
<bit-skeleton-group class="tw-mb-4">
|
||||
<bit-skeleton edgeShape="circle" class="tw-size-5" slot="start"></bit-skeleton>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-9 tw-w-28"></bit-skeleton>
|
||||
</bit-skeleton-group>
|
||||
<bit-skeleton-text [lines]="2" class="tw-w-full tw-mb-4"></bit-skeleton-text>
|
||||
<bit-skeleton edgeShape="box" class="tw-h-8 tw-w-28"></bit-skeleton>
|
||||
</div>
|
||||
</bit-card>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class PageLoadingComponent {}
|
||||
@@ -1,11 +1,23 @@
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center tw-min-h-[70vh] tw-gap-4">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
<p bitTypography="h2" class="tw-text-main tw-mb-0">
|
||||
{{ "generatingYourAccessIntelligence" | i18n }}
|
||||
</p>
|
||||
<div class="tw-flex tw-justify-center tw-items-center tw-min-h-[60vh]">
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4">
|
||||
<!-- Progress bar -->
|
||||
<div class="tw-w-64" role="progressbar" attr.aria-label="{{ 'loadingProgress' | i18n }}">
|
||||
<bit-progress
|
||||
[barWidth]="progress()"
|
||||
[showText]="false"
|
||||
size="default"
|
||||
bgColor="primary"
|
||||
></bit-progress>
|
||||
</div>
|
||||
|
||||
<!-- Status message and subtitle -->
|
||||
<div class="tw-text-center tw-flex tw-flex-col tw-gap-1">
|
||||
<span class="tw-text-main tw-text-base tw-font-medium tw-leading-4">
|
||||
{{ currentMessage() | i18n }}
|
||||
</span>
|
||||
<span class="tw-text-muted tw-text-sm tw-font-normal tw-leading-4">
|
||||
{{ "thisMightTakeFewMinutes" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,57 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
ReportProgress,
|
||||
RiskInsightsDataService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { ProgressModule } from "@bitwarden/components";
|
||||
|
||||
const PROGRESS_STEPS = [
|
||||
{ step: ReportProgress.FetchingMembers, message: "fetchingMemberData", progress: 20 },
|
||||
{ step: ReportProgress.AnalyzingPasswords, message: "analyzingPasswordHealth", progress: 40 },
|
||||
{ step: ReportProgress.CalculatingRisks, message: "calculatingRiskScores", progress: 60 },
|
||||
{ step: ReportProgress.GeneratingReport, message: "generatingReportData", progress: 80 },
|
||||
{ step: ReportProgress.Saving, message: "savingReport", progress: 95 },
|
||||
{ step: ReportProgress.Complete, message: "compilingInsights", progress: 100 },
|
||||
] as const;
|
||||
|
||||
type LoadingMessage = (typeof PROGRESS_STEPS)[number]["message"];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "dirt-risk-insights-loading",
|
||||
imports: [CommonModule, JslibModule],
|
||||
imports: [CommonModule, JslibModule, ProgressModule],
|
||||
templateUrl: "./risk-insights-loading.component.html",
|
||||
})
|
||||
export class ApplicationsLoadingComponent {
|
||||
constructor() {}
|
||||
export class ApplicationsLoadingComponent implements OnInit {
|
||||
private dataService = inject(RiskInsightsDataService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly currentMessage = signal<LoadingMessage>(PROGRESS_STEPS[0].message);
|
||||
readonly progress = signal<number>(PROGRESS_STEPS[0].progress);
|
||||
|
||||
ngOnInit(): void {
|
||||
// Subscribe to actual progress events from the orchestrator
|
||||
this.dataService.reportProgress$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((progressStep) => {
|
||||
if (progressStep === null) {
|
||||
// Reset to initial state
|
||||
this.currentMessage.set(PROGRESS_STEPS[0].message);
|
||||
this.progress.set(PROGRESS_STEPS[0].progress);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the matching step configuration
|
||||
const stepConfig = PROGRESS_STEPS.find((config) => config.step === progressStep);
|
||||
if (stepConfig) {
|
||||
this.currentMessage.set(stepConfig.message);
|
||||
this.progress.set(stepConfig.progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user