1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[PM-26074] All Activities tab - Updated UI (#16587)

* PM-26074 simplified and update the all-activities tab

* PM-26074 removed learn more

* PM-26074 fixing missing line
This commit is contained in:
Vijay Oommen
2025-09-29 15:19:14 -05:00
committed by GitHub
parent e784622f67
commit 0bfc5daa7c
10 changed files with 168 additions and 42 deletions

View File

@@ -59,9 +59,28 @@
"createNewLoginItem": { "createNewLoginItem": {
"message": "Create new login item" "message": "Create new login item"
}, },
"criticalApplicationsActivityDescription": { "onceYouMarkCriticalApplicationsActivityDescription": {
"message": "Once you mark applications critical, they will display here." "message": "Once you mark applications critical, they will display here."
}, },
"viewAtRiskMembers": {
"message": "View at-risk members"
},
"viewAtRiskApplications": {
"message": "View at-risk applications"
},
"criticalApplicationsAreAtRisk": {
"message": "$COUNT$ out of $TOTAL$ critical applications are at-risk due to at-risk passwords",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
},
"total": {
"content": "$2",
"example": "5"
}
}
},
"criticalApplicationsWithCount": { "criticalApplicationsWithCount": {
"message": "Critical applications ($COUNT$)", "message": "Critical applications ($COUNT$)",
"placeholders": { "placeholders": {
@@ -80,6 +99,15 @@
} }
} }
}, },
"countOfApplicationsAtRisk": {
"message": "$COUNT$ applications at-risk",
"placeholders": {
"count": {
"content": "$1",
"example": "3"
}
}
},
"notifiedMembersWithCount": { "notifiedMembersWithCount": {
"message": "Notified members ($COUNT$)", "message": "Notified members ($COUNT$)",
"placeholders": { "placeholders": {

View File

@@ -0,0 +1,44 @@
import { BehaviorSubject } from "rxjs";
import { OrganizationReportSummary } from "../models/report-models";
export class AllActivitiesService {
/// This class is used to manage the summary of all applications
/// and critical applications.
/// Going forward, this class can be simplified by using the RiskInsightsDataService
/// as it contains the application summary data.
private reportSummarySubject$ = new BehaviorSubject<OrganizationReportSummary>({
totalMemberCount: 0,
totalCriticalMemberCount: 0,
totalAtRiskMemberCount: 0,
totalCriticalAtRiskMemberCount: 0,
totalApplicationCount: 0,
totalCriticalApplicationCount: 0,
totalAtRiskApplicationCount: 0,
totalCriticalAtRiskApplicationCount: 0,
newApplications: [],
});
reportSummary$ = this.reportSummarySubject$.asObservable();
setCriticalAppsReportSummary(summary: OrganizationReportSummary) {
this.reportSummarySubject$.next({
...this.reportSummarySubject$.getValue(),
totalCriticalApplicationCount: summary.totalApplicationCount,
totalCriticalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
totalCriticalMemberCount: summary.totalMemberCount,
totalCriticalAtRiskMemberCount: summary.totalAtRiskMemberCount,
});
}
setAllAppsReportSummary(summary: OrganizationReportSummary) {
this.reportSummarySubject$.next({
...this.reportSummarySubject$.getValue(),
totalMemberCount: summary.totalMemberCount,
totalAtRiskMemberCount: summary.totalAtRiskMemberCount,
totalApplicationCount: summary.totalApplicationCount,
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
});
}
}

View File

@@ -5,3 +5,4 @@ export * from "./critical-apps-api.service";
export * from "./risk-insights-api.service"; export * from "./risk-insights-api.service";
export * from "./risk-insights-report.service"; export * from "./risk-insights-report.service";
export * from "./risk-insights-data.service"; export * from "./risk-insights-data.service";
export * from "./all-activities.service";

View File

@@ -3,6 +3,7 @@ import { NgModule } from "@angular/core";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { CriticalAppsService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { CriticalAppsService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { import {
AllActivitiesService,
CriticalAppsApiService, CriticalAppsApiService,
MemberCipherDetailsApiService, MemberCipherDetailsApiService,
PasswordHealthService, PasswordHealthService,
@@ -73,6 +74,11 @@ import { RiskInsightsComponent } from "./risk-insights.component";
useClass: CriticalAppsApiService, useClass: CriticalAppsApiService,
deps: [ApiService], deps: [ApiService],
}), }),
safeProvider({
provide: AllActivitiesService,
useClass: AllActivitiesService,
deps: [],
}),
], ],
}) })
export class AccessIntelligenceModule {} export class AccessIntelligenceModule {}

View File

