mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-14 20:01:31 +01:00
[PM-16104] [PM-15929] Org at risk members click on the card (#12732)
* Org at risk members click on the card * Fixing at risk member counts * At risk member text modification * Changing ok button to close
This commit is contained in:
parent
8062475044
commit
52b6bfea1d
@ -113,6 +113,27 @@
|
|||||||
"atRiskMembers": {
|
"atRiskMembers": {
|
||||||
"message": "At-risk members"
|
"message": "At-risk members"
|
||||||
},
|
},
|
||||||
|
"atRiskMembersWithCount": {
|
||||||
|
"message": "At-risk members ($COUNT$)",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"atRiskMembersDescription": {
|
||||||
|
"message": "These members are logging into applications with weak, exposed, or reused passwords."
|
||||||
|
},
|
||||||
|
"atRiskMembersDescriptionWithApp": {
|
||||||
|
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
|
||||||
|
"placeholders": {
|
||||||
|
"appname": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Salesforce"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"totalMembers": {
|
"totalMembers": {
|
||||||
"message": "Total members"
|
"message": "Total members"
|
||||||
},
|
},
|
||||||
|
@ -90,3 +90,13 @@ export type MemberDetailsFlat = {
|
|||||||
email: string;
|
email: string;
|
||||||
cipherId: string;
|
cipherId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member email with the number of at risk passwords
|
||||||
|
* At risk member detail that contains the email
|
||||||
|
* and the count of at risk ciphers
|
||||||
|
*/
|
||||||
|
export type AtRiskMemberDetail = {
|
||||||
|
email: string;
|
||||||
|
atRiskPasswordCount: number;
|
||||||
|
};
|
||||||
|
@ -12,6 +12,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import {
|
import {
|
||||||
ApplicationHealthReportDetail,
|
ApplicationHealthReportDetail,
|
||||||
ApplicationHealthReportSummary,
|
ApplicationHealthReportSummary,
|
||||||
|
AtRiskMemberDetail,
|
||||||
CipherHealthReportDetail,
|
CipherHealthReportDetail,
|
||||||
CipherHealthReportUriDetail,
|
CipherHealthReportUriDetail,
|
||||||
ExposedPasswordDetail,
|
ExposedPasswordDetail,
|
||||||
@ -89,6 +90,30 @@ export class RiskInsightsReportService {
|
|||||||
return results$;
|
return results$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
|
||||||
|
*/
|
||||||
|
generateAtRiskMemberList(
|
||||||
|
cipherHealthReportDetails: ApplicationHealthReportDetail[],
|
||||||
|
): AtRiskMemberDetail[] {
|
||||||
|
const memberRiskMap = new Map<string, number>();
|
||||||
|
|
||||||
|
cipherHealthReportDetails.forEach((app) => {
|
||||||
|
app.atRiskMemberDetails.forEach((member) => {
|
||||||
|
if (memberRiskMap.has(member.email)) {
|
||||||
|
memberRiskMap.set(member.email, memberRiskMap.get(member.email) + 1);
|
||||||
|
} else {
|
||||||
|
memberRiskMap.set(member.email, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
|
||||||
|
email,
|
||||||
|
atRiskPasswordCount,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the summary from the application health report. Returns total members and applications as well
|
* Gets the summary from the application health report. Returns total members and applications as well
|
||||||
* as the total at risk members and at risk applications
|
* as the total at risk members and at risk applications
|
||||||
|
@ -27,10 +27,11 @@
|
|||||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||||
<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]="applicationSummary.totalAtRiskMemberCount"
|
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||||
[maxValue]="applicationSummary.totalMemberCount"
|
[maxValue]="applicationSummary.totalMemberCount"
|
||||||
|
(click)="showOrgAtRiskMembers()"
|
||||||
>
|
>
|
||||||
</tools-card>
|
</tools-card>
|
||||||
<tools-card
|
<tools-card
|
||||||
@ -82,7 +83,7 @@
|
|||||||
(change)="onCheckboxChange(r.id, $event)"
|
(change)="onCheckboxChange(r.id, $event)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
||||||
<span>{{ r.applicationName }}</span>
|
<span>{{ r.applicationName }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td bitCell>
|
||||||
|
@ -19,6 +19,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import {
|
import {
|
||||||
|
DialogService,
|
||||||
Icons,
|
Icons,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
@ -30,6 +31,8 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
|
|||||||
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 { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component";
|
||||||
|
import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component";
|
||||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -99,6 +102,7 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
protected dataService: RiskInsightsDataService,
|
protected dataService: RiskInsightsDataService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
protected reportService: RiskInsightsReportService,
|
protected reportService: RiskInsightsReportService,
|
||||||
|
protected dialogService: DialogService,
|
||||||
) {
|
) {
|
||||||
this.searchControl.valueChanges
|
this.searchControl.valueChanges
|
||||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||||
@ -135,6 +139,21 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
return item.applicationName;
|
return item.applicationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onCheckboxChange(id: number, event: Event) {
|
onCheckboxChange(id: number, event: Event) {
|
||||||
const isChecked = (event.target as HTMLInputElement).checked;
|
const isChecked = (event.target as HTMLInputElement).checked;
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<bit-dialog>
|
||||||
|
<span bitDialogTitle>{{ applicationName }}</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||||
|
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: members.length }} </span>
|
||||||
|
<span class="tw-text-muted">{{
|
||||||
|
"atRiskMembersDescriptionWithApp" | i18n: applicationName
|
||||||
|
}}</span>
|
||||||
|
<div class="tw-mt-1">
|
||||||
|
<ng-container *ngFor="let member of members">
|
||||||
|
<div>{{ member.email }}</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
|
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
type AppAtRiskMembersDialogParams = {
|
||||||
|
members: MemberDetailsFlat[];
|
||||||
|
applicationName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openAppAtRiskMembersDialog = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
dialogConfig: AppAtRiskMembersDialogParams,
|
||||||
|
) =>
|
||||||
|
dialogService.open<boolean, AppAtRiskMembersDialogParams>(AppAtRiskMembersDialogComponent, {
|
||||||
|
data: dialogConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./app-at-risk-members-dialog.component.html",
|
||||||
|
imports: [ButtonModule, CommonModule, JslibModule, DialogModule],
|
||||||
|
})
|
||||||
|
export class AppAtRiskMembersDialogComponent {
|
||||||
|
protected members: MemberDetailsFlat[];
|
||||||
|
protected applicationName: string;
|
||||||
|
|
||||||
|
constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) {
|
||||||
|
this.members = params.members;
|
||||||
|
this.applicationName = params.applicationName;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<bit-dialog>
|
||||||
|
<ng-container bitDialogTitle>
|
||||||
|
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} </span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||||
|
<span bitTypography="body2" class="tw-text-muted">{{
|
||||||
|
"atRiskMembersDescription" | i18n
|
||||||
|
}}</span>
|
||||||
|
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||||
|
<div bitTypography="body2" class="tw-font-bold">{{ "email" | i18n }}</div>
|
||||||
|
<div bitTypography="body2" class="tw-font-bold">{{ "atRiskPasswords" | i18n }}</div>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngFor="let member of atRiskMembers">
|
||||||
|
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||||
|
<div>{{ member.email }}</div>
|
||||||
|
<div>{{ member.atRiskPasswordCount }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,24 @@
|
|||||||
|
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
|
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export const openOrgAtRiskMembersDialog = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
dialogConfig: AtRiskMemberDetail[],
|
||||||
|
) =>
|
||||||
|
dialogService.open<boolean, AtRiskMemberDetail[]>(OrgAtRiskMembersDialogComponent, {
|
||||||
|
data: dialogConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: "./org-at-risk-members-dialog.component.html",
|
||||||
|
imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule],
|
||||||
|
})
|
||||||
|
export class OrgAtRiskMembersDialogComponent {
|
||||||
|
constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user