1
0
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:
Vijay Oommen
2025-09-18 10:10:46 -05:00
committed by GitHub
parent 8d4d38d334
commit f04b24851c
5 changed files with 143 additions and 26 deletions

View File

@@ -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": {

View File

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

View File

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

View File

@@ -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>
}

View File

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