mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-25874] Activity Tab - At Risk Members Card (#16469)
* PM-25869 add a new tab called Activity in risk insights * PM-25869 fixed type errors * PM-25874 Activity card for At-risk member count
This commit is contained in:
@@ -59,6 +59,9 @@
|
|||||||
"createNewLoginItem": {
|
"createNewLoginItem": {
|
||||||
"message": "Create new login item"
|
"message": "Create new login item"
|
||||||
},
|
},
|
||||||
|
"criticalApplicationsActivityDescription": {
|
||||||
|
"message": "Once you mark applications critical, they will display here."
|
||||||
|
},
|
||||||
"criticalApplicationsWithCount": {
|
"criticalApplicationsWithCount": {
|
||||||
"message": "Critical applications ($COUNT$)",
|
"message": "Critical applications ($COUNT$)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -68,6 +71,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"countOfCriticalApplications": {
|
||||||
|
"message": "$COUNT$ critical applications",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"notifiedMembersWithCount": {
|
"notifiedMembersWithCount": {
|
||||||
"message": "Notified members ($COUNT$)",
|
"message": "Notified members ($COUNT$)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -122,6 +134,18 @@
|
|||||||
"atRiskMembers": {
|
"atRiskMembers": {
|
||||||
"message": "At-risk members"
|
"message": "At-risk members"
|
||||||
},
|
},
|
||||||
|
"membersAtRiskActivityDescription":{
|
||||||
|
"message": "Members with edit access to at-risk items for critical applications"
|
||||||
|
},
|
||||||
|
"membersAtRisk": {
|
||||||
|
"message": "$COUNT$ members at risk",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"atRiskMembersWithCount": {
|
"atRiskMembersWithCount": {
|
||||||
"message": "At-risk members ($COUNT$)",
|
"message": "At-risk members ($COUNT$)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="tw-flex-col">
|
||||||
|
<span bitTypography="h6" class="tw-flex tw-text-main">{{ title | i18n }}</span>
|
||||||
|
<div class="tw-flex tw-items-baseline tw-gap-2">
|
||||||
|
<span bitTypography="h3">{{ cardMetrics | i18n: value }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex tw-items-baseline tw-gap-2">
|
||||||
|
<span bitTypography="body2">{{ metricDescription | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "dirt-activity-card",
|
||||||
|
templateUrl: "./activity-card.component.html",
|
||||||
|
imports: [CommonModule, TypographyModule, JslibModule],
|
||||||
|
host: {
|
||||||
|
class:
|
||||||
|
"tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class ActivityCardComponent {
|
||||||
|
/**
|
||||||
|
* The title of the card goes here
|
||||||
|
*/
|
||||||
|
@Input() title: string = "";
|
||||||
|
/**
|
||||||
|
* The current value of the card as emphasized text
|
||||||
|
*/
|
||||||
|
@Input() value: number | null = null;
|
||||||
|
/**
|
||||||
|
* The card metrics text to display next to the value
|
||||||
|
*/
|
||||||
|
@Input() cardMetrics: string = "";
|
||||||
|
/**
|
||||||
|
* The description text to display below the value and metrics
|
||||||
|
*/
|
||||||
|
@Input() metricDescription: string = "";
|
||||||
|
}
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
<div *ngIf="isLoading$ | async">
|
@if (isLoading$ | async) {
|
||||||
<tools-risk-insights-loading></tools-risk-insights-loading>
|
<div *ngIf="isLoading$ | async">
|
||||||
</div>
|
<tools-risk-insights-loading></tools-risk-insights-loading>
|
||||||
<div class="tw-mt-4" *ngIf="!(isLoading$ | async) && !atRiskAppDetails?.length">
|
</div>
|
||||||
<bit-no-items class="tw-text-main">
|
}
|
||||||
<ng-container slot="title">
|
|
||||||
<h2 class="tw-font-semibold tw-mt-4">
|
@if (!(isLoading$ | async) && (noData$ | async)) {
|
||||||
{{ organization.name }}
|
<div class="tw-mt-4" *ngIf="">
|
||||||
</h2>
|
<bit-no-items class="tw-text-main">
|
||||||
</ng-container>
|
<ng-container slot="title">
|
||||||
<ng-container slot="description">
|
<h2 class="tw-font-semibold tw-mt-4">
|
||||||
<div class="tw-flex tw-flex-col tw-mb-2">
|
{{ organization?.name }}
|
||||||
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
</h2>
|
||||||
</div>
|
</ng-container>
|
||||||
</ng-container>
|
<ng-container slot="description">
|
||||||
</bit-no-items>
|
<div class="tw-flex tw-flex-col tw-mb-2">
|
||||||
</div>
|
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!(isLoading$ | async) && !(noData$ | async)) {
|
||||||
|
<div class="tw-mt-4 tw-flex tw-flex-col">
|
||||||
|
<div class="tw-flex tw-gap-4 tw-col-span-6">
|
||||||
|
<dirt-activity-card
|
||||||
|
class="tw-col-span-2 tw-cursor-pointer"
|
||||||
|
[title]="'atRiskMembers'"
|
||||||
|
[value]="atRiskMemberCount"
|
||||||
|
[cardMetrics]="'membersAtRisk'"
|
||||||
|
[metricDescription]="'membersAtRiskActivityDescription'"
|
||||||
|
>
|
||||||
|
</dirt-activity-card>
|
||||||
|
<dirt-activity-card
|
||||||
|
#allAppsOrgAtRiskApplications
|
||||||
|
class="tw-col-span-2 tw-cursor-pointer"
|
||||||
|
[title]="'criticalApplications'"
|
||||||
|
[value]="criticalApplicationsCount"
|
||||||
|
[cardMetrics]="'countOfCriticalApplications'"
|
||||||
|
[metricDescription]="'criticalApplicationsActivityDescription'"
|
||||||
|
>
|
||||||
|
</dirt-activity-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, of, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
import {
|
||||||
import { AtRiskApplicationDetail } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
|
CriticalAppsService,
|
||||||
|
RiskInsightsDataService,
|
||||||
|
RiskInsightsReportService,
|
||||||
|
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -11,17 +15,22 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { getById } from "@bitwarden/common/platform/misc";
|
import { getById } from "@bitwarden/common/platform/misc";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
|
import { ActivityCardComponent } from "./activity-card.component";
|
||||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "tools-all-activity",
|
selector: "tools-all-activity",
|
||||||
imports: [ApplicationsLoadingComponent, SharedModule],
|
imports: [ApplicationsLoadingComponent, SharedModule, ActivityCardComponent],
|
||||||
templateUrl: "./all-activity.component.html",
|
templateUrl: "./all-activity.component.html",
|
||||||
})
|
})
|
||||||
export class AllActivityComponent implements OnInit {
|
export class AllActivityComponent implements OnInit {
|
||||||
isLoading$: Observable<boolean> = this.dataService.isLoading$;
|
protected isLoading$ = this.dataService.isLoading$;
|
||||||
atRiskAppDetails: AtRiskApplicationDetail[] = [];
|
protected noData$ = new BehaviorSubject(true);
|
||||||
organization: Organization | null = null;
|
organization: Organization | null = null;
|
||||||
|
atRiskMemberCount = 0;
|
||||||
|
criticalApplicationsCount = 0;
|
||||||
|
|
||||||
|
destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
||||||
@@ -32,9 +41,20 @@ export class AllActivityComponent implements OnInit {
|
|||||||
(await firstValueFrom(
|
(await firstValueFrom(
|
||||||
this.organizationService.organizations$(userId).pipe(getById(organizationId)),
|
this.organizationService.organizations$(userId).pipe(getById(organizationId)),
|
||||||
)) ?? null;
|
)) ?? null;
|
||||||
|
|
||||||
this.atRiskAppDetails = this.dataService.atRiskAppDetails ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dataService.applications$
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
switchMap((apps) => {
|
||||||
|
const atRiskMembers = this.reportService.generateAtRiskMemberList(apps ?? []);
|
||||||
|
return of({ apps, atRiskMembers });
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe(({ apps, atRiskMembers }) => {
|
||||||
|
this.noData$.next((apps?.length ?? 0) === 0);
|
||||||
|
this.atRiskMemberCount = atRiskMembers?.length;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -42,5 +62,7 @@ export class AllActivityComponent implements OnInit {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
protected dataService: RiskInsightsDataService,
|
protected dataService: RiskInsightsDataService,
|
||||||
|
protected reportService: RiskInsightsReportService,
|
||||||
|
protected criticalAppsService: CriticalAppsService,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user