mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-27694] Handle empty report response (#17162)
* Consolidate loading state and handle null report from api response * Fix jumping of page when ciphers are still loading * Fix type errors * Fix loading state
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
@if (dataService.isLoading$ | async) {
|
||||
@if ((dataService.reportStatus$ | async) == ReportStatusEnum.Loading) {
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
<ul
|
||||
|
||||
@@ -5,6 +5,7 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
AllActivitiesService,
|
||||
ReportStatus,
|
||||
RiskInsightsDataService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -44,6 +45,8 @@ export class AllActivityComponent implements OnInit {
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
|
||||
protected ReportStatusEnum = ReportStatus;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@if (dataService.isLoading$ | async) {
|
||||
@if ((dataService.reportStatus$ | async) == ReportStatusEnum.Loading) {
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
@let drawerDetails = dataService.drawerDetails$ | async;
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
RiskInsightsDataService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
||||
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import {
|
||||
OrganizationReportSummary,
|
||||
ReportStatus,
|
||||
} 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 {
|
||||
@@ -53,6 +56,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
noItemsIcon = Security;
|
||||
protected markingAsCritical = false;
|
||||
protected applicationSummary: OrganizationReportSummary = createNewSummaryData();
|
||||
protected ReportStatusEnum = ReportStatus;
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading">
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-justify-between tw-mb-4">
|
||||
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
|
||||
<button
|
||||
|
||||
@@ -46,7 +46,6 @@ import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks
|
||||
})
|
||||
export class CriticalApplicationsComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
protected loading = false;
|
||||
protected enableRequestPasswordChange = false;
|
||||
protected organizationId: OrganizationId;
|
||||
noItemsIcon = Security;
|
||||
|
||||
@@ -1,83 +1,106 @@
|
||||
<ng-container>
|
||||
<div class="tw-min-h-screen tw-flex tw-flex-col">
|
||||
<div>
|
||||
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
|
||||
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="shouldShowTabs">
|
||||
{{ "reviewAtRiskPasswords" | i18n }}
|
||||
</div>
|
||||
@if (dataLastUpdated) {
|
||||
<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"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
@let isRunningReport = dataService.isGeneratingReport$ | async;
|
||||
<span class="tw-flex tw-justify-center">
|
||||
<button
|
||||
*ngIf="!isRunningReport"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
|
||||
tabindex="0"
|
||||
[bitAction]="generateReport.bind(this)"
|
||||
>
|
||||
{{ "riskInsightsRunReport" | i18n }}
|
||||
</button>
|
||||
<span>
|
||||
<i
|
||||
*ngIf="isRunningReport"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="tw-flex-1 tw-flex tw-flex-col">
|
||||
@if (shouldShowTabs) {
|
||||
<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>
|
||||
<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>
|
||||
} @else {
|
||||
<div class="tw-flex tw-justify-center tw-items-center tw-min-h-[70vh] tw-w-full">
|
||||
@let status = dataService.reportStatus$ | async;
|
||||
@let hasCiphers = dataService.hasCiphers$ | async;
|
||||
@if (status == ReportStatusEnum.Initializing || hasCiphers === null) {
|
||||
<!-- Show loading state when initializing risk insights -->
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
<!-- Check final states after initial calls have been completed -->
|
||||
@if (!(dataService.hasReportData$ | async)) {
|
||||
<div 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
|
||||
[videoSrc]="emptyStateVideoSrc"
|
||||
[title]="emptyStateTitle"
|
||||
[description]="emptyStateDescription"
|
||||
[title]="this.i18nService.t('noApplicationsInOrgTitle', organizationName)"
|
||||
[description]="this.i18nService.t('noApplicationsInOrgDescription')"
|
||||
[benefits]="emptyStateBenefits"
|
||||
[buttonText]="emptyStateButtonText"
|
||||
[buttonIcon]="emptyStateButtonIcon"
|
||||
[buttonAction]="emptyStateButtonAction"
|
||||
[buttonText]="this.i18nService.t('importData')"
|
||||
[buttonIcon]="IMPORT_ICON"
|
||||
[buttonAction]="this.goToImportPage"
|
||||
></empty-state-card>
|
||||
} @else {
|
||||
<!-- Show empty state for no reports run -->
|
||||
<empty-state-card
|
||||
[videoSrc]="emptyStateVideoSrc"
|
||||
[title]="this.i18nService.t('noReportRunTitle')"
|
||||
[description]="this.i18nService.t('noReportRunDescription')"
|
||||
[benefits]="emptyStateBenefits"
|
||||
[buttonText]="this.i18nService.t('riskInsightsRunReport')"
|
||||
[buttonIcon]=""
|
||||
[buttonAction]="this.generateReport.bind(this)"
|
||||
></empty-state-card>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Show screen when there is report data -->
|
||||
<div class="tw-min-h-screen tw-flex tw-flex-col">
|
||||
<div>
|
||||
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
|
||||
<div class="tw-text-main tw-max-w-4xl tw-mb-2" *ngIf="appsCount > 0">
|
||||
{{ "reviewAtRiskPasswords" | i18n }}
|
||||
</div>
|
||||
@if (dataLastUpdated) {
|
||||
<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"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
@let isRunningReport = dataService.isGeneratingReport$ | async;
|
||||
<span class="tw-flex tw-justify-center">
|
||||
<button
|
||||
*ngIf="!isRunningReport"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
|
||||
tabindex="0"
|
||||
[bitAction]="generateReport.bind(this)"
|
||||
>
|
||||
{{ "riskInsightsRunReport" | i18n }}
|
||||
</button>
|
||||
<span>
|
||||
<i
|
||||
*ngIf="isRunningReport"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (dataService.drawerDetails$ | async; as drawerDetails) {
|
||||
<bit-drawer style="width: 30%" [(open)]="isDrawerOpen" (openChange)="dataService.closeDrawer()">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { map, tap } from "rxjs/operators";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
DrawerType,
|
||||
ReportStatus,
|
||||
RiskInsightsDataService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -29,6 +30,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 { 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
|
||||
@@ -48,41 +50,35 @@ import { RiskInsightsTabType } from "./models/risk-insights.models";
|
||||
DrawerBodyComponent,
|
||||
DrawerHeaderComponent,
|
||||
AllActivityComponent,
|
||||
ApplicationsLoadingComponent,
|
||||
],
|
||||
})
|
||||
export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private _isDrawerOpen: boolean = false;
|
||||
protected ReportStatusEnum = ReportStatus;
|
||||
|
||||
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps;
|
||||
isRiskInsightsActivityTabFeatureEnabled: boolean = false;
|
||||
|
||||
appsCount: number = 0;
|
||||
// Leaving this commented because it's not used but seems important
|
||||
// notifiedMembersCount: number = 0;
|
||||
|
||||
private organizationId: OrganizationId = "" as OrganizationId;
|
||||
|
||||
dataLastUpdated: Date | null = null;
|
||||
refetching: boolean = false;
|
||||
|
||||
// Empty state properties
|
||||
protected hasReportBeenRun = false;
|
||||
protected reportHasLoaded = false;
|
||||
protected hasVaultItems = false;
|
||||
private organizationName = "";
|
||||
protected organizationName = "";
|
||||
|
||||
// Empty state computed properties
|
||||
protected shouldShowImportDataState = false;
|
||||
protected emptyStateTitle = "";
|
||||
protected emptyStateDescription = "";
|
||||
protected emptyStateBenefits: [string, string][] = [];
|
||||
protected emptyStateButtonText = "";
|
||||
protected emptyStateButtonIcon = "";
|
||||
protected emptyStateButtonAction: (() => void) | null = null;
|
||||
protected emptyStateBenefits: [string, string][] = [
|
||||
[this.i18nService.t("benefit1Title"), this.i18nService.t("benefit1Description")],
|
||||
[this.i18nService.t("benefit2Title"), this.i18nService.t("benefit2Description")],
|
||||
[this.i18nService.t("benefit3Title"), this.i18nService.t("benefit3Description")],
|
||||
];
|
||||
protected emptyStateVideoSrc: string | null = "/videos/risk-insights-mark-as-critical.mp4";
|
||||
|
||||
private static readonly IMPORT_ICON = "bwi bwi-download";
|
||||
protected IMPORT_ICON = "bwi bwi-download";
|
||||
|
||||
// TODO: See https://github.com/bitwarden/clients/pull/16832#discussion_r2474523235
|
||||
|
||||
@@ -91,7 +87,7 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
private i18nService: I18nService,
|
||||
protected i18nService: I18nService,
|
||||
) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||
@@ -125,28 +121,15 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Combine report data, vault items check, organization details, and generation state
|
||||
// This declarative pattern ensures proper cleanup and prevents memory leaks
|
||||
combineLatest([
|
||||
this.dataService.enrichedReportData$,
|
||||
this.dataService.hasVaultItems$,
|
||||
this.dataService.organizationDetails$,
|
||||
this.dataService.isGeneratingReport$,
|
||||
])
|
||||
combineLatest([this.dataService.enrichedReportData$, this.dataService.organizationDetails$])
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(([report, hasVaultItems, orgDetails, isGenerating]) => {
|
||||
.subscribe(([report, orgDetails]) => {
|
||||
// Update report state
|
||||
this.reportHasLoaded = true;
|
||||
this.hasReportBeenRun = !!report?.creationDate;
|
||||
this.appsCount = report?.reportData.length ?? 0;
|
||||
this.dataLastUpdated = report?.creationDate ?? null;
|
||||
|
||||
// Update vault items state
|
||||
this.hasVaultItems = hasVaultItems;
|
||||
|
||||
// Update organization name
|
||||
this.organizationName = orgDetails?.organizationName ?? "";
|
||||
|
||||
// Update all empty state properties based on current state
|
||||
this.updateEmptyStateProperties(isGenerating);
|
||||
});
|
||||
|
||||
// Subscribe to drawer state changes
|
||||
@@ -171,10 +154,6 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
get shouldShowTabs(): boolean {
|
||||
return this.appsCount > 0;
|
||||
}
|
||||
|
||||
async onTabChange(newIndex: number): Promise<void> {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
@@ -228,37 +207,4 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
|
||||
"import",
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates all empty state properties based on current state.
|
||||
* Called whenever the underlying data (hasVaultItems, hasReportBeenRun, reportHasLoaded) changes.
|
||||
*/
|
||||
private updateEmptyStateProperties(isGenerating: boolean): void {
|
||||
// Calculate boolean flags
|
||||
// Note: We only show empty states when there are NO apps (appsCount === 0)
|
||||
// The template uses @if(shouldShowTabs) to determine whether to show tabs or empty state
|
||||
this.shouldShowImportDataState = !this.hasVaultItems && !isGenerating;
|
||||
|
||||
// Update benefits (constant for all states)
|
||||
this.emptyStateBenefits = [
|
||||
[this.i18nService.t("benefit1Title"), this.i18nService.t("benefit1Description")],
|
||||
[this.i18nService.t("benefit2Title"), this.i18nService.t("benefit2Description")],
|
||||
[this.i18nService.t("benefit3Title"), this.i18nService.t("benefit3Description")],
|
||||
];
|
||||
|
||||
// Update all state-dependent properties in single if/else
|
||||
if (this.shouldShowImportDataState) {
|
||||
this.emptyStateTitle = this.i18nService.t("noApplicationsInOrgTitle", this.organizationName);
|
||||
this.emptyStateDescription = this.i18nService.t("noApplicationsInOrgDescription");
|
||||
this.emptyStateButtonText = this.i18nService.t("importData");
|
||||
this.emptyStateButtonIcon = RiskInsightsComponent.IMPORT_ICON;
|
||||
this.emptyStateButtonAction = this.goToImportPage;
|
||||
} else {
|
||||
this.emptyStateTitle = this.i18nService.t("noReportRunTitle");
|
||||
this.emptyStateDescription = this.i18nService.t("noReportRunDescription");
|
||||
this.emptyStateButtonText = this.i18nService.t("riskInsightsRunReport");
|
||||
this.emptyStateButtonIcon = "";
|
||||
this.emptyStateButtonAction = this.generateReport.bind(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user