1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

add risk insight data service and wire up components to it

This commit is contained in:
jaasen-livefront
2024-12-11 16:56:07 -08:00
parent 82e002a0e4
commit be310e885c
5 changed files with 185 additions and 81 deletions

View File

@@ -59,7 +59,6 @@ export class OrganizationLayoutComponent implements OnInit {
showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false;
enterpriseOrganization$: Observable<boolean>;
constructor(

View File

@@ -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";

View File

@@ -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<string, Observable<ApplicationHealthReportDetail[]>>();
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<ApplicationHealthReportDetail[]> {
// 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);
}
}
}

View File

@@ -1,49 +1,66 @@
<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">
{{ "reviewAtRiskPasswords" | i18n }}
&nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
<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-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
<i class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted" aria-hidden="true"></i>
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}</span>
<a
bitButton
buttonType="unstyled"
class="tw-border-none !tw-font-normal tw-cursor-pointer"
[bitAction]="refreshData.bind(this)"
<ng-container *ngIf="!loading">
<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">
{{ "reviewAtRiskPasswords" | i18n }}
&nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
<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"
>
{{ "refresh" | i18n }}
</a>
</div>
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications>
</bit-tab>
<bit-tab *ngIf="isCritialAppsFeatureEnabled">
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}
</ng-template>
<tools-critical-applications></tools-critical-applications>
</bit-tab>
<bit-tab label="Raw Data">
<tools-password-health></tools-password-health>
</bit-tab>
<bit-tab label="Raw Data + members">
<tools-password-health-members></tools-password-health-members>
</bit-tab>
<bit-tab label="Raw Data + uri">
<tools-password-health-members-uri></tools-password-health-members-uri>
</bit-tab>
<!-- <bit-tab>
<ng-template bitTabLabel>
<i class="bwi bwi-envelope"></i>
{{ "notifiedMembersWithCount" | i18n: priorityApps.length }}
</ng-template>
<h2 bitTypography="h2">{{ "notifiedMembers" | i18n }}</h2>
<tools-notified-members-table></tools-notified-members-table>
</bit-tab> -->
</bit-tab-group>
<i
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted"
aria-hidden="true"
></i>
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}</span>
<span class="tw-flex tw-justify-center tw-w-16">
<a
*ngIf="!refetching"
bitButton
buttonType="unstyled"
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
[bitAction]="refreshData.bind(this)"
>
{{ "refresh" | i18n }}
</a>
<span>
<i
*ngIf="refetching"
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
aria-hidden="true"
></i>
</span>
</span>
</div>
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
<tools-all-applications></tools-all-applications>
</bit-tab>
<bit-tab *ngIf="isCriticalAppsFeatureEnabled">
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }}
</ng-template>
<tools-critical-applications></tools-critical-applications>
</bit-tab>
<bit-tab label="Raw Data">
<tools-password-health></tools-password-health>
</bit-tab>
<bit-tab label="Raw Data + members">
<tools-password-health-members></tools-password-health-members>
</bit-tab>
<bit-tab label="Raw Data + uri">
<tools-password-health-members-uri></tools-password-health-members-uri>
</bit-tab>
</bit-tab-group>
</ng-container>

View File

@@ -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<void> {
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;
});
}
}