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

[PM-27933] Skip assign tasks view if no critical applications are selected (#17351)

* Fix reviews not saving in new applications review. Skip assign page if no at risk passwords are to be assigned. Fix bug in password change widget

* Claude comment improvements
This commit is contained in:
Leslie Tilton
2025-11-13 12:18:52 -06:00
committed by GitHub
parent 9c2da604b1
commit 37c1a5ee17
5 changed files with 110 additions and 52 deletions

View File

@@ -12161,5 +12161,11 @@
"example": "5"
}
}
},
"confirmNoSelectedCriticalApplicationsTitle": {
"message": "No critical applications are selected"
},
"confirmNoSelectedCriticalApplicationsDesc": {
"message": "Are you sure you want to continue?"
}
}

View File

@@ -43,7 +43,7 @@
</div>
<div class="tw-items-baseline tw-gap-2">
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: atRiskPasswordCount() }}</span>
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: unassignedCipherIds() }}</span>
</div>
<div class="tw-mt-4">

View File

@@ -66,16 +66,13 @@ export class PasswordChangeMetricComponent implements OnInit {
readonly completedTasksCount = computed(
() => this._tasks().filter((task) => task.status === SecurityTaskStatus.Completed).length,
);
readonly uncompletedTasksCount = computed(
() => this._tasks().filter((task) => task.status == SecurityTaskStatus.Pending).length,
);
readonly completedTasksPercent = computed(() => {
const total = this.tasksCount();
// Account for case where there are no tasks to avoid NaN
return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0;
});
readonly atRiskPasswordCount = computed<number>(() => {
readonly unassignedCipherIds = computed<number>(() => {
const atRiskIds = this._atRiskCipherIds();
const tasks = this._tasks();
@@ -83,12 +80,20 @@ export class PasswordChangeMetricComponent implements OnInit {
return atRiskIds.length;
}
const assignedIdSet = new Set(tasks.map((task) => task.cipherId));
const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
const unassignedIds = atRiskIds.filter((id) => !assignedIdSet.has(id));
return unassignedIds.length;
});
readonly atRiskPasswordCount = computed<number>(() => {
const atRiskIds = this._atRiskCipherIds();
const atRiskIdsSet = new Set(atRiskIds);
return atRiskIdsSet.size;
});
readonly currentView = computed<PasswordChangeView>(() => {
if (!this._hasCriticalApplications()) {
return PasswordChangeView.EMPTY;
@@ -96,7 +101,7 @@ export class PasswordChangeMetricComponent implements OnInit {
if (this.tasksCount() === 0) {
return PasswordChangeView.NO_TASKS_ASSIGNED;
}
if (this.atRiskPasswordCount() > 0) {
if (this.unassignedCipherIds() > 0) {
return PasswordChangeView.NEW_TASKS_AVAILABLE;
}
return PasswordChangeView.PROGRESS;

View File

@@ -38,8 +38,8 @@
@if (currentView() === DialogView.AssignTasks) {
<dirt-assign-tasks-view
[criticalApplicationsCount]="atRiskCriticalApplicationsCount()"
[totalApplicationsCount]="totalCriticalApplicationsCount()"
[criticalApplicationsCount]="newAtRiskCriticalApplications().length"
[totalApplicationsCount]="newCriticalApplications().length"
[atRiskCriticalMembersCount]="atRiskCriticalMembersCount()"
>
</dirt-assign-tasks-view>

View File

@@ -2,12 +2,15 @@ import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
Inject,
inject,
Injector,
Signal,
signal,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { from, switchMap, take } from "rxjs";
import {
@@ -17,7 +20,8 @@ import {
import { getUniqueMembers } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
import { SecurityTask, SecurityTaskStatus } from "@bitwarden/common/vault/tasks";
import {
ButtonModule,
DIALOG_DATA,
@@ -70,9 +74,9 @@ export type NewApplicationsDialogResultType =
(typeof NewApplicationsDialogResultType)[keyof typeof NewApplicationsDialogResultType];
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-new-applications-dialog",
templateUrl: "./new-applications-dialog.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
ButtonModule,
@@ -95,10 +99,41 @@ export class NewApplicationsDialogComponent {
// Applications selected to save as critical applications
protected readonly selectedApplications = signal<Set<string>>(new Set());
// Assign tasks variables
readonly atRiskCriticalApplicationsCount = signal<number>(0);
readonly totalCriticalApplicationsCount = signal<number>(0);
readonly atRiskCriticalMembersCount = signal<number>(0);
// Used to determine if there are unassigned at-risk cipher IDs
private readonly _tasks!: Signal<SecurityTask[]>;
// Computed properties for selected applications
protected readonly newCriticalApplications = computed(() => {
return this.dialogParams.newApplications.filter((newApp) =>
this.selectedApplications().has(newApp.applicationName),
);
});
// New at risk critical applications
protected readonly newAtRiskCriticalApplications = computed(() => {
return this.newCriticalApplications().filter((app) => app.atRiskPasswordCount > 0);
});
// Count of unique members with at-risk passwords in newly marked critical applications
protected readonly atRiskCriticalMembersCount = computed(() => {
return getUniqueMembers(this.newCriticalApplications().flatMap((x) => x.atRiskMemberDetails))
.length;
});
protected readonly newUnassignedAtRiskCipherIds = computed<CipherId[]>(() => {
const newAtRiskCipherIds = this.newCriticalApplications().flatMap((app) => app.atRiskCipherIds);
const tasks = this._tasks();
if (tasks.length === 0) {
return newAtRiskCipherIds;
}
const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
const unassignedIds = newAtRiskCipherIds.filter((id) => !assignedIdSet.has(id));
return unassignedIds;
});
readonly saving = signal<boolean>(false);
// Loading states
@@ -106,13 +141,21 @@ export class NewApplicationsDialogComponent {
constructor(
@Inject(DIALOG_DATA) protected dialogParams: NewApplicationsDialogData,
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
private dataService: RiskInsightsDataService,
private toastService: ToastService,
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
private dialogService: DialogService,
private i18nService: I18nService,
private accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
private injector: Injector,
private logService: LogService,
) {}
private securityTasksService: AccessIntelligenceSecurityTasksService,
private toastService: ToastService,
) {
// Setup the _tasks signal by manually passing in the injector
this._tasks = toSignal(this.securityTasksService.tasks$, {
initialValue: [],
injector: this.injector,
});
}
/**
* Opens the new applications dialog
@@ -170,53 +213,57 @@ export class NewApplicationsDialogComponent {
});
}
handleMarkAsCritical() {
if (this.markingAsCritical() || this.saving()) {
return; // Prevent action if already processing
// Checks if there are selected applications and proceeds to assign tasks
async handleMarkAsCritical() {
if (this.selectedApplications().size === 0) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "confirmNoSelectedCriticalApplicationsTitle" },
content: { key: "confirmNoSelectedCriticalApplicationsDesc" },
type: "warning",
});
if (!confirmed) {
return;
}
}
this.markingAsCritical.set(true);
const onlyNewCriticalApplications = this.dialogParams.newApplications.filter((newApp) =>
this.selectedApplications().has(newApp.applicationName),
);
// Count only critical applications that have at-risk passwords
const atRiskCriticalApplicationsCount = onlyNewCriticalApplications.filter(
(app) => app.atRiskPasswordCount > 0,
).length;
this.atRiskCriticalApplicationsCount.set(atRiskCriticalApplicationsCount);
// Total number of selected critical applications
this.totalCriticalApplicationsCount.set(onlyNewCriticalApplications.length);
const atRiskCriticalMembersCount = getUniqueMembers(
onlyNewCriticalApplications.flatMap((x) => x.atRiskMemberDetails),
).length;
this.atRiskCriticalMembersCount.set(atRiskCriticalMembersCount);
// Skip the assign tasks view if there are no new unassigned at-risk cipher IDs
if (this.newUnassignedAtRiskCipherIds().length === 0) {
this.handleAssignTasks();
} else {
this.currentView.set(DialogView.AssignTasks);
this.markingAsCritical.set(false);
}
}
/**
* Handles the assign tasks button click
*/
// Saves the application review and assigns tasks for unassigned at-risk ciphers
protected handleAssignTasks() {
if (this.saving()) {
return; // Prevent double-click
}
this.saving.set(true);
const reviewedDate = new Date();
const updatedApplications = this.dialogParams.newApplications.map((app) => {
const isCritical = this.selectedApplications().has(app.applicationName);
return {
applicationName: app.applicationName,
isCritical,
reviewedDate,
};
});
// Save the application review dates and critical markings
this.dataService.criticalApplicationAtRiskCipherIds$
this.dataService
.saveApplicationReviewStatus(updatedApplications)
.pipe(
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
take(1), // Handle unsubscribe for one off operation
switchMap((criticalApplicationAtRiskCipherIds) => {
take(1),
switchMap(() => {
// Assign password change tasks for unassigned at-risk ciphers for critical applications
return from(
this.accessIntelligenceSecurityTasksService.requestPasswordChangeForCriticalApplications(
this.securityTasksService.requestPasswordChangeForCriticalApplications(
this.dialogParams.organizationId,
criticalApplicationAtRiskCipherIds,
this.newUnassignedAtRiskCipherIds(),
),
);
}),