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:
@@ -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>
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user