mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 11:43:46 +00:00
[PM-26018] All Activity tab - Password change metric (#16644)
This commit is contained in:
@@ -59,8 +59,42 @@
|
|||||||
"createNewLoginItem": {
|
"createNewLoginItem": {
|
||||||
"message": "Create new login item"
|
"message": "Create new login item"
|
||||||
},
|
},
|
||||||
"onceYouMarkApplicationsCriticalTheyWillDisplayHere": {
|
"percentageCompleted": {
|
||||||
"message": "Once you mark applications critical, they will display here"
|
"message": "$PERCENT$% complete",
|
||||||
|
"placeholders": {
|
||||||
|
"percent": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "75"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securityTasksCompleted": {
|
||||||
|
"message": "$COUNT$ out of $TOTAL$ security tasks completed",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passwordChangeProgress": {
|
||||||
|
"message": "Password change progress"
|
||||||
|
},
|
||||||
|
"assignMembersTasksToMonitorProgress": {
|
||||||
|
"message": "Assign members tasks to monitor progress"
|
||||||
|
},
|
||||||
|
"onceYouReviewApplications": {
|
||||||
|
"message": "Once you review applications and mark them as critical, they will display here."
|
||||||
|
},
|
||||||
|
"sendReminders": {
|
||||||
|
"message": "Send reminders"
|
||||||
|
},
|
||||||
|
"criticalApplicationsActivityDescription": {
|
||||||
|
"message": "Once you mark applications critical, they will display here."
|
||||||
},
|
},
|
||||||
"viewAtRiskMembers": {
|
"viewAtRiskMembers": {
|
||||||
"message": "View at-risk members"
|
"message": "View at-risk members"
|
||||||
@@ -108,6 +142,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"countOfAtRiskPasswords": {
|
||||||
|
"message": "$COUNT$ passwords at-risk",
|
||||||
|
"placeholders": {
|
||||||
|
"count": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"notifiedMembersWithCount": {
|
"notifiedMembersWithCount": {
|
||||||
"message": "Notified members ($COUNT$)",
|
"message": "Notified members ($COUNT$)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -9523,6 +9566,9 @@
|
|||||||
"assign": {
|
"assign": {
|
||||||
"message": "Assign"
|
"message": "Assign"
|
||||||
},
|
},
|
||||||
|
"assignTasks": {
|
||||||
|
"message": "Assign tasks"
|
||||||
|
},
|
||||||
"assignToCollections": {
|
"assignToCollections": {
|
||||||
"message": "Assign to collections"
|
"message": "Assign to collections"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "../models";
|
||||||
import { OrganizationReportSummary } from "../models/report-models";
|
import { OrganizationReportSummary } from "../models/report-models";
|
||||||
|
|
||||||
export class AllActivitiesService {
|
export class AllActivitiesService {
|
||||||
@@ -22,6 +23,18 @@ export class AllActivitiesService {
|
|||||||
|
|
||||||
reportSummary$ = this.reportSummarySubject$.asObservable();
|
reportSummary$ = this.reportSummarySubject$.asObservable();
|
||||||
|
|
||||||
|
private allApplicationsDetailsSubject$: BehaviorSubject<
|
||||||
|
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[]
|
||||||
|
> = new BehaviorSubject<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[]>([]);
|
||||||
|
allApplicationsDetails$ = this.allApplicationsDetailsSubject$.asObservable();
|
||||||
|
|
||||||
|
private atRiskPasswordsCountSubject$ = new BehaviorSubject<number>(0);
|
||||||
|
atRiskPasswordsCount$ = this.atRiskPasswordsCountSubject$.asObservable();
|
||||||
|
|
||||||
|
private passwordChangeProgressMetricHasProgressBarSubject$ = new BehaviorSubject<boolean>(false);
|
||||||
|
passwordChangeProgressMetricHasProgressBar$ =
|
||||||
|
this.passwordChangeProgressMetricHasProgressBarSubject$.asObservable();
|
||||||
|
|
||||||
setCriticalAppsReportSummary(summary: OrganizationReportSummary) {
|
setCriticalAppsReportSummary(summary: OrganizationReportSummary) {
|
||||||
this.reportSummarySubject$.next({
|
this.reportSummarySubject$.next({
|
||||||
...this.reportSummarySubject$.getValue(),
|
...this.reportSummarySubject$.getValue(),
|
||||||
@@ -41,4 +54,20 @@ export class AllActivitiesService {
|
|||||||
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
|
totalAtRiskApplicationCount: summary.totalAtRiskApplicationCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAllAppsReportDetails(
|
||||||
|
applications: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||||
|
) {
|
||||||
|
const totalAtRiskPasswords = applications.reduce(
|
||||||
|
(sum, app) => sum + app.atRiskPasswordCount,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
this.atRiskPasswordsCountSubject$.next(totalAtRiskPasswords);
|
||||||
|
|
||||||
|
this.allApplicationsDetailsSubject$.next(applications);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasswordChangeProgressMetricHasProgressBar(hasProgressBar: boolean) {
|
||||||
|
this.passwordChangeProgressMetricHasProgressBarSubject$.next(hasProgressBar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ 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";
|
export * from "./all-activities.service";
|
||||||
|
export * from "./security-tasks-api.service";
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { SecurityTasksApiService, TaskMetrics } from "./security-tasks-api.service";
|
||||||
|
|
||||||
|
describe("SecurityTasksApiService", () => {
|
||||||
|
const apiServiceMock = mock<ApiService>();
|
||||||
|
let service: SecurityTasksApiService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new SecurityTasksApiService(apiServiceMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be created", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getTaskMetrics", () => {
|
||||||
|
it("should call apiService.send with correct parameters", (done) => {
|
||||||
|
const orgId = { toString: () => "org-123" } as OrganizationId;
|
||||||
|
const mockMetrics: TaskMetrics = { completedTasks: 2, totalTasks: 5 };
|
||||||
|
apiServiceMock.send.mockReturnValue(Promise.resolve(mockMetrics));
|
||||||
|
|
||||||
|
service.getTaskMetrics(orgId).subscribe((metrics) => {
|
||||||
|
expect(apiServiceMock.send).toHaveBeenCalledWith(
|
||||||
|
"GET",
|
||||||
|
"/tasks/org-123/metrics",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(metrics).toEqual(mockMetrics);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should propagate errors from apiService.send", (done) => {
|
||||||
|
const orgId = { toString: () => "org-456" } as OrganizationId;
|
||||||
|
const error = new Error("API error");
|
||||||
|
apiServiceMock.send.mockReturnValue(Promise.reject(error));
|
||||||
|
|
||||||
|
service.getTaskMetrics(orgId).subscribe({
|
||||||
|
next: () => {},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
expect(err).toBe(error);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { from, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
export type TaskMetrics = {
|
||||||
|
completedTasks: number;
|
||||||
|
totalTasks: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SecurityTasksApiService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
getTaskMetrics(orgId: OrganizationId): Observable<TaskMetrics> {
|
||||||
|
const dbResponse = this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
`/tasks/${orgId.toString()}/metrics`,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return from(dbResponse as Promise<TaskMetrics>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
RiskInsightsApiService,
|
RiskInsightsApiService,
|
||||||
RiskInsightsDataService,
|
RiskInsightsDataService,
|
||||||
RiskInsightsReportService,
|
RiskInsightsReportService,
|
||||||
|
SecurityTasksApiService,
|
||||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights/services";
|
} from "@bitwarden/bit-common/dirt/reports/risk-insights/services";
|
||||||
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
|
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -79,6 +80,11 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
|||||||
useClass: AllActivitiesService,
|
useClass: AllActivitiesService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SecurityTasksApiService,
|
||||||
|
useClass: SecurityTasksApiService,
|
||||||
|
deps: [ApiService],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccessIntelligenceModule {}
|
export class AccessIntelligenceModule {}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/component
|
|||||||
imports: [CommonModule, TypographyModule, JslibModule, LinkModule, ButtonModule],
|
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 tw-h-56 tw-max-h-56",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class ActivityCardComponent {
|
export class ActivityCardComponent {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div
|
||||||
|
class="tw-flex tw-flex-col tw-p-6 tw-box-border tw-bg-background tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-rounded-lg tw-h-56 tw-max-h-56"
|
||||||
|
>
|
||||||
|
<div bitTypography="h6" class="tw-mb-2">
|
||||||
|
{{ "passwordChangeProgress" | i18n }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (renderMode === renderModes.noCriticalApps) {
|
||||||
|
<div class="tw-items-start tw-mb-2">
|
||||||
|
<span bitTypography="h3">{{ "assignMembersTasksToMonitorProgress" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-items-baseline tw-gap-2">
|
||||||
|
<span bitTypography="body2">{{ "onceYouReviewApplications" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (renderMode === renderModes.criticalAppsWithAtRiskAppsAndNoTasks) {
|
||||||
|
<div class="tw-items-start tw-mb-2">
|
||||||
|
<span bitTypography="h3">{{ "assignMembersTasksToMonitorProgress" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-items-baseline tw-gap-2">
|
||||||
|
<span bitTypography="body2">{{ "countOfAtRiskPasswords" | i18n: atRiskPasswordsCount }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-mt-4">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="!canAssignTasks"
|
||||||
|
(click)="assignTasks()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-envelope tw-mr-2"></i>
|
||||||
|
{{ "assignTasks" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (renderMode === renderModes.criticalAppsWithAtRiskAppsAndTasks) {
|
||||||
|
<div class="tw-items-start tw-mb-2">
|
||||||
|
<span bitTypography="h3">{{ "percentageCompleted" | i18n: completedPercent }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-items-baseline tw-gap-2">
|
||||||
|
<span bitTypography="body2">{{
|
||||||
|
"securityTasksCompleted" | i18n: completedTasksCount : totalTasksCount
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-mt-4">
|
||||||
|
<div class="tw-flex tw-justify-between">
|
||||||
|
<div bitTypography="body2">{{ completedTasksCount }}</div>
|
||||||
|
<div bitTypography="body2">{{ totalTasksCount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<bit-progress
|
||||||
|
[showText]="false"
|
||||||
|
size="small"
|
||||||
|
bgColor="primary"
|
||||||
|
[barWidth]="completedPercent"
|
||||||
|
[ariaLabel]="'passwordChangeProgressBar' | i18n"
|
||||||
|
>
|
||||||
|
</bit-progress>
|
||||||
|
|
||||||
|
<!-- TODO: Implement reminder functionality -->
|
||||||
|
<!-- <div class="tw-items-start tw-mt-4 tw-gap-4">
|
||||||
|
<button bitButton type="button" buttonType="secondary">
|
||||||
|
<i class="bwi bwi-envelope" aria-hidden="true"></i>
|
||||||
|
{{ "sendReminders" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { Subject, switchMap, takeUntil, of, BehaviorSubject, combineLatest } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import {
|
||||||
|
AllActivitiesService,
|
||||||
|
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
|
SecurityTasksApiService,
|
||||||
|
TaskMetrics,
|
||||||
|
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||||
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
ProgressModule,
|
||||||
|
ToastService,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CreateTasksRequest } from "../../../vault/services/abstractions/admin-task.abstraction";
|
||||||
|
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
|
||||||
|
|
||||||
|
export const RenderMode = {
|
||||||
|
noCriticalApps: "noCriticalApps",
|
||||||
|
criticalAppsWithAtRiskAppsAndNoTasks: "criticalAppsWithAtRiskAppsAndNoTasks",
|
||||||
|
criticalAppsWithAtRiskAppsAndTasks: "criticalAppsWithAtRiskAppsAndTasks",
|
||||||
|
} as const;
|
||||||
|
export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode];
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "dirt-password-change-metric",
|
||||||
|
imports: [CommonModule, TypographyModule, JslibModule, ProgressModule, ButtonModule],
|
||||||
|
templateUrl: "./password-change-metric.component.html",
|
||||||
|
providers: [DefaultAdminTaskService],
|
||||||
|
})
|
||||||
|
export class PasswordChangeMetricComponent implements OnInit {
|
||||||
|
protected taskMetrics$ = new BehaviorSubject<TaskMetrics>({ totalTasks: 0, completedTasks: 0 });
|
||||||
|
private completedTasks: number = 0;
|
||||||
|
private totalTasks: number = 0;
|
||||||
|
private allApplicationsDetails: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
atRiskAppsCount: number = 0;
|
||||||
|
atRiskPasswordsCount: number = 0;
|
||||||
|
private organizationId!: OrganizationId;
|
||||||
|
private destroyRef = new Subject<void>();
|
||||||
|
renderMode: RenderMode = "noCriticalApps";
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.activatedRoute.paramMap
|
||||||
|
.pipe(
|
||||||
|
switchMap((paramMap) => {
|
||||||
|
const orgId = paramMap.get("organizationId");
|
||||||
|
if (orgId) {
|
||||||
|
this.organizationId = orgId as OrganizationId;
|
||||||
|
return this.securityTasksApiService.getTaskMetrics(this.organizationId);
|
||||||
|
}
|
||||||
|
return of({ totalTasks: 0, completedTasks: 0 });
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroyRef),
|
||||||
|
)
|
||||||
|
.subscribe((metrics) => {
|
||||||
|
this.taskMetrics$.next(metrics);
|
||||||
|
});
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
this.taskMetrics$,
|
||||||
|
this.allActivitiesService.reportSummary$,
|
||||||
|
this.allActivitiesService.atRiskPasswordsCount$,
|
||||||
|
this.allActivitiesService.allApplicationsDetails$,
|
||||||
|
])
|
||||||
|
.pipe(takeUntil(this.destroyRef))
|
||||||
|
.subscribe(([taskMetrics, summary, atRiskPasswordsCount, allApplicationsDetails]) => {
|
||||||
|
this.atRiskAppsCount = summary.totalCriticalAtRiskApplicationCount;
|
||||||
|
this.atRiskPasswordsCount = atRiskPasswordsCount;
|
||||||
|
this.completedTasks = taskMetrics.completedTasks;
|
||||||
|
this.totalTasks = taskMetrics.totalTasks;
|
||||||
|
this.allApplicationsDetails = allApplicationsDetails;
|
||||||
|
|
||||||
|
// No critical apps setup
|
||||||
|
this.renderMode =
|
||||||
|
summary.totalCriticalApplicationCount === 0 ? RenderMode.noCriticalApps : this.renderMode;
|
||||||
|
|
||||||
|
// Critical apps setup with at-risk apps but no tasks
|
||||||
|
this.renderMode =
|
||||||
|
summary.totalCriticalApplicationCount > 0 &&
|
||||||
|
summary.totalCriticalAtRiskApplicationCount >= 0 &&
|
||||||
|
taskMetrics.totalTasks === 0
|
||||||
|
? RenderMode.criticalAppsWithAtRiskAppsAndNoTasks
|
||||||
|
: this.renderMode;
|
||||||
|
|
||||||
|
// Critical apps setup with at-risk apps and tasks
|
||||||
|
this.renderMode =
|
||||||
|
summary.totalAtRiskApplicationCount > 0 &&
|
||||||
|
summary.totalCriticalAtRiskApplicationCount >= 0 &&
|
||||||
|
taskMetrics.totalTasks > 0
|
||||||
|
? RenderMode.criticalAppsWithAtRiskAppsAndTasks
|
||||||
|
: this.renderMode;
|
||||||
|
|
||||||
|
this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar(
|
||||||
|
this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private securityTasksApiService: SecurityTasksApiService,
|
||||||
|
private allActivitiesService: AllActivitiesService,
|
||||||
|
private adminTaskService: DefaultAdminTaskService,
|
||||||
|
protected toastService: ToastService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get completedPercent(): number {
|
||||||
|
if (this.totalTasks === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.round((this.completedTasks / this.totalTasks) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
get completedTasksCount(): number {
|
||||||
|
switch (this.renderMode) {
|
||||||
|
case RenderMode.noCriticalApps:
|
||||||
|
case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks:
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case RenderMode.criticalAppsWithAtRiskAppsAndTasks:
|
||||||
|
return this.completedTasks;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalTasksCount(): number {
|
||||||
|
switch (this.renderMode) {
|
||||||
|
case RenderMode.noCriticalApps:
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks:
|
||||||
|
return this.atRiskAppsCount;
|
||||||
|
|
||||||
|
case RenderMode.criticalAppsWithAtRiskAppsAndTasks:
|
||||||
|
return this.totalTasks;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get canAssignTasks(): boolean {
|
||||||
|
return this.atRiskAppsCount > this.totalTasks ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderModes() {
|
||||||
|
return RenderMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignTasks() {
|
||||||
|
const taskCount = await this.requestPasswordChange();
|
||||||
|
this.taskMetrics$.next({
|
||||||
|
totalTasks: this.totalTasks + taskCount,
|
||||||
|
completedTasks: this.completedTasks,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this method is shared between here and critical-applications.component.ts
|
||||||
|
async requestPasswordChange() {
|
||||||
|
const apps = this.allApplicationsDetails;
|
||||||
|
const cipherIds = apps
|
||||||
|
.filter((_) => _.atRiskPasswordCount > 0)
|
||||||
|
.flatMap((app) => app.atRiskCipherIds);
|
||||||
|
|
||||||
|
const distinctCipherIds = Array.from(new Set(cipherIds));
|
||||||
|
|
||||||
|
const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({
|
||||||
|
cipherId: cipherId as CipherId,
|
||||||
|
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks);
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("notifiedMembers"),
|
||||||
|
variant: "success",
|
||||||
|
title: this.i18nService.t("success"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return tasks.length;
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("unexpectedError"),
|
||||||
|
variant: "error",
|
||||||
|
title: this.i18nService.t("error"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (!(isLoading$ | async) && !(noData$ | async)) {
|
@if (!(isLoading$ | async) && !(noData$ | async)) {
|
||||||
<div class="tw-mt-4 tw-flex tw-flex-col">
|
<ul
|
||||||
<div class="tw-flex tw-gap-4 tw-col-span-6">
|
class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none"
|
||||||
|
>
|
||||||
|
<li class="tw-col-span-1" [ngClass]="{ 'tw-col-span-2': passwordChangeMetricHasProgressBar }">
|
||||||
|
<dirt-password-change-metric></dirt-password-change-metric>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tw-col-span-1">
|
||||||
<dirt-activity-card
|
<dirt-activity-card
|
||||||
class="tw-col-span-2 tw-cursor-pointer"
|
|
||||||
[title]="'atRiskMembers' | i18n"
|
[title]="'atRiskMembers' | i18n"
|
||||||
[cardMetrics]="'membersAtRiskCount' | i18n: totalCriticalAppsAtRiskMemberCount"
|
[cardMetrics]="'membersAtRiskCount' | i18n: totalCriticalAppsAtRiskMemberCount"
|
||||||
[metricDescription]="'membersAtRiskActivityDescription' | i18n"
|
[metricDescription]="'membersAtRiskActivityDescription' | i18n"
|
||||||
@@ -29,10 +34,11 @@
|
|||||||
[showNavigationLink]="totalCriticalAppsAtRiskMemberCount > 0"
|
[showNavigationLink]="totalCriticalAppsAtRiskMemberCount > 0"
|
||||||
>
|
>
|
||||||
</dirt-activity-card>
|
</dirt-activity-card>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="tw-col-span-1">
|
||||||
<dirt-activity-card
|
<dirt-activity-card
|
||||||
#allAppsOrgAtRiskApplications
|
#allAppsOrgAtRiskApplications
|
||||||
class="tw-col-span-2 tw-cursor-pointer"
|
|
||||||
[title]="'criticalApplications' | i18n"
|
[title]="'criticalApplications' | i18n"
|
||||||
[cardMetrics]="
|
[cardMetrics]="
|
||||||
totalCriticalAppsCount === 0
|
totalCriticalAppsCount === 0
|
||||||
@@ -50,6 +56,6 @@
|
|||||||
[showNavigationLink]="totalCriticalAppsAtRiskCount > 0"
|
[showNavigationLink]="totalCriticalAppsAtRiskCount > 0"
|
||||||
>
|
>
|
||||||
</dirt-activity-card>
|
</dirt-activity-card>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ 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 { ActivityCardComponent } from "./activity-card.component";
|
||||||
|
import { PasswordChangeMetricComponent } from "./activity-cards/password-change-metric.component";
|
||||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "tools-all-activity",
|
selector: "tools-all-activity",
|
||||||
imports: [ApplicationsLoadingComponent, SharedModule, ActivityCardComponent],
|
imports: [
|
||||||
|
ApplicationsLoadingComponent,
|
||||||
|
SharedModule,
|
||||||
|
ActivityCardComponent,
|
||||||
|
PasswordChangeMetricComponent,
|
||||||
|
],
|
||||||
templateUrl: "./all-activity.component.html",
|
templateUrl: "./all-activity.component.html",
|
||||||
})
|
})
|
||||||
export class AllActivityComponent implements OnInit {
|
export class AllActivityComponent implements OnInit {
|
||||||
@@ -30,6 +36,7 @@ export class AllActivityComponent implements OnInit {
|
|||||||
totalCriticalAppsAtRiskMemberCount = 0;
|
totalCriticalAppsAtRiskMemberCount = 0;
|
||||||
totalCriticalAppsCount = 0;
|
totalCriticalAppsCount = 0;
|
||||||
totalCriticalAppsAtRiskCount = 0;
|
totalCriticalAppsAtRiskCount = 0;
|
||||||
|
passwordChangeMetricHasProgressBar = false;
|
||||||
|
|
||||||
destroyRef = inject(DestroyRef);
|
destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
@@ -51,6 +58,12 @@ export class AllActivityComponent implements OnInit {
|
|||||||
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
|
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
|
||||||
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
|
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((hasProgressBar) => {
|
||||||
|
this.passwordChangeMetricHasProgressBar = hasProgressBar;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export class CriticalApplicationsComponent implements OnInit {
|
|||||||
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);
|
this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary);
|
||||||
|
this.allActivitiesService.setAllAppsReportDetails(applications);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user