mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
PM-17065 Display critical apps (#12867)
This commit is contained in:
@@ -57,10 +57,15 @@ export class AllApplicationsComponent implements OnInit {
|
|||||||
protected selectedUrls: Set<string> = new Set<string>();
|
protected selectedUrls: Set<string> = new Set<string>();
|
||||||
protected searchControl = new FormControl("", { nonNullable: true });
|
protected searchControl = new FormControl("", { nonNullable: true });
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
protected organization = {} as Organization;
|
protected organization = new Organization();
|
||||||
noItemsIcon = Icons.Security;
|
noItemsIcon = Icons.Security;
|
||||||
protected markingAsCritical = false;
|
protected markingAsCritical = false;
|
||||||
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
protected applicationSummary: ApplicationHealthReportSummary = {
|
||||||
|
totalMemberCount: 0,
|
||||||
|
totalAtRiskMemberCount: 0,
|
||||||
|
totalApplicationCount: 0,
|
||||||
|
totalAtRiskApplicationCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
destroyRef = inject(DestroyRef);
|
destroyRef = inject(DestroyRef);
|
||||||
isLoading$: Observable<boolean> = of(false);
|
isLoading$: Observable<boolean> = of(false);
|
||||||
@@ -90,8 +95,10 @@ export class AllApplicationsComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe(({ data, organization }) => {
|
.subscribe(({ data, organization }) => {
|
||||||
this.dataSource.data = data ?? [];
|
if (data) {
|
||||||
this.applicationSummary = this.reportService.generateApplicationsSummary(data ?? []);
|
this.dataSource.data = data;
|
||||||
|
this.applicationSummary = this.reportService.generateApplicationsSummary(data);
|
||||||
|
}
|
||||||
if (organization) {
|
if (organization) {
|
||||||
this.organization = organization;
|
this.organization = organization;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
<ng-container bitDialogContent>
|
<ng-container bitDialogContent>
|
||||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||||
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: members.length }} </span>
|
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: members.length }} </span>
|
||||||
<span class="tw-text-muted">{{
|
<span>{{ "atRiskMembersDescriptionWithApp" | i18n: applicationName }}</span>
|
||||||
"atRiskMembersDescriptionWithApp" | i18n: applicationName
|
|
||||||
}}</span>
|
|
||||||
<div class="tw-mt-1">
|
<div class="tw-mt-1">
|
||||||
<ng-container *ngFor="let member of members">
|
<ng-container *ngFor="let member of members">
|
||||||
<div>{{ member.email }}</div>
|
<div>{{ member.email }}</div>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
export const applicationTableMockData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "google.com",
|
|
||||||
atRiskPasswords: 4,
|
|
||||||
totalPasswords: 10,
|
|
||||||
atRiskMembers: 2,
|
|
||||||
totalMembers: 5,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "facebook.com",
|
|
||||||
atRiskPasswords: 3,
|
|
||||||
totalPasswords: 8,
|
|
||||||
atRiskMembers: 1,
|
|
||||||
totalMembers: 3,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "twitter.com",
|
|
||||||
atRiskPasswords: 2,
|
|
||||||
totalPasswords: 6,
|
|
||||||
atRiskMembers: 0,
|
|
||||||
totalMembers: 2,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "linkedin.com",
|
|
||||||
atRiskPasswords: 1,
|
|
||||||
totalPasswords: 4,
|
|
||||||
atRiskMembers: 0,
|
|
||||||
totalMembers: 1,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "instagram.com",
|
|
||||||
atRiskPasswords: 0,
|
|
||||||
totalPasswords: 2,
|
|
||||||
atRiskMembers: 0,
|
|
||||||
totalMembers: 0,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: "tiktok.com",
|
|
||||||
atRiskPasswords: 0,
|
|
||||||
totalPasswords: 1,
|
|
||||||
atRiskMembers: 0,
|
|
||||||
totalMembers: 0,
|
|
||||||
isMarkedAsCritical: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -35,17 +35,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tw-flex tw-gap-6">
|
<div class="tw-flex tw-gap-6">
|
||||||
<tools-card
|
<tools-card
|
||||||
class="tw-flex-1"
|
class="tw-flex-1 tw-cursor-pointer"
|
||||||
[title]="'atRiskMembers' | i18n"
|
[title]="'atRiskMembers' | i18n"
|
||||||
[value]="mockAtRiskMembersCount"
|
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||||
[maxValue]="mockTotalMembersCount"
|
[maxValue]="applicationSummary.totalMemberCount"
|
||||||
|
(click)="showOrgAtRiskMembers()"
|
||||||
>
|
>
|
||||||
</tools-card>
|
</tools-card>
|
||||||
<tools-card
|
<tools-card
|
||||||
class="tw-flex-1"
|
class="tw-flex-1"
|
||||||
[title]="'atRiskApplications' | i18n"
|
[title]="'atRiskApplications' | i18n"
|
||||||
[value]="mockAtRiskAppsCount"
|
[value]="applicationSummary.totalAtRiskApplicationCount"
|
||||||
[maxValue]="mockTotalAppsCount"
|
[maxValue]="applicationSummary.totalApplicationCount"
|
||||||
|
(click)="showOrgAtRiskApps()"
|
||||||
>
|
>
|
||||||
</tools-card>
|
</tools-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,38 +62,38 @@
|
|||||||
<ng-container header>
|
<ng-container header>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
|
<th bitSortable="applicationName" bitCell>{{ "application" | i18n }}</th>
|
||||||
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
<th bitSortable="atRiskPasswordCount" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||||
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
|
<th bitSortable="passwordCount" bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||||
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
<th bitSortable="atRiskMemberCount" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
||||||
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
|
<th bitSortable="memberCount" bitCell>{{ "totalMembers" | i18n }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template body let-rows$>
|
<ng-template body let-rows$>
|
||||||
<tr bitRow *ngFor="let r of rows$ | async">
|
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||||
<td>
|
<td>
|
||||||
<i class="bwi bwi-star-f"></i>
|
<i class="bwi bwi-star-f" *ngIf="r.isMarkedAsCritical"></i>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
||||||
<span>{{ r.name }}</span>
|
<span>{{ r.applicationName }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td bitCell>
|
||||||
<span>
|
<span>
|
||||||
{{ r.atRiskPasswords }}
|
{{ r.atRiskPasswordCount }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td bitCell>
|
||||||
<span>
|
<span>
|
||||||
{{ r.totalPasswords }}
|
{{ r.passwordCount }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td bitCell>
|
||||||
<span>
|
<span>
|
||||||
{{ r.atRiskMembers }}
|
{{ r.atRiskMemberCount }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell data-testid="total-membership">
|
<td bitCell data-testid="total-membership">
|
||||||
{{ r.totalMembers }}
|
{{ r.memberCount }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -4,16 +4,32 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
|||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormControl } from "@angular/forms";
|
import { FormControl } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { debounceTime, map } from "rxjs";
|
import { combineLatest, debounceTime, map } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import {
|
||||||
import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components";
|
CriticalAppsService,
|
||||||
|
RiskInsightsDataService,
|
||||||
|
RiskInsightsReportService,
|
||||||
|
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||||
|
import {
|
||||||
|
ApplicationHealthReportDetailWithCriticalFlag,
|
||||||
|
ApplicationHealthReportSummary,
|
||||||
|
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
|
import {
|
||||||
|
DialogService,
|
||||||
|
Icons,
|
||||||
|
NoItemsModule,
|
||||||
|
SearchModule,
|
||||||
|
TableDataSource,
|
||||||
|
} from "@bitwarden/components";
|
||||||
import { CardComponent } from "@bitwarden/tools-card";
|
import { CardComponent } from "@bitwarden/tools-card";
|
||||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
||||||
|
|
||||||
import { applicationTableMockData } from "./application-table.mock";
|
import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component";
|
||||||
|
import { OrgAtRiskAppsDialogComponent } from "./org-at-risk-apps-dialog.component";
|
||||||
|
import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component";
|
||||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,30 +39,38 @@ import { RiskInsightsTabType } from "./risk-insights.component";
|
|||||||
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
|
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
|
||||||
})
|
})
|
||||||
export class CriticalApplicationsComponent implements OnInit {
|
export class CriticalApplicationsComponent implements OnInit {
|
||||||
protected dataSource = new TableDataSource<any>();
|
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||||
protected selectedIds: Set<number> = new Set<number>();
|
protected selectedIds: Set<number> = new Set<number>();
|
||||||
protected searchControl = new FormControl("", { nonNullable: true });
|
protected searchControl = new FormControl("", { nonNullable: true });
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
protected loading = false;
|
protected loading = false;
|
||||||
protected organizationId: string;
|
protected organizationId: string;
|
||||||
|
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
||||||
noItemsIcon = Icons.Security;
|
noItemsIcon = Icons.Security;
|
||||||
// MOCK DATA
|
|
||||||
protected mockData = applicationTableMockData;
|
|
||||||
protected mockAtRiskMembersCount = 0;
|
|
||||||
protected mockAtRiskAppsCount = 0;
|
|
||||||
protected mockTotalMembersCount = 0;
|
|
||||||
protected mockTotalAppsCount = 0;
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.activatedRoute.paramMap
|
this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
|
||||||
|
combineLatest([
|
||||||
|
this.dataService.applications$,
|
||||||
|
this.criticalAppsService.getAppsListForOrg(this.organizationId),
|
||||||
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
map(async (params) => {
|
map(([applications, criticalApps]) => {
|
||||||
this.organizationId = params.get("organizationId");
|
const criticalUrls = criticalApps.map((ca) => ca.uri);
|
||||||
// TODO: use organizationId to fetch data
|
const data = applications?.map((app) => ({
|
||||||
|
...app,
|
||||||
|
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
|
||||||
|
})) as ApplicationHealthReportDetailWithCriticalFlag[];
|
||||||
|
return data?.filter((app) => app.isMarkedAsCritical);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe((applications) => {
|
||||||
|
if (applications) {
|
||||||
|
this.dataSource.data = applications;
|
||||||
|
this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
goToAllAppsTab = async () => {
|
goToAllAppsTab = async () => {
|
||||||
@@ -57,13 +81,40 @@ export class CriticalApplicationsComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected i18nService: I18nService,
|
|
||||||
protected activatedRoute: ActivatedRoute,
|
protected activatedRoute: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected dataService: RiskInsightsDataService,
|
||||||
|
protected criticalAppsService: CriticalAppsService,
|
||||||
|
protected reportService: RiskInsightsReportService,
|
||||||
|
protected dialogService: DialogService,
|
||||||
) {
|
) {
|
||||||
this.dataSource.data = []; //applicationTableMockData;
|
|
||||||
this.searchControl.valueChanges
|
this.searchControl.valueChanges
|
||||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||||
.subscribe((v) => (this.dataSource.filter = v));
|
.subscribe((v) => (this.dataSource.filter = v));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showAppAtRiskMembers = async (applicationName: string) => {
|
||||||
|
openAppAtRiskMembersDialog(this.dialogService, {
|
||||||
|
members:
|
||||||
|
this.dataSource.data.find((app) => app.applicationName === applicationName)
|
||||||
|
?.atRiskMemberDetails ?? [],
|
||||||
|
applicationName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
showOrgAtRiskMembers = async () => {
|
||||||
|
this.dialogService.open(OrgAtRiskMembersDialogComponent, {
|
||||||
|
data: this.reportService.generateAtRiskMemberList(this.dataSource.data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
showOrgAtRiskApps = async () => {
|
||||||
|
this.dialogService.open(OrgAtRiskAppsDialogComponent, {
|
||||||
|
data: this.reportService.generateAtRiskApplicationList(this.dataSource.data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) {
|
||||||
|
return item.applicationName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user