From be310e885c13ca6e0cb23ec4cdae376355890963 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Wed, 11 Dec 2024 16:56:07 -0800 Subject: [PATCH] add risk insight data service and wire up components to it --- .../layouts/organization-layout.component.ts | 1 - .../reports/risk-insights/services/index.ts | 1 + .../services/risk-insights-data.service.ts | 53 +++++++++ .../risk-insights.component.html | 111 ++++++++++-------- .../risk-insights.component.ts | 100 ++++++++++------ 5 files changed, 185 insertions(+), 81 deletions(-) create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 6ead83b01d8..4f9fa1837c8 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -59,7 +59,6 @@ export class OrganizationLayoutComponent implements OnInit { showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; - isAccessIntelligenceFeatureEnabled = false; enterpriseOrganization$: Observable; constructor( diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts index e930c7666e8..a8e62437b9d 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts @@ -1,3 +1,4 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; export * from "./risk-insights-report.service"; +export * from "./risk-insights-data.service"; 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 new file mode 100644 index 00000000000..3a0ff4176eb --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { shareReplay } 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", +}) +export class RiskInsightsDataService { + // Map to store observables per organizationId + private applicationsReportMap = new Map>(); + + 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. + * @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)!; + } + + const applicationsReport$ = 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$; + } + + /** + * 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); + } + } +} 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 6df47e3c46f..efee6207027 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,49 +1,66 @@ -
{{ "accessIntelligence" | i18n }}
-

{{ "riskInsights" | i18n }}

-
- {{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }} +
+ + {{ "loading" | i18n }}
-
- - {{ - "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") - }} - +
{{ "accessIntelligence" | i18n }}
+

{{ "riskInsights" | i18n }}

+
+
- {{ "refresh" | i18n }} - -
- - - - - - - - {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} - - - - - - - - - - - - - - + + {{ + "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") + }} + + + {{ "refresh" | i18n }} + + + + + +
+ + + + + + + + {{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} + + + + + + + + + + + + + + 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 5ea39bd0513..738ddab577e 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 @@ -1,11 +1,16 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; +import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + MemberCipherDetailsApiService, + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; @@ -41,47 +46,76 @@ export enum RiskInsightsTabType { NotifiedMembersTableComponent, TabsModule, ], + providers: [RiskInsightsReportService, RiskInsightsDataService, MemberCipherDetailsApiService], }) export class RiskInsightsComponent implements OnInit { - tabIndex: RiskInsightsTabType; + tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps; + dataLastUpdated = new Date(); - isCritialAppsFeatureEnabled = false; - apps: any[] = []; - criticalApps: any[] = []; - notifiedMembers: any[] = []; + isCriticalAppsFeatureEnabled = false; - async refreshData() { - // TODO: Implement - return new Promise((resolve) => - setTimeout(() => { - this.dataLastUpdated = new Date(); - resolve(true); - }, 1000), - ); + appsCount = 0; + criticalAppsCount = 0; + notifiedMembersCount = 0; + + private organizationId: string; + private destroyRef = inject(DestroyRef); + loading = true; + refetching = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private configService: ConfigService, + private dataService: RiskInsightsDataService, + ) { + this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { + this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; + }); } - onTabChange = async (newIndex: number) => { + async ngOnInit() { + this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.CriticalApps, + ); + + this.route.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((orgId) => { + this.organizationId = orgId; + return this.dataService.getApplicationsReport$(orgId); + }), + ) + .subscribe({ + next: (applications: ApplicationHealthReportDetail[]) => { + if (applications) { + this.appsCount = applications.length; + this.loading = false; + this.refetching = false; + this.dataLastUpdated = new Date(); + } + }, + }); + } + + async refreshData() { + 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(); + } + } + + async onTabChange(newIndex: number): Promise { await this.router.navigate([], { relativeTo: this.route, queryParams: { tabIndex: newIndex }, queryParamsHandling: "merge", }); - }; - - async ngOnInit() { - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( - FeatureFlag.CriticalApps, - ); - } - - constructor( - protected route: ActivatedRoute, - private router: Router, - private configService: ConfigService, - ) { - route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { - this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps; - }); } }