mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-31 22:51:28 +01:00
[PM-15498] - add risk insight data service (#12361)
* Adding more test cases * Removing unnecessary file * Test cases update * Adding the fixme for strict types * moving the fixme * add risk insight data service and wire up components to it * hook up all applications to risk insights report service. add loading state * link up remaining props to risk insight report * wire up children to risk insight data service * add missing copy. remove loading state from risk insights * fix types * fix DI issue * remove @Injectable from RiskInsightsDataService --------- Co-authored-by: Tom <ttalty@bitwarden.com> Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
parent
fc37d6df6b
commit
da9e12dae6
@ -57,7 +57,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
showPaymentAndHistory$: Observable<boolean>;
|
||||
hideNewOrgButton$: Observable<boolean>;
|
||||
organizationIsUnmanaged$: Observable<boolean>;
|
||||
isAccessIntelligenceFeatureEnabled = false;
|
||||
enterpriseOrganization$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
|
@ -3882,6 +3882,9 @@
|
||||
"updateBrowser": {
|
||||
"message": "Update browser"
|
||||
},
|
||||
"generatingRiskInsights": {
|
||||
"message": "Generating your risk insights..."
|
||||
},
|
||||
"updateBrowserDesc": {
|
||||
"message": "You are using an unsupported web browser. The web vault may not function properly."
|
||||
},
|
||||
|
@ -25,7 +25,7 @@ export type ApplicationHealthReportDetail = {
|
||||
passwordCount: number;
|
||||
atRiskPasswordCount: number;
|
||||
memberCount: number;
|
||||
|
||||
atRiskMemberCount: number;
|
||||
memberDetails: MemberDetailsFlat[];
|
||||
atRiskMemberDetails: MemberDetailsFlat[];
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -0,0 +1,60 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { finalize } from "rxjs/operators";
|
||||
|
||||
import { ApplicationHealthReportDetail } from "../models/password-health";
|
||||
|
||||
import { RiskInsightsReportService } from "./risk-insights-report.service";
|
||||
|
||||
export class RiskInsightsDataService {
|
||||
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) {}
|
||||
|
||||
/**
|
||||
* Fetches the applications report and updates the applicationsSubject.
|
||||
* @param organizationId The ID of the organization.
|
||||
*/
|
||||
fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void {
|
||||
if (isRefresh) {
|
||||
this.isRefreshingSubject.next(true);
|
||||
} else {
|
||||
this.isLoadingSubject.next(true);
|
||||
}
|
||||
this.reportService
|
||||
.generateApplicationsReport$(organizationId)
|
||||
.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([]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
refreshApplicationsReport(organizationId: string): void {
|
||||
this.fetchApplicationsReport(organizationId, true);
|
||||
}
|
||||
}
|
@ -287,6 +287,7 @@ export class RiskInsightsReportService {
|
||||
: newUriDetail.cipherMembers,
|
||||
atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [],
|
||||
atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0,
|
||||
atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0,
|
||||
} as ApplicationHealthReportDetail;
|
||||
|
||||
if (isAtRisk) {
|
||||
|
@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/services";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@ -28,6 +29,10 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
MemberCipherDetailsApiService,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: RiskInsightsDataService,
|
||||
deps: [RiskInsightsReportService],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AccessIntelligenceModule {}
|
||||
|
@ -1,16 +1,11 @@
|
||||
<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 *ngIf="isLoading$ | async">
|
||||
<tools-risk-insights-loading></tools-risk-insights-loading>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!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">
|
||||
@ -28,21 +23,21 @@
|
||||
</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
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="mockAtRiskMembersCount"
|
||||
[maxValue]="mockTotalMembersCount"
|
||||
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||
[maxValue]="applicationSummary.totalMemberCount"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="mockAtRiskAppsCount"
|
||||
[maxValue]="mockTotalAppsCount"
|
||||
[value]="applicationSummary.totalAtRiskApplicationCount"
|
||||
[maxValue]="applicationSummary.totalApplicationCount"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
@ -57,7 +52,7 @@
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
*ngIf="isCritialAppsFeatureEnabled"
|
||||
*ngIf="isCriticalAppsFeatureEnabled"
|
||||
[disabled]="!selectedIds.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
@ -69,17 +64,17 @@
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th *ngIf="isCritialAppsFeatureEnabled"></th>
|
||||
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
|
||||
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
||||
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
|
||||
<th *ngIf="isCriticalAppsFeatureEnabled"></th>
|
||||
<th bitSortable="applicationName" bitCell>{{ "application" | i18n }}</th>
|
||||
<th bitSortable="atRiskPasswordCount" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||
<th bitSortable="passwordCount" bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||
<th bitSortable="atRiskMemberCount" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
||||
<th bitSortable="memberCount" bitCell>{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||
<td *ngIf="isCritialAppsFeatureEnabled">
|
||||
<td *ngIf="isCriticalAppsFeatureEnabled">
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
@ -88,25 +83,25 @@
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ r.name }}</span>
|
||||
<span>{{ r.applicationName }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskPasswords }}
|
||||
{{ r.atRiskPasswordCount }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.totalPasswords }}
|
||||
{{ r.passwordCount }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskMembers }}
|
||||
{{ r.atRiskMemberCount }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell data-testid="total-membership">
|
||||
{{ r.totalMembers }}
|
||||
{{ r.memberCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
@ -1,20 +1,23 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
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, firstValueFrom, map } from "rxjs";
|
||||
import { debounceTime, map, Observable, of, Subscription } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import {
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
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";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
@ -27,60 +30,76 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { applicationTableMockData } from "./application-table.mock";
|
||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-all-applications",
|
||||
templateUrl: "./all-applications.component.html",
|
||||
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
|
||||
imports: [
|
||||
ApplicationsLoadingComponent,
|
||||
HeaderModule,
|
||||
CardComponent,
|
||||
SearchModule,
|
||||
PipesModule,
|
||||
NoItemsModule,
|
||||
SharedModule,
|
||||
],
|
||||
})
|
||||
export class AllApplicationsComponent implements OnInit {
|
||||
protected dataSource = new TableDataSource<any>();
|
||||
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 = false;
|
||||
protected organization: Organization;
|
||||
protected loading = true;
|
||||
protected organization = {} as Organization;
|
||||
noItemsIcon = Icons.Security;
|
||||
protected markingAsCritical = false;
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
||||
private subscription = new Subscription();
|
||||
|
||||
// MOCK DATA
|
||||
protected mockData = applicationTableMockData;
|
||||
protected mockAtRiskMembersCount = 0;
|
||||
protected mockAtRiskAppsCount = 0;
|
||||
protected mockTotalMembersCount = 0;
|
||||
protected mockTotalAppsCount = 0;
|
||||
destroyRef = inject(DestroyRef);
|
||||
isLoading$: Observable<boolean> = of(false);
|
||||
isCriticalAppsFeatureEnabled = false;
|
||||
|
||||
async ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
|
||||
// TODO: use organizationId to fetch data
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CriticalApps,
|
||||
);
|
||||
|
||||
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(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected toastService: ToastService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
) {
|
||||
this.dataSource.data = applicationTableMockData;
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
@ -90,7 +109,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
// TODO: implement
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: null,
|
||||
title: "",
|
||||
message: "Not yet implemented",
|
||||
});
|
||||
};
|
||||
@ -103,7 +122,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
this.selectedIds.clear();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||
});
|
||||
resolve(true);
|
||||
@ -112,8 +131,8 @@ export class AllApplicationsComponent implements OnInit {
|
||||
});
|
||||
};
|
||||
|
||||
trackByFunction(_: number, item: CipherView) {
|
||||
return item.id;
|
||||
trackByFunction(_: number, item: ApplicationHealthReportDetail) {
|
||||
return item.applicationName;
|
||||
}
|
||||
|
||||
onCheckboxChange(id: number, event: Event) {
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="tw-flex-col tw-flex tw-justify-center tw-items-center tw-gap-5 tw-mt-4">
|
||||
<i
|
||||
class="bwi bwi-2x bwi-spinner bwi-spin text-primary"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<h2 bitTypography="h1">{{ "generatingRiskInsights" | i18n }}</h2>
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
|
||||
@Component({
|
||||
selector: "tools-risk-insights-loading",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule],
|
||||
templateUrl: "./risk-insights-loading.component.html",
|
||||
})
|
||||
export class ApplicationsLoadingComponent {
|
||||
constructor() {}
|
||||
}
|
@ -1,49 +1,58 @@
|
||||
<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 }}
|
||||
<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-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>
|
||||
<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 }}
|
||||
<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$ | async | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
<span class="tw-flex tw-justify-center tw-w-16">
|
||||
<a
|
||||
*ngIf="!(isRefreshing$ | async)"
|
||||
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="isRefreshing$ | async"
|
||||
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>
|
||||
|
@ -1,11 +1,13 @@
|
||||
// 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 { Observable, EMPTY } from "rxjs";
|
||||
import { map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { RiskInsightsDataService } 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";
|
||||
@ -43,45 +45,80 @@ export enum RiskInsightsTabType {
|
||||
],
|
||||
})
|
||||
export class RiskInsightsComponent implements OnInit {
|
||||
tabIndex: RiskInsightsTabType;
|
||||
dataLastUpdated = new Date();
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps;
|
||||
|
||||
apps: any[] = [];
|
||||
criticalApps: any[] = [];
|
||||
notifiedMembers: any[] = [];
|
||||
dataLastUpdated: Date = new Date();
|
||||
|
||||
async refreshData() {
|
||||
// TODO: Implement
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
this.dataLastUpdated = new Date();
|
||||
resolve(true);
|
||||
}, 1000),
|
||||
);
|
||||
isCriticalAppsFeatureEnabled: boolean = false;
|
||||
|
||||
appsCount: number = 0;
|
||||
criticalAppsCount: number = 0;
|
||||
notifiedMembersCount: number = 0;
|
||||
|
||||
private organizationId: string | null = null;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
isLoading$: Observable<boolean> = new Observable<boolean>();
|
||||
isRefreshing$: Observable<boolean> = new Observable<boolean>();
|
||||
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
|
||||
refetching: boolean = 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: string | null) => {
|
||||
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$;
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (applications: ApplicationHealthReportDetail[] | null) => {
|
||||
if (applications) {
|
||||
this.appsCount = applications.length;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.dataService.refreshApplicationsReport(this.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user