1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 03:03:26 +00:00

feat(dirt): create assign tasks view component

Create standalone view component for task assignment UI that can be
embedded within dialogs or other containers.

- Add AssignTasksViewComponent with signal-based inputs/outputs
- Use input.required<number>() for selectedApplicationsCount
- Use output<void>() for tasksAssigned and back events
- Implement task calculation using SecurityTasksApiService
- Add onAssignTasks() method with loading state and error handling
- Include task summary card UI matching password-change-metric style
- Add proper subscription cleanup with takeUntilDestroyed (ADR-0003)
- Buttons included in component template (not dialog footer)
- Component retrieves organizationId from route params

Related to PM-27619
This commit is contained in:
Alex
2025-10-29 23:24:48 -04:00
parent 6912be9be3
commit 5d37ede9f0
2 changed files with 206 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
<div class="tw-flex tw-flex-col tw-gap-4">
<!-- Task Summary Info Card (matches password-change-metric styling) -->
<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"
>
<div class="tw-mb-2">
<i class="bwi bwi-info-circle tw-text-muted tw-mr-2" aria-hidden="true"></i>
<span bitTypography="h6">{{ "taskSummary" | i18n }}</span>
</div>
<div class="tw-flex tw-items-baseline tw-gap-2 tw-mb-4">
<span bitTypography="h3">{{ selectedApplicationsCount() }}</span>
<span bitTypography="body2" class="tw-text-muted">
{{ "criticalApplicationsMarked" | i18n }}
</span>
</div>
<div class="tw-flex tw-items-baseline tw-gap-2">
<span bitTypography="h3">{{ totalTasksToAssign }}</span>
<span bitTypography="body2" class="tw-text-muted">
{{ "membersWithAtRiskPasswords" | i18n }}
</span>
</div>
</div>
<!-- Description Text -->
<div bitTypography="body2" class="tw-text-muted">
{{ "membersWillReceiveNotification" | i18n }}
</div>
<!-- Illustration/Preview (matches password change metric pattern) -->
<div class="tw-flex tw-justify-center tw-p-4 tw-bg-background-alt tw-rounded-lg">
<div class="tw-text-center">
<i class="bwi bwi-envelope tw-text-6xl tw-text-muted tw-mb-2" aria-hidden="true"></i>
<p bitTypography="body2" class="tw-text-muted">
{{ "reviewAtRiskLoginsPrompt" | i18n }}
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="tw-flex tw-gap-2">
<button
type="button"
bitButton
size="small"
buttonType="primary"
(click)="onAssignTasks()"
[disabled]="isAssigning"
[loading]="isAssigning"
[attr.aria-label]="'assignTasks' | i18n"
>
<i class="bwi bwi-envelope tw-mr-2" aria-hidden="true"></i>
{{ "assignTasks" | i18n }}
</button>
<button
type="button"
bitButton
size="small"
buttonType="secondary"
(click)="onBack()"
[disabled]="isAssigning"
[attr.aria-label]="'back' | i18n"
>
{{ "back" | i18n }}
</button>
</div>
</div>

View File

@@ -0,0 +1,138 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit, inject, input, output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { map, filter } from "rxjs/operators";
import {
AllActivitiesService,
SecurityTasksApiService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
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 { ButtonModule, ToastService, TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service";
/**
* Embedded component for displaying task assignment UI.
* Not a dialog - intended to be embedded within a parent dialog.
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "dirt-assign-tasks-view",
templateUrl: "./assign-tasks-view.component.html",
imports: [CommonModule, ButtonModule, TypographyModule, I18nPipe],
})
export class AssignTasksViewComponent implements OnInit {
/**
* Number of applications selected as critical
*/
readonly selectedApplicationsCount = input.required<number>();
/**
* Emitted when tasks have been successfully assigned
*/
readonly tasksAssigned = output<void>();
/**
* Emitted when user clicks Back button
*/
readonly back = output<void>();
protected totalTasksToAssign = 0;
protected isAssigning = false;
private destroyRef = inject(DestroyRef);
private allActivitiesService = inject(AllActivitiesService);
private securityTasksApiService = inject(SecurityTasksApiService);
private accessIntelligenceSecurityTasksService = inject(AccessIntelligenceSecurityTasksService);
private toastService = inject(ToastService);
private i18nService = inject(I18nService);
private logService = inject(LogService);
private activatedRoute = inject(ActivatedRoute);
private organizationId: OrganizationId = "" as OrganizationId;
async ngOnInit(): Promise<void> {
// Get organization ID from route params
this.activatedRoute.paramMap
.pipe(
map((params) => params.get("organizationId")),
filter((orgId): orgId is string => !!orgId),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((orgId) => {
this.organizationId = orgId as OrganizationId;
});
// Calculate tasks to assign
await this.calculateTasksToAssign();
}
/**
* Calculates the number of tasks that will be assigned
*/
private async calculateTasksToAssign(): Promise<void> {
try {
const taskMetrics = await firstValueFrom(
this.securityTasksApiService.getTaskMetrics(this.organizationId),
);
const atRiskPasswordsCount = await firstValueFrom(
this.allActivitiesService.atRiskPasswordsCount$,
);
const newTasksCount = atRiskPasswordsCount - taskMetrics.totalTasks;
this.totalTasksToAssign = newTasksCount > 0 ? newTasksCount : 0;
} catch (error) {
this.logService.error("[AssignTasksView] Failed to calculate tasks", error);
this.totalTasksToAssign = 0;
}
}
/**
* Handles the assign tasks button click
*/
protected onAssignTasks = async () => {
if (this.isAssigning) {
return; // Prevent double-click
}
this.isAssigning = true;
try {
// Get critical applications details
const allApplicationsDetails = await firstValueFrom(
this.allActivitiesService.allApplicationsDetails$,
);
// Filter to only critical apps
const criticalApps = allApplicationsDetails.filter((app) => app.isMarkedAsCritical);
// Assign tasks using the security tasks service
await this.accessIntelligenceSecurityTasksService.assignTasks(
this.organizationId,
criticalApps,
);
// Success toast is shown by the service
// Emit success event to parent
this.tasksAssigned.emit();
} catch (error) {
this.logService.error("[AssignTasksView] Failed to assign tasks", error);
// Error toast is shown by the service
this.isAssigning = false; // Re-enable button on error
}
};
/**
* Handles the back button click
*/
protected onBack = () => {
this.back.emit();
};
}