mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +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:
@@ -280,6 +280,24 @@
|
|||||||
"totalApplications": {
|
"totalApplications": {
|
||||||
"message": "Total applications"
|
"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": {
|
"unmarkAsCritical": {
|
||||||
"message": "Unmark as critical"
|
"message": "Unmark as critical"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export class AllActivitiesService {
|
|||||||
totalAtRiskMemberCount: summary.totalAtRiskMemberCount,
|
totalAtRiskMemberCount: summary.totalAtRiskMemberCount,
|
||||||
totalApplicationCount: summary.totalApplicationCount,
|
totalApplicationCount: summary.totalApplicationCount,
|
||||||
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
|
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
|
||||||
|
newApplications: summary.newApplications,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,23 @@ export class RiskInsightsReportService {
|
|||||||
const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails);
|
const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails);
|
||||||
const uniqueAtRiskMembers = getUniqueMembers(atRiskMembers);
|
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 {
|
return {
|
||||||
totalMemberCount: uniqueMembers.length,
|
totalMemberCount: uniqueMembers.length,
|
||||||
totalAtRiskMemberCount: uniqueAtRiskMembers.length,
|
totalAtRiskMemberCount: uniqueAtRiskMembers.length,
|
||||||
@@ -236,7 +252,7 @@ export class RiskInsightsReportService {
|
|||||||
totalCriticalAtRiskMemberCount: 0,
|
totalCriticalAtRiskMemberCount: 0,
|
||||||
totalCriticalApplicationCount: 0,
|
totalCriticalApplicationCount: 0,
|
||||||
totalCriticalAtRiskApplicationCount: 0,
|
totalCriticalAtRiskApplicationCount: 0,
|
||||||
newApplications: [],
|
newApplications: dummyNewApplications,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
<div class="tw-flex-col">
|
<div class="tw-flex-col">
|
||||||
<span bitTypography="h6" class="tw-flex tw-text-main">{{ title }}</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">
|
||||||
|
@if (iconClass) {
|
||||||
|
<i class="bwi {{ iconClass }} tw-text-muted" aria-hidden="true"></i>
|
||||||
|
}
|
||||||
<span bitTypography="h3">{{ cardMetrics }}</span>
|
<span bitTypography="h3">{{ cardMetrics }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
|
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
|
||||||
<span bitTypography="body2">{{ metricDescription }}</span>
|
<span bitTypography="body2">{{ metricDescription }}</span>
|
||||||
</div>
|
</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">
|
<div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2">
|
||||||
<p bitTypography="body1">
|
<p bitTypography="body1">
|
||||||
<a bitLink (click)="navigateToLink(navigationLink)" rel="noreferrer">
|
<a bitLink (click)="navigateToLink(navigationLink)" rel="noreferrer">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
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 { Router } from "@angular/router";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components";
|
import { ButtonModule, ButtonType, LinkModule, TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "dirt-activity-card",
|
selector: "dirt-activity-card",
|
||||||
@@ -43,9 +43,34 @@ export class ActivityCardComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() showNavigationLink: boolean = false;
|
@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) {}
|
constructor(private router: Router) {}
|
||||||
|
|
||||||
navigateToLink = async (navigationLink: string) => {
|
navigateToLink = async (navigationLink: string) => {
|
||||||
await this.router.navigateByUrl(navigationLink);
|
await this.router.navigateByUrl(navigationLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onButtonClick = () => {
|
||||||
|
this.buttonClick.emit();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,5 +41,18 @@
|
|||||||
>
|
>
|
||||||
</dirt-activity-card>
|
</dirt-activity-card>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { OrganizationService } from "@bitwarden/common/admin-console/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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { getById } from "@bitwarden/common/platform/misc";
|
import { getById } from "@bitwarden/common/platform/misc";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
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";
|
||||||
@@ -34,6 +36,7 @@ export class AllActivityComponent implements OnInit {
|
|||||||
totalCriticalAppsAtRiskMemberCount = 0;
|
totalCriticalAppsAtRiskMemberCount = 0;
|
||||||
totalCriticalAppsCount = 0;
|
totalCriticalAppsCount = 0;
|
||||||
totalCriticalAppsAtRiskCount = 0;
|
totalCriticalAppsAtRiskCount = 0;
|
||||||
|
newApplicationsCount = 0;
|
||||||
passwordChangeMetricHasProgressBar = false;
|
passwordChangeMetricHasProgressBar = false;
|
||||||
|
|
||||||
destroyRef = inject(DestroyRef);
|
destroyRef = inject(DestroyRef);
|
||||||
@@ -54,6 +57,7 @@ export class AllActivityComponent implements OnInit {
|
|||||||
this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount;
|
this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount;
|
||||||
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
|
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
|
||||||
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
|
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
|
||||||
|
this.newApplicationsCount = summary.newApplications.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
|
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
|
||||||
@@ -70,6 +74,8 @@ export class AllActivityComponent implements OnInit {
|
|||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
protected dataService: RiskInsightsDataService,
|
protected dataService: RiskInsightsDataService,
|
||||||
protected allActivitiesService: AllActivitiesService,
|
protected allActivitiesService: AllActivitiesService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get RiskInsightsTabType() {
|
get RiskInsightsTabType() {
|
||||||
@@ -80,4 +86,17 @@ export class AllActivityComponent implements OnInit {
|
|||||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
||||||
return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`;
|
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()),
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user