From 7f1cfef1b0782f9b7540632690337f9de77eda13 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Thu, 12 Dec 2024 13:25:56 -0800 Subject: [PATCH] wire up children to risk insight data service --- .../services/risk-insights-data.service.ts | 77 +++++++++++-------- .../all-applications.component.html | 8 +- .../all-applications.component.ts | 55 +++++++------ .../risk-insights.component.html | 10 +-- .../risk-insights.component.ts | 32 ++++---- 5 files changed, 102 insertions(+), 80 deletions(-) diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts index 3a0ff4176eb..3992794e55b 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -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>(); + private applicationsSubject = new BehaviorSubject(null); + + applications$ = this.applicationsSubject.asObservable(); + + private isLoadingSubject = new BehaviorSubject(false); + isLoading$ = this.isLoadingSubject.asObservable(); + + private isRefreshingSubject = new BehaviorSubject(false); + isRefreshing$ = this.isRefreshingSubject.asObservable(); + + private errorSubject = new BehaviorSubject(null); + error$ = this.errorSubject.asObservable(); + + private dataLastUpdatedSubject = new BehaviorSubject(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 { - // 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); } } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index a1798e2ba9a..ea1a4f9db31 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -1,11 +1,11 @@ -
+
-
+

- {{ "noAppsInOrgTitle" | i18n: organization.name }} + {{ "noAppsInOrgTitle" | i18n: organization?.name }}

@@ -23,7 +23,7 @@
-
+

{{ "allApplications" | i18n }}

(); protected selectedIds: Set = new Set(); 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; 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 diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index efee6207027..dd7aa59ecb8 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,4 +1,4 @@ -
+
{{ "loading" | i18n }}
- +
{{ "accessIntelligence" | i18n }}

{{ "riskInsights" | i18n }}

@@ -21,11 +21,11 @@ aria-hidden="true" > {{ - "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") + "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 738ddab577e..eb24fc9902b 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -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; + isRefreshing$: Observable; + dataLastUpdated$: Observable; 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); } }