mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +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": {
|
"generatingYourAccessIntelligence": {
|
||||||
"message": "Generating your Access Intelligence..."
|
"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": {
|
"riskInsightsRunReport": {
|
||||||
"message": "Run report"
|
"message": "Run report"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -107,6 +107,17 @@ export const ReportStatus = Object.freeze({
|
|||||||
|
|
||||||
export type ReportStatus = (typeof ReportStatus)[keyof typeof ReportStatus];
|
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 {
|
export interface RiskInsightsData {
|
||||||
id: OrganizationReportId;
|
id: OrganizationReportId;
|
||||||
creationDate: Date;
|
creationDate: Date;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
OrganizationReportSummary,
|
OrganizationReportSummary,
|
||||||
ReportStatus,
|
ReportStatus,
|
||||||
ReportState,
|
ReportState,
|
||||||
|
ReportProgress,
|
||||||
ApplicationHealthReportDetail,
|
ApplicationHealthReportDetail,
|
||||||
} from "../../models/report-models";
|
} from "../../models/report-models";
|
||||||
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
|
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
|
||||||
@@ -128,6 +129,10 @@ export class RiskInsightsOrchestratorService {
|
|||||||
private _generateReportTriggerSubject = new BehaviorSubject<boolean>(false);
|
private _generateReportTriggerSubject = new BehaviorSubject<boolean>(false);
|
||||||
generatingReport$ = this._generateReportTriggerSubject.asObservable();
|
generatingReport$ = this._generateReportTriggerSubject.asObservable();
|
||||||
|
|
||||||
|
// Report generation progress
|
||||||
|
private _reportProgressSubject = new BehaviorSubject<ReportProgress | null>(null);
|
||||||
|
reportProgress$ = this._reportProgressSubject.asObservable();
|
||||||
|
|
||||||
// --------------------------- Critical Application data ---------------------
|
// --------------------------- Critical Application data ---------------------
|
||||||
criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
|
criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
|
||||||
|
|
||||||
@@ -631,19 +636,33 @@ export class RiskInsightsOrchestratorService {
|
|||||||
organizationId: OrganizationId,
|
organizationId: OrganizationId,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Observable<ReportState> {
|
): 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(
|
const memberCiphers$ = from(
|
||||||
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
|
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
|
||||||
).pipe(map((memberCiphers) => flattenMemberDetails(memberCiphers)));
|
).pipe(map((memberCiphers) => flattenMemberDetails(memberCiphers)));
|
||||||
|
|
||||||
return forkJoin([this._ciphers$.pipe(take(1)), memberCiphers$]).pipe(
|
// Start the generation pipeline
|
||||||
tap(() => {
|
const reportGeneration$ = forkJoin([this._ciphers$.pipe(take(1)), memberCiphers$]).pipe(
|
||||||
this.logService.debug("[RiskInsightsOrchestratorService] Generating new report");
|
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$),
|
withLatestFrom(this.rawReportData$),
|
||||||
map(([report, previousReport]) => {
|
map(([report, previousReport]) => {
|
||||||
// Update the application data
|
// Update the application data
|
||||||
@@ -680,6 +699,8 @@ export class RiskInsightsOrchestratorService {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
switchMap(({ report, summary, applications, metrics }) => {
|
switchMap(({ report, summary, applications, metrics }) => {
|
||||||
|
this.logService.debug("[RiskInsightsOrchestratorService] Saving report");
|
||||||
|
this._reportProgressSubject.next(ReportProgress.Saving);
|
||||||
return this.reportService
|
return this.reportService
|
||||||
.saveRiskInsightsReport$(report, summary, applications, metrics, {
|
.saveRiskInsightsReport$(report, summary, applications, metrics, {
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -696,6 +717,10 @@ export class RiskInsightsOrchestratorService {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
// Update the running state
|
// Update the running state
|
||||||
|
tap(() => {
|
||||||
|
this.logService.debug("[RiskInsightsOrchestratorService] Report generation complete");
|
||||||
|
this._reportProgressSubject.next(ReportProgress.Complete);
|
||||||
|
}),
|
||||||
map((mappedResult): ReportState => {
|
map((mappedResult): ReportState => {
|
||||||
const { id, report, summary, applications, contentEncryptionKey } = mappedResult;
|
const { id, report, summary, applications, contentEncryptionKey } = mappedResult;
|
||||||
return {
|
return {
|
||||||
@@ -723,7 +748,9 @@ export class RiskInsightsOrchestratorService {
|
|||||||
error: null,
|
error: null,
|
||||||
data: null,
|
data: null,
|
||||||
}),
|
}),
|
||||||
);
|
) as Observable<ReportState>;
|
||||||
|
|
||||||
|
return reportGeneration$;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates the metrics for a report
|
// Calculates the metrics for a report
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
DrawerType,
|
DrawerType,
|
||||||
RiskInsightsEnrichedData,
|
RiskInsightsEnrichedData,
|
||||||
ReportStatus,
|
ReportStatus,
|
||||||
|
ReportProgress,
|
||||||
ApplicationHealthReportDetail,
|
ApplicationHealthReportDetail,
|
||||||
OrganizationReportApplication,
|
OrganizationReportApplication,
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
@@ -38,6 +39,7 @@ export class RiskInsightsDataService {
|
|||||||
readonly isGeneratingReport$: Observable<boolean> = of(false);
|
readonly isGeneratingReport$: Observable<boolean> = of(false);
|
||||||
readonly criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
|
readonly criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
|
||||||
readonly hasCiphers$: Observable<boolean | null> = of(null);
|
readonly hasCiphers$: Observable<boolean | null> = of(null);
|
||||||
|
readonly reportProgress$: Observable<ReportProgress | null> = of(null);
|
||||||
|
|
||||||
// New applications that need review (reviewedDate === null)
|
// New applications that need review (reviewedDate === null)
|
||||||
readonly newApplications$: Observable<ApplicationHealthReportDetail[]> = of([]);
|
readonly newApplications$: Observable<ApplicationHealthReportDetail[]> = of([]);
|
||||||
@@ -62,6 +64,7 @@ export class RiskInsightsDataService {
|
|||||||
this.enrichedReportData$ = this.orchestrator.enrichedReportData$;
|
this.enrichedReportData$ = this.orchestrator.enrichedReportData$;
|
||||||
this.criticalReportResults$ = this.orchestrator.criticalReportResults$;
|
this.criticalReportResults$ = this.orchestrator.criticalReportResults$;
|
||||||
this.newApplications$ = this.orchestrator.newApplications$;
|
this.newApplications$ = this.orchestrator.newApplications$;
|
||||||
|
this.reportProgress$ = this.orchestrator.reportProgress$;
|
||||||
|
|
||||||
this.hasCiphers$ = this.orchestrator.hasCiphers$.pipe(distinctUntilChanged());
|
this.hasCiphers$ = this.orchestrator.hasCiphers$.pipe(distinctUntilChanged());
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
@let status = dataService.reportStatus$ | async;
|
@let status = dataService.reportStatus$ | async;
|
||||||
@let hasCiphers = dataService.hasCiphers$ | async;
|
@let hasCiphers = dataService.hasCiphers$ | async;
|
||||||
|
@let isGeneratingReport = dataService.isGeneratingReport$ | async;
|
||||||
@if (status == ReportStatusEnum.Initializing || hasCiphers === null) {
|
@if (status == ReportStatusEnum.Initializing || hasCiphers === null) {
|
||||||
<!-- Show loading state when initializing risk insights -->
|
<!-- Show page loading state when initializing risk insights (quick page load) -->
|
||||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
<dirt-page-loading></dirt-page-loading>
|
||||||
} @else {
|
} @else {
|
||||||
<!-- Check final states after initial calls have been completed -->
|
<!-- Check final states after initial calls have been completed -->
|
||||||
@if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
|
@if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) {
|
||||||
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
||||||
<!-- Show empty state only when feature flag is enabled and there's no report data -->
|
<!-- 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) {
|
@if (!hasCiphers) {
|
||||||
<!-- Show Empty state when there are no applications (no ciphers to make reports on) -->
|
<!-- Show Empty state when there are no applications (no ciphers to make reports on) -->
|
||||||
<empty-state-card
|
<empty-state-card
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<!-- Show screen when there is report data OR when feature flag is disabled (show tabs even without data) -->
|
<!-- 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>
|
<div>
|
||||||
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
<h1 bitTypography="h1">{{ "accessIntelligence" | i18n }}</h1>
|
||||||
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="appsCount > 0">
|
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="appsCount > 0">
|
||||||
@@ -78,28 +79,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tw-flex-1 tw-flex tw-flex-col">
|
@if (status == ReportStatusEnum.Loading && isGeneratingReport) {
|
||||||
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
|
<!-- Show detailed progress when generating report (longer operation) -->
|
||||||
@if (isRiskInsightsActivityTabFeatureEnabled) {
|
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||||
<bit-tab label="{{ 'activity' | i18n }}">
|
} @else {
|
||||||
<dirt-all-activity></dirt-all-activity>
|
<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>
|
||||||
}
|
<bit-tab>
|
||||||
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
|
<ng-template bitTabLabel>
|
||||||
<dirt-all-applications></dirt-all-applications>
|
<i class="bwi bwi-star"></i>
|
||||||
</bit-tab>
|
{{
|
||||||
<bit-tab>
|
"criticalApplicationsWithCount"
|
||||||
<ng-template bitTabLabel>
|
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
|
||||||
<i class="bwi bwi-star"></i>
|
}}
|
||||||
{{
|
</ng-template>
|
||||||
"criticalApplicationsWithCount"
|
<dirt-critical-applications></dirt-critical-applications>
|
||||||
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
|
</bit-tab>
|
||||||
}}
|
</bit-tab-group>
|
||||||
</ng-template>
|
</div>
|
||||||
<dirt-critical-applications></dirt-critical-applications>
|
}
|
||||||
</bit-tab>
|
|
||||||
</bit-tab-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { animate, style, transition, trigger } from "@angular/animations";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
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 { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component";
|
||||||
import { EmptyStateCardComponent } from "./empty-state-card.component";
|
import { EmptyStateCardComponent } from "./empty-state-card.component";
|
||||||
import { RiskInsightsTabType } from "./models/risk-insights.models";
|
import { RiskInsightsTabType } from "./models/risk-insights.models";
|
||||||
|
import { PageLoadingComponent } from "./shared/page-loading.component";
|
||||||
import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.component";
|
import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.component";
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
@@ -55,6 +57,15 @@ import { ApplicationsLoadingComponent } from "./shared/risk-insights-loading.com
|
|||||||
DrawerHeaderComponent,
|
DrawerHeaderComponent,
|
||||||
AllActivityComponent,
|
AllActivityComponent,
|
||||||
ApplicationsLoadingComponent,
|
ApplicationsLoadingComponent,
|
||||||
|
PageLoadingComponent,
|
||||||
|
],
|
||||||
|
animations: [
|
||||||
|
trigger("fadeIn", [
|
||||||
|
transition(":enter", [
|
||||||
|
style({ opacity: 0 }),
|
||||||
|
animate("300ms 100ms ease-in", style({ opacity: 1 })),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RiskInsightsComponent implements OnInit, OnDestroy {
|
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">
|
<div class="tw-flex tw-justify-center tw-items-center tw-min-h-[60vh]">
|
||||||
<i
|
<div class="tw-flex tw-flex-col tw-items-center tw-gap-4">
|
||||||
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600"
|
<!-- Progress bar -->
|
||||||
title="{{ 'loading' | i18n }}"
|
<div class="tw-w-64" role="progressbar" attr.aria-label="{{ 'loadingProgress' | i18n }}">
|
||||||
aria-hidden="true"
|
<bit-progress
|
||||||
></i>
|
[barWidth]="progress()"
|
||||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
[showText]="false"
|
||||||
<p bitTypography="h2" class="tw-text-main tw-mb-0">
|
size="default"
|
||||||
{{ "generatingYourAccessIntelligence" | i18n }}
|
bgColor="primary"
|
||||||
</p>
|
></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>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,57 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
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 { 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
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||||
@Component({
|
@Component({
|
||||||
selector: "dirt-risk-insights-loading",
|
selector: "dirt-risk-insights-loading",
|
||||||
imports: [CommonModule, JslibModule],
|
imports: [CommonModule, JslibModule, ProgressModule],
|
||||||
templateUrl: "./risk-insights-loading.component.html",
|
templateUrl: "./risk-insights-loading.component.html",
|
||||||
})
|
})
|
||||||
export class ApplicationsLoadingComponent {
|
export class ApplicationsLoadingComponent implements OnInit {
|
||||||
constructor() {}
|
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