@@ -1,9 +1,18 @@
<div class="tw-flex-col"> <div class="tw-flex-col">
<span bitTypography="h6" class="tw-flex tw-text-main">{{ title | i18n }}</span> <span bitTypography="h6" class="tw-flex tw-text-main">{{ title }}</span>
<div class="tw-flex tw-items-baseline tw-gap-2"> <div class="tw-flex tw-items-baseline tw-gap-2">
<span bitTypography="h3">{{ cardMetrics | i18n: value }}</span> <span bitTypography="h3">{{ cardMetrics }}</span>
</div> </div>
<div class="tw-flex tw-items-baseline tw-gap-2"> <div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
<span bitTypography="body2">{{ metricDescription | i18n }}</span> <span bitTypography="body2">{{ metricDescription }}</span>
</div> </div>
@if (showNavigationLink) {
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
<p bitTypography="body1">
<a bitLink (click)="navigateToLink(navigationLink)" rel="noreferrer">
{{ navigationText }}
</a>
</p>
</div>
}
</div> </div>

View File

@@ -1,13 +1,14 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TypographyModule } from "@bitwarden/components"; import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
@Component({ @Component({
selector: "dirt-activity-card", selector: "dirt-activity-card",
templateUrl: "./activity-card.component.html", templateUrl: "./activity-card.component.html",
imports: [CommonModule, TypographyModule, JslibModule], imports: [CommonModule, TypographyModule, JslibModule, LinkModule, ButtonModule],
host: { host: {
class: 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", "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",
@@ -18,10 +19,6 @@ export class ActivityCardComponent {
* The title of the card goes here * The title of the card goes here
*/ */
@Input() title: string = ""; @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 * The card metrics text to display next to the value
*/ */
@@ -30,4 +27,25 @@ export class ActivityCardComponent {
* The description text to display below the value and metrics * The description text to display below the value and metrics
*/ */
@Input() metricDescription: string = ""; @Input() metricDescription: string = "";
/**
* The link to navigate to for more information
*/
@Input() navigationLink: string = "";
/**
* The text to display for the navigation link
*/
@Input() navigationText: string = "";
/**
* Show Navigation link
*/
@Input() showNavigationLink: boolean = false;
constructor(private router: Router) {}
navigateToLink = async (navigationLink: string) => {
await this.router.navigateByUrl(navigationLink);
};
} }

View File

@@ -21,19 +21,33 @@
<div class="tw-flex tw-gap-4 tw-col-span-6"> <div class="tw-flex tw-gap-4 tw-col-span-6">
<dirt-activity-card <dirt-activity-card
class="tw-col-span-2 tw-cursor-pointer" class="tw-col-span-2 tw-cursor-pointer"
[title]="'atRiskMembers'" [title]="'atRiskMembers' | i18n"
[value]="atRiskMemberCount" [cardMetrics]="'membersAtRisk' | i18n: totalCriticalAppsAtRiskMemberCount"
[cardMetrics]="'membersAtRisk'" [metricDescription]="'membersAtRiskActivityDescription' | i18n"
[metricDescription]="'membersAtRiskActivityDescription'" navigationText="{{ 'viewAtRiskMembers' | i18n }}"
navigationLink="{{ getLinkForRiskInsightsTab(RiskInsightsTabType.AllApps) }}"
[showNavigationLink]="totalCriticalAppsAtRiskMemberCount > 0"
> >
</dirt-activity-card> </dirt-activity-card>
<dirt-activity-card <dirt-activity-card
#allAppsOrgAtRiskApplications #allAppsOrgAtRiskApplications
class="tw-col-span-2 tw-cursor-pointer" class="tw-col-span-2 tw-cursor-pointer"
[title]="'criticalApplications'" [title]="'criticalApplications' | i18n"
[value]="criticalApplicationsCount" [cardMetrics]="
[cardMetrics]="'countOfCriticalApplications'" totalCriticalAppsCount === 0
[metricDescription]="'criticalApplicationsActivityDescription'" ? ('countOfCriticalApplications' | i18n: totalCriticalAppsCount)
: ('countOfApplicationsAtRisk' | i18n: totalCriticalAppsCount)
"
[metricDescription]="
totalCriticalAppsCount === 0
? ('onceYouMarkCriticalApplicationsActivityDescription' | i18n)
: ('criticalApplicationsAreAtRisk'
| i18n: totalCriticalAppsAtRiskCount : totalCriticalAppsCount)
"
navigationText="{{ 'viewAtRiskApplications' | i18n }}"
navigationLink="{{ getLinkForRiskInsightsTab(RiskInsightsTabType.CriticalApps) }}"
[showNavigationLink]="totalCriticalAppsAtRiskCount > 0"
> >
</dirt-activity-card> </dirt-activity-card>
</div> </div>

View File

@@ -1,23 +1,22 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { BehaviorSubject, combineLatest, firstValueFrom, of, switchMap } from "rxjs"; import { BehaviorSubject, firstValueFrom } from "rxjs";
import { import {
CriticalAppsService, AllActivitiesService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights"; } 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";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { getById } from "@bitwarden/common/platform/misc"; import { getById } from "@bitwarden/common/platform/misc";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { ActivityCardComponent } from "./activity-card.component"; import { ActivityCardComponent } from "./activity-card.component";
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({ @Component({
selector: "tools-all-activity", selector: "tools-all-activity",
@@ -28,8 +27,9 @@ export class AllActivityComponent implements OnInit {
protected isLoading$ = this.dataService.isLoading$; protected isLoading$ = this.dataService.isLoading$;
protected noData$ = new BehaviorSubject(true); protected noData$ = new BehaviorSubject(true);
organization: Organization | null = null; organization: Organization | null = null;
atRiskMemberCount = 0; totalCriticalAppsAtRiskMemberCount = 0;
criticalApplicationsCount = 0; totalCriticalAppsCount = 0;
totalCriticalAppsAtRiskCount = 0;
destroyRef = inject(DestroyRef); destroyRef = inject(DestroyRef);
@@ -43,21 +43,13 @@ export class AllActivityComponent implements OnInit {
this.organizationService.organizations$(userId).pipe(getById(organizationId)), this.organizationService.organizations$(userId).pipe(getById(organizationId)),
)) ?? null; )) ?? null;
combineLatest([ this.allActivitiesService.reportSummary$
this.dataService.applications$, .pipe(takeUntilDestroyed(this.destroyRef))
this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId), .subscribe((summary) => {
]) this.noData$.next(summary.totalApplicationCount === 0);
.pipe( this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount;
takeUntilDestroyed(this.destroyRef), this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
switchMap(([apps, criticalApps]) => { this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
const atRiskMembers = this.reportService.generateAtRiskMemberList(apps ?? []);
return of({ apps, atRiskMembers, criticalApps });
}),
)
.subscribe(({ apps, atRiskMembers, criticalApps }) => {
this.noData$.next((apps?.length ?? 0) === 0);
this.atRiskMemberCount = atRiskMembers?.length ?? 0;
this.criticalApplicationsCount = criticalApps?.length ?? 0;
}); });
} }
} }
@@ -67,7 +59,15 @@ 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 allActivitiesService: AllActivitiesService,
protected criticalAppsService: CriticalAppsService,
) {} ) {}
get RiskInsightsTabType() {
return RiskInsightsTabType;
}
getLinkForRiskInsightsTab(tabIndex: RiskInsightsTabType): string {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`;
}
} }

View File

@@ -6,6 +6,7 @@ import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, switc
import { Security } from "@bitwarden/assets/svg"; import { Security } from "@bitwarden/assets/svg";
import { import {
AllActivitiesService,
CriticalAppsService, CriticalAppsService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService, RiskInsightsReportService,
@@ -120,6 +121,7 @@ export class AllApplicationsComponent implements OnInit {
if (data) { if (data) {
this.dataSource.data = data; this.dataSource.data = data;
this.applicationSummary = this.reportService.generateApplicationsSummary(data); this.applicationSummary = this.reportService.generateApplicationsSummary(data);
this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary);
} }
if (organization) { if (organization) {
this.organization = organization; this.organization = organization;
@@ -142,6 +144,7 @@ export class AllApplicationsComponent implements OnInit {
private accountService: AccountService, private accountService: AccountService,
protected criticalAppsService: CriticalAppsService, protected criticalAppsService: CriticalAppsService,
protected riskInsightsEncryptionService: RiskInsightsEncryptionService, protected riskInsightsEncryptionService: RiskInsightsEncryptionService,
protected allActivitiesService: AllActivitiesService,
) { ) {
this.searchControl.valueChanges this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed()) .pipe(debounceTime(200), takeUntilDestroyed())

View File

@@ -8,6 +8,7 @@ import { combineLatest, debounceTime, firstValueFrom, map, switchMap } from "rxj
import { Security } from "@bitwarden/assets/svg"; import { Security } from "@bitwarden/assets/svg";
import { import {
AllActivitiesService,
CriticalAppsService, CriticalAppsService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService, RiskInsightsReportService,
@@ -99,6 +100,7 @@ export class CriticalApplicationsComponent implements OnInit {
this.dataSource.data = applications; this.dataSource.data = applications;
this.applicationSummary = this.reportService.generateApplicationsSummary(applications); this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0;
this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary);
} }
}); });
} }
@@ -176,6 +178,7 @@ export class CriticalApplicationsComponent implements OnInit {
private configService: ConfigService, private configService: ConfigService,
private adminTaskService: DefaultAdminTaskService, private adminTaskService: DefaultAdminTaskService,
private accountService: AccountService, private accountService: AccountService,
private allActivitiesService: AllActivitiesService,
) { ) {
this.searchControl.valueChanges this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed()) .pipe(debounceTime(200), takeUntilDestroyed())