mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 06:23:38 +00:00
wire up children to risk insight data service
This commit is contained in:
@@ -1,53 +1,62 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { shareReplay } from "rxjs/operators";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { finalize } from "rxjs/operators";
|
||||
|
||||
import { ApplicationHealthReportDetail } from "../models/password-health";
|
||||
|
||||
import { RiskInsightsReportService } from "./risk-insights-report.service";
|
||||
|
||||
/**
|
||||
* Singleton service to manage the report details for the Risk Insights reports.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
@Injectable()
|
||||
export class RiskInsightsDataService {
|
||||
// Map to store observables per organizationId
|
||||
private applicationsReportMap = new Map<string, Observable<ApplicationHealthReportDetail[]>>();
|
||||
private applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(null);
|
||||
|
||||
applications$ = this.applicationsSubject.asObservable();
|
||||
|
||||
private isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||
isLoading$ = this.isLoadingSubject.asObservable();
|
||||
|
||||
private isRefreshingSubject = new BehaviorSubject<boolean>(false);
|
||||
isRefreshing$ = this.isRefreshingSubject.asObservable();
|
||||
|
||||
private errorSubject = new BehaviorSubject<string | null>(null);
|
||||
error$ = this.errorSubject.asObservable();
|
||||
|
||||
private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
|
||||
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();
|
||||
|
||||
constructor(private reportService: RiskInsightsReportService) {}
|
||||
|
||||
/**
|
||||
* Returns an observable for the applications report of a given organizationId.
|
||||
* Utilizes shareReplay to ensure that the data is fetched only once
|
||||
* and shared among multiple subscribers.
|
||||
* Fetches the applications report and updates the applicationsSubject.
|
||||
* @param organizationId The ID of the organization.
|
||||
* @returns Observable of ApplicationHealthReportDetail[].
|
||||
*/
|
||||
getApplicationsReport$(organizationId: string): Observable<ApplicationHealthReportDetail[]> {
|
||||
// If the observable for this organizationId already exists, return it
|
||||
if (this.applicationsReportMap.has(organizationId)) {
|
||||
return this.applicationsReportMap.get(organizationId)!;
|
||||
fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void {
|
||||
if (isRefresh) {
|
||||
this.isRefreshingSubject.next(true);
|
||||
} else {
|
||||
this.isLoadingSubject.next(true);
|
||||
}
|
||||
|
||||
const applicationsReport$ = this.reportService
|
||||
this.reportService
|
||||
.generateApplicationsReport$(organizationId)
|
||||
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
// Store the observable in the map for future subscribers
|
||||
this.applicationsReportMap.set(organizationId, applicationsReport$);
|
||||
|
||||
return applicationsReport$;
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.isLoadingSubject.next(false);
|
||||
this.isRefreshingSubject.next(false);
|
||||
this.dataLastUpdatedSubject.next(new Date());
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (reports: ApplicationHealthReportDetail[]) => {
|
||||
this.applicationsSubject.next(reports);
|
||||
this.errorSubject.next(null);
|
||||
},
|
||||
error: () => {
|
||||
this.applicationsSubject.next([]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached observable for a specific organizationId.
|
||||
* @param organizationId The ID of the organization.
|
||||
*/
|
||||
clearApplicationsReportCache(organizationId: string): void {
|
||||
if (this.applicationsReportMap.has(organizationId)) {
|
||||
this.applicationsReportMap.delete(organizationId);
|
||||
}
|
||||
refreshApplicationsReport(organizationId: string): void {
|
||||
this.fetchApplicationsReport(organizationId, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div *ngIf="loading">
|
||||
<div *ngIf="isLoading$ | async">
|
||||
<tools-risk-insights-loading></tools-risk-insights-loading>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!loading && !dataSource.data.length">
|
||||
<div class="tw-mt-4" *ngIf="!(isLoading$ | async) && !dataSource.data.length">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold mt-4">
|
||||
{{ "noAppsInOrgTitle" | i18n: organization.name }}
|
||||
{{ "noAppsInOrgTitle" | i18n: organization?.name }}
|
||||
</h2>
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
@@ -23,7 +23,7 @@
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!(isLoading$ | async) && dataSource.data.length">
|
||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { debounceTime, map, switchMap } from "rxjs";
|
||||
import { debounceTime, map, Observable, Subscription } from "rxjs";
|
||||
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ApplicationHealthReportDetail,
|
||||
ApplicationHealthReportSummary,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -45,19 +45,21 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
|
||||
NoItemsModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [MemberCipherDetailsApiService, RiskInsightsReportService],
|
||||
providers: [RiskInsightsReportService],
|
||||
})
|
||||
export class AllApplicationsComponent implements OnInit {
|
||||
export class AllApplicationsComponent implements OnInit, OnDestroy {
|
||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetail>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
private destroyRef = inject(DestroyRef);
|
||||
protected loading = true;
|
||||
protected organization: Organization;
|
||||
noItemsIcon = Icons.Security;
|
||||
protected markingAsCritical = false;
|
||||
protected applicationSummary: ApplicationHealthReportSummary;
|
||||
private subscription: Subscription;
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
isLoading$: Observable<boolean>;
|
||||
isCriticalAppsFeatureEnabled = false;
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -65,24 +67,28 @@ export class AllApplicationsComponent implements OnInit {
|
||||
FeatureFlag.CriticalApps,
|
||||
);
|
||||
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap((orgId) => {
|
||||
return this.dataService.getApplicationsReport$(orgId);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (applications: ApplicationHealthReportDetail[]) => {
|
||||
if (applications) {
|
||||
this.dataSource.data = applications;
|
||||
const summary = this.reportService.generateApplicationsSummary(applications);
|
||||
this.applicationSummary = summary;
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
||||
|
||||
if (organizationId) {
|
||||
this.organization = await this.organizationService.get(organizationId);
|
||||
this.subscription = this.dataService.applications$
|
||||
.pipe(
|
||||
map((applications) => {
|
||||
if (applications) {
|
||||
this.dataSource.data = applications;
|
||||
this.applicationSummary =
|
||||
this.reportService.generateApplicationsSummary(applications);
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe();
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -92,6 +98,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
protected toastService: ToastService,
|
||||
protected configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div *ngIf="loading">
|
||||
<div *ngIf="isLoading$ | async">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
@@ -6,7 +6,7 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<ng-container *ngIf="!loading">
|
||||
<ng-container *ngIf="!(isLoading$ | async)">
|
||||
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
|
||||
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
|
||||
<div class="tw-text-muted tw-max-w-4xl tw-mb-2">
|
||||
@@ -21,11 +21,11 @@
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
<span class="tw-flex tw-justify-center tw-w-16">
|
||||
<a
|
||||
*ngIf="!refetching"
|
||||
*ngIf="!(isRefreshing$ | async)"
|
||||
bitButton
|
||||
buttonType="unstyled"
|
||||
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
|
||||
@@ -35,7 +35,7 @@
|
||||
</a>
|
||||
<span>
|
||||
<i
|
||||
*ngIf="refetching"
|
||||
*ngIf="isRefreshing$ | async"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Observable } from "rxjs";
|
||||
import { map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -61,7 +62,9 @@ export class RiskInsightsComponent implements OnInit {
|
||||
|
||||
private organizationId: string;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
loading = true;
|
||||
isLoading$: Observable<boolean>;
|
||||
isRefreshing$: Observable<boolean>;
|
||||
dataLastUpdated$: Observable<Date>;
|
||||
refetching = false;
|
||||
|
||||
constructor(
|
||||
@@ -85,29 +88,32 @@ export class RiskInsightsComponent implements OnInit {
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap((orgId) => {
|
||||
this.organizationId = orgId;
|
||||
return this.dataService.getApplicationsReport$(orgId);
|
||||
if (orgId) {
|
||||
this.organizationId = orgId;
|
||||
this.dataService.fetchApplicationsReport(orgId);
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
this.isRefreshing$ = this.dataService.isRefreshing$;
|
||||
this.dataLastUpdated$ = this.dataService.dataLastUpdated$;
|
||||
return this.dataService.applications$;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (applications: ApplicationHealthReportDetail[]) => {
|
||||
next: (applications: ApplicationHealthReportDetail[] | null) => {
|
||||
if (applications) {
|
||||
this.appsCount = applications.length;
|
||||
this.loading = false;
|
||||
this.refetching = false;
|
||||
this.dataLastUpdated = new Date();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async refreshData() {
|
||||
/**
|
||||
* Refreshes the data by re-fetching the applications report.
|
||||
* This will automatically notify child components subscribed to the RiskInsightsDataService observables.
|
||||
*/
|
||||
refreshData(): void {
|
||||
if (this.organizationId) {
|
||||
this.refetching = true;
|
||||
// Clear the cache to ensure fresh data is fetched
|
||||
this.dataService.clearApplicationsReportCache(this.organizationId);
|
||||
// Re-initialize to fetch data again
|
||||
await this.ngOnInit();
|
||||
this.dataService.refreshApplicationsReport(this.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user