1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-26185] new app metric card (#16658)

* new messages.json keys

* button changes for dirt activity card

* dummy data

* newApplicationsCount and temp toast

* Added third dirt-activity-card component after the existing two cards

* added newApplications to setAllAppsReportSummary

* make button smaller

* cleanup/nice-to-haves

* remove comment

* simplify activity card icon logic to use nullable iconClass

* use buttonText presence to determine button display in activity card

* apps needing review card
- I think accidentally deleted when resolving merge conflicts

* buttonClick.observed && buttonText
This commit is contained in:
Alex
2025-10-06 15:29:59 -04:00
committed by GitHub
parent 5260c8336b
commit f29e5e223d
7 changed files with 114 additions and 5 deletions

View File

@@ -280,6 +280,24 @@
"totalApplications": {
"message": "Total applications"
},
"applicationsNeedingReview": {
"message": "Applications needing review"
},
"newApplicationsWithCount": {
"message": "$COUNT$ new applications",
"placeholders": {
"count": {
"content": "$1",
"example": "13"
}
}
},
"newApplicationsDescription": {
"message": "Review new applications to mark as critical and keep your organization secure"
},
"reviewNow": {
"message": "Review now"
},
"unmarkAsCritical": {
"message": "Unmark as critical"
},

View File

@@ -68,6 +68,7 @@ export class AllActivitiesService {
totalAtRiskMemberCount: summary.totalAtRiskMemberCount,
totalApplicationCount: summary.totalApplicationCount,
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
newApplications: summary.newApplications,
});
}

View File

@@ -226,7 +226,23 @@ export class RiskInsightsReportService {
const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails);
const uniqueAtRiskMembers = getUniqueMembers(atRiskMembers);
// TODO: totalCriticalMemberCount, totalCriticalAtRiskMemberCount, totalCriticalApplicationCount, totalCriticalAtRiskApplicationCount, and newApplications will be handled with future logic implementation
// TODO: Replace with actual new applications detection logic (PM-26185)
const dummyNewApplications = [
"github.com",
"google.com",
"stackoverflow.com",
"gitlab.com",
"bitbucket.org",
"npmjs.com",
"docker.com",
"aws.amazon.com",
"azure.microsoft.com",
"jenkins.io",
"terraform.io",
"kubernetes.io",
"atlassian.net",
];
return {
totalMemberCount: uniqueMembers.length,
totalAtRiskMemberCount: uniqueAtRiskMembers.length,
@@ -236,7 +252,7 @@ export class RiskInsightsReportService {
totalCriticalAtRiskMemberCount: 0,
totalCriticalApplicationCount: 0,
totalCriticalAtRiskApplicationCount: 0,
newApplications: [],
newApplications: dummyNewApplications,
};
}

View File

@@ -1,12 +1,29 @@
<div class="tw-flex-col">
<span bitTypography="h6" class="tw-flex tw-text-main">{{ title }}</span>
<div class="tw-flex tw-items-baseline tw-gap-2">
@if (iconClass) {
<i class="bwi {{ iconClass }} tw-text-muted" aria-hidden="true"></i>
}
<span bitTypography="h3">{{ cardMetrics }}</span>
</div>
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
<span bitTypography="body2">{{ metricDescription }}</span>
</div>
@if (showNavigationLink) {
@if (buttonClick.observed && buttonText) {
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
<button
type="button"
bitButton
size="small"
[buttonType]="buttonType"
[attr.aria-label]="buttonText"
(click)="onButtonClick()"
>
{{ buttonText }}
</button>
</div>
}
@if (showNavigationLink && !buttonText) {
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
<p bitTypography="body1">
<a bitLink (click)="navigateToLink(navigationLink)" rel="noreferrer">

View File

@@ -1,9 +1,9 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
import { ButtonModule, ButtonType, LinkModule, TypographyModule } from "@bitwarden/components";
@Component({
selector: "dirt-activity-card",
@@ -43,9 +43,34 @@ export class ActivityCardComponent {
*/
@Input() showNavigationLink: boolean = false;
/**
* Icon class to display next to metrics (e.g., "bwi-exclamation-triangle").
* If null, no icon is displayed.
*/
@Input() iconClass: string | null = null;
/**
* Button text. If provided, a button will be displayed instead of a navigation link.
*/
@Input() buttonText: string = "";
/**
* Button type (e.g., "primary", "secondary")
*/
@Input() buttonType: ButtonType = "primary";
/**
* Event emitted when button is clicked
*/
@Output() buttonClick = new EventEmitter<void>();
constructor(private router: Router) {}
navigateToLink = async (navigationLink: string) => {
await this.router.navigateByUrl(navigationLink);
};
onButtonClick = () => {
this.buttonClick.emit();
};
}

View File

@@ -41,5 +41,18 @@
>
</dirt-activity-card>
</li>
<li class="tw-col-span-1">
<dirt-activity-card
[title]="'applicationsNeedingReview' | i18n"
[cardMetrics]="'newApplicationsWithCount' | i18n: newApplicationsCount"
[metricDescription]="'newApplicationsDescription' | i18n"
[iconClass]="'bwi-exclamation-triangle'"
[buttonText]="'reviewNow' | i18n"
[buttonType]="'primary'"
(buttonClick)="onReviewNewApplications()"
>
</dirt-activity-card>
</li>
</ul>
}

View File

@@ -11,7 +11,9 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { getById } from "@bitwarden/common/platform/misc";
import { ToastService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { ActivityCardComponent } from "./activity-card.component";
@@ -34,6 +36,7 @@ export class AllActivityComponent implements OnInit {
totalCriticalAppsAtRiskMemberCount = 0;
totalCriticalAppsCount = 0;
totalCriticalAppsAtRiskCount = 0;
newApplicationsCount = 0;
passwordChangeMetricHasProgressBar = false;
destroyRef = inject(DestroyRef);
@@ -54,6 +57,7 @@ export class AllActivityComponent implements OnInit {
this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount;
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
this.newApplicationsCount = summary.newApplications.length;
});
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
@@ -70,6 +74,8 @@ export class AllActivityComponent implements OnInit {
protected organizationService: OrganizationService,
protected dataService: RiskInsightsDataService,
protected allActivitiesService: AllActivitiesService,
private toastService: ToastService,
private i18nService: I18nService,
) {}
get RiskInsightsTabType() {
@@ -80,4 +86,17 @@ export class AllActivityComponent implements OnInit {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`;
}
/**
* Handles the review new applications button click.
* Shows a toast notification as a placeholder until the dialog is implemented.
* TODO: Implement dialog for reviewing new applications (follow-up task)
*/
onReviewNewApplications = () => {
this.toastService.showToast({
variant: "info",
title: this.i18nService.t("applicationsNeedingReview"),
message: this.i18nService.t("newApplicationsWithCount", this.newApplicationsCount.toString()),
});
};
}