1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-26 21:23:34 +00:00
Files
browser/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts
Alex c6d759650b [PM-27619] assign tasks component (#17125)
* feat(dirt): add newApplications$ observable to orchestrator

Add reactive observable that filters applicationData for unreviewed apps
(reviewedDate === null). Observable automatically updates when report
state changes through the pipeline.

- Add newApplications$ observable with distinctUntilChanged
- Filters rawReportData$.data.applicationData
- Uses shareReplay for multi-subscriber efficiency

Related to PM-27284

* feat(dirt): add saveApplicationReviewStatus$ to orchestrator

Implement method to save application review status and critical flags.
Updates all applications where reviewedDate === null to set current date,
and marks selected applications as critical.

- Add saveApplicationReviewStatus$() method
- Add _updateReviewStatusAndCriticalFlags() helper
- Uses existing encryption and API update patterns
- Single API call for both review status and critical flags
- Follows same pattern as saveCriticalApplications$()

Related to PM-27284

* feat(dirt): expose newApplications$ in data service

Expose orchestrator's newApplications$ observable and save method
through RiskInsightsDataService facade. Maintains clean separation
between orchestrator (business logic) and components (UI).

- Expose newApplications$ observable
- Expose saveApplicationReviewStatus() delegation method
- Maintains facade pattern consistency

Related to PM-27284

* feat(dirt): make AllActivitiesService reactive to new applications

Update AllActivitiesService to subscribe to orchestrator's newApplications$
observable instead of receiving data through summary updates.

- Subscribe to dataService.newApplications$ in constructor
- Add setNewApplications() helper method
- Remove newApplications update from setAllAppsReportSummary()
- New applications now update reactively when review status changes

Related to PM-27284

* feat(dirt): connect dialog to review status save method

Update NewApplicationsDialogComponent to call the data service's
saveApplicationReviewStatus method when marking applications as critical.

- Inject RiskInsightsDataService
- Replace placeholder onMarkAsCritical() with real implementation
- Handle success/error cases with appropriate toast notifications
- Close dialog on successful save
- Show different messages based on whether apps were marked critical

Related to PM-27284

* feat(dirt): add i18n strings for application review

Add internationalization strings for the new applications review dialog
success and error messages.

- applicationReviewSaved: Success toast title
- applicationsMarkedAsCritical: Success message when apps marked critical
- newApplicationsReviewed: Success message when apps reviewed only
- errorSavingReviewStatus: Error toast title
- pleaseTryAgain: Error toast message

Related to PM-27284

* fix(dirt): add subscription cleanup to AllActivitiesService

Critical fix for production code quality and memory leak prevention.
Adds takeUntil pattern to all subscriptions to comply with ADR-0003
(Observable Data Services) requirements.

**Subscription Cleanup (ADR-0003 Compliance):**
- Add takeUntil pattern to AllActivitiesService subscriptions
- Add _destroy$ Subject and destroy() method
- Prevents memory leaks by properly unsubscribing from observables
- Follows Observable Data Services ADR requirements

Changes:
- Import Subject and takeUntil from rxjs
- Add private _destroy$ Subject for cleanup coordination
- Apply takeUntil(this._destroy$) to all 3 subscriptions:
  - enrichedReportData$ subscription
  - criticalReportResults$ subscription
  - newApplications$ subscription
- Add destroy() method for proper resource cleanup

This ensures proper resource cleanup and follows Bitwarden's
architectural decision records for observable management.

Related to PM-27284

* fix(dirt): replace manual takeUntil with takeUntilDestroyed in AllActivitiesService

Fixes critical memory leak by replacing manual subscription cleanup
with Angular's automatic DestroyRef-based cleanup pattern.

**Changes:**
- Replace `takeUntil(this._destroy$)` with `takeUntilDestroyed()` for all 3 subscriptions
- Remove unused `_destroy$` Subject and manual `destroy()` method
- Update imports to use `@angular/core/rxjs-interop`

**Why:**
- Manual `destroy()` method was never called anywhere in codebase
- Subscriptions accumulated without cleanup, causing memory leaks
- `takeUntilDestroyed()` uses Angular's DestroyRef for automatic cleanup
- Aligns with ADR-0003 and .claude/CLAUDE.md requirements

**Impact:**
- Automatic subscription cleanup when service context is destroyed
- Prevents memory leaks during hot module reloads and route changes
- Reduces code complexity (no manual lifecycle management needed)

Related to PM-27284

* refactor(dirt): remove newApplications from OrganizationReportSummary

Removes redundant newApplications field from summary type and uses
derived newApplications$ observable from orchestrator instead.

**Changes:**
- Remove newApplications from OrganizationReportSummary type definition
- Remove dummy data array from RiskInsightsReportService.getApplicationsSummary()
- Remove newApplications subscription from AllActivitiesService
- Update AllActivityComponent to subscribe directly to dataService.newApplications$

**Why:**
- Eliminates data redundancy (stored vs derived)
- newApplications$ already computes from applicationData.reviewedDate === null
- Single source of truth: applicationData is the source
- Simplifies encrypted payload (less data in summary)
- Better separation: stored data (counts) vs computed data (lists)

**Impact:**
- No functional changes - UI continues to display new applications correctly
- Cleaner architecture with computed observable pattern

* cleanup

* fix(dirt): improve dialog type safety and error logging

Addresses critical PR review issues in NewApplicationsDialogComponent:

**Type Safety:**
- Replace unsafe type casting `(this as any).dialogRef` with proper DialogRef injection
- Inject DialogRef<boolean | undefined> using Angular's inject() function
- Ensures type safety and prevents runtime errors from missing dialogRef

**Error Handling:**
- Add LogService to dialog component
- Log errors with "[NewApplicationsDialog]" for debugging
- Maintain user-facing error toast while adding server-side logging

**Impact:**
- Eliminates TypeScript safety bypasses
- Improves production debugging capabilities
- Follows Angular dependency injection best practices

* fixing mock data and test cases for new apps

* 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

* refactor(dirt): add multi-view state management to new applications dialog

Add view state const object and properties to support toggling between
application selection and embedded assign tasks component.

- Add DialogView const object with SelectApplications and AssignTasks states (ADR-0025)
- Add DialogView type for type safety
- Add currentView property to track active view
- Import AssignTasksViewComponent for embedded use
- Add isCalculatingTasks loading state
- Inject AllActivitiesService and SecurityTasksApiService for task checking
- Implement OnInit with organizationId retrieval from route params
- Add proper subscription cleanup with takeUntilDestroyed (ADR-0003)
- Expose DialogView constants to template

Related to PM-27619

* feat(dirt): integrate assign tasks view into dialog

Implement logic to embed AssignTasksViewComponent within dialog and
handle communication via event bindings.

- Update onMarkAsCritical to check for tasks before closing dialog
- Add checkForTasksToAssign() method using SecurityTasksApiService
- Conditionally transition to AssignTasks view when tasks are available
- Add onTasksAssigned() handler to close dialog after successful assignment
- Add onBack() handler to navigate back to SelectApplications view
- Add loading state guard to prevent double-click on Mark as Critical button
- Only show success toast and close dialog if no tasks to assign

Related to PM-27619

* feat(dirt): add embedded assign tasks view to dialog template

Update dialog template to conditionally render embedded
AssignTasksViewComponent using @if directive.

- Add conditional rendering for SelectApplications and AssignTasks views
- Update dialog title dynamically based on currentView
- Embed dirt-assign-tasks-view component in AssignTasks view
- Pass selectedApplicationsCount via input binding
- Listen to tasksAssigned and back output events
- Show footer buttons only for SelectApplications view
- Add loading and disabled states to Mark as Critical button
- Change Cancel button to not auto-close (user must navigate)

Related to PM-27619

* feat(dirt): add i18n keys for assign tasks view

Add localized strings for embedded assign tasks view component.

* resolve organizationId and DI issues in assign tasks flow

- Pass organizationId via dialog data to prevent async race conditions
- Pass organizationId as input to AssignTasksViewComponent (embedded components can't access route params)
- Add DefaultAdminTaskService to component providers to fix NullInjectorError
- Remove unnecessary route subscription from embedded component
- Follow password-change-metric.component.ts pattern for consistency
- Add detailed comments explaining architectural decisions and bug fixes

* cleanup styling

* refactor(dirt): remove newApplications validation from OrganizationReportSummary type guard

Removes redundant newApplications field validation from the
OrganizationReportSummary type guard and related test cases.

**Changes:**
- Remove "newApplications" from allowed keys in isOrganizationReportSummary()
- Remove newApplications array validation logic
- Remove newApplications validation from validateOrganizationReportSummary()
- Remove 2 test cases for newApplications validation
- Remove newApplications field from 8 test data objects

**Rationale:**
The newApplications field was removed from OrganizationReportSummary type
definition because it's derived data that can be calculated from
applicationData (filtering where reviewedDate === null). The data is now
accessed via the reactive newApplications$ observable instead of being
stored redundantly in the summary object.

**Impact:**
- No functional changes - UI continues to display new applications via observable
- Type guard now correctly validates the actual OrganizationReportSummary structure
- Eliminates data redundancy and maintains single source of truth
- All 43 tests passing

* improve assign tasks view display

- Remove illustration/preview section (mailbox icon and prompt text)
- Show unique member count instead of calculated task count
- Use reportSummary.totalCriticalAtRiskMemberCount from AllActivitiesService
- Remove unused SecurityTasksApiService dependency
- Follow same pattern as all-activity.component.ts for consistency

* logic to fetch totals and new styling

* Fix review applications review view and assign view flow

* Fix null type checks

* refactor assign tasks dialog: use callout component, add video, fix OnPush, improve error handling

* Add columns, description, search, and bulk select to new applications dialog

* Add count placeholder for critical applications marked message

* Address claude comments

---------

Co-authored-by: Tom <ttalty@bitwarden.com>
Co-authored-by: Leslie Tilton <23057410+Banrion@users.noreply.github.com>
Co-authored-by: maxkpower <mpower@bitwarden.com>
2025-11-03 15:25:19 +00:00

167 lines
6.4 KiB
TypeScript

import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import {
AllActivitiesService,
ApplicationHealthReportDetail,
ReportStatus,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { getById } from "@bitwarden/common/platform/misc";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component";
import { ActivityCardComponent } from "./activity-card.component";
import { PasswordChangeMetricComponent } from "./activity-cards/password-change-metric.component";
import { NewApplicationsDialogComponent } from "./application-review-dialog/new-applications-dialog.component";
// 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-all-activity",
imports: [
ApplicationsLoadingComponent,
SharedModule,
ActivityCardComponent,
PasswordChangeMetricComponent,
],
templateUrl: "./all-activity.component.html",
})
export class AllActivityComponent implements OnInit {
organization: Organization | null = null;
totalCriticalAppsAtRiskMemberCount = 0;
totalCriticalAppsCount = 0;
totalCriticalAppsAtRiskCount = 0;
newApplicationsCount = 0;
newApplications: ApplicationHealthReportDetail[] = [];
passwordChangeMetricHasProgressBar = false;
allAppsHaveReviewDate = false;
isAllCaughtUp = false;
hasLoadedApplicationData = false;
destroyRef = inject(DestroyRef);
protected ReportStatusEnum = ReportStatus;
constructor(
private accountService: AccountService,
protected activatedRoute: ActivatedRoute,
protected allActivitiesService: AllActivitiesService,
protected dataService: RiskInsightsDataService,
private dialogService: DialogService,
protected organizationService: OrganizationService,
) {}
async ngOnInit(): Promise<void> {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (organizationId) {
this.organization =
(await firstValueFrom(
this.organizationService.organizations$(userId).pipe(getById(organizationId)),
)) ?? null;
this.allActivitiesService.reportSummary$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((summary) => {
this.totalCriticalAppsAtRiskMemberCount = summary.totalCriticalAtRiskMemberCount;
this.totalCriticalAppsCount = summary.totalCriticalApplicationCount;
this.totalCriticalAppsAtRiskCount = summary.totalCriticalAtRiskApplicationCount;
});
this.dataService.newApplications$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((newApps) => {
this.newApplications = newApps;
this.newApplicationsCount = newApps.length;
this.updateIsAllCaughtUp();
});
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((hasProgressBar) => {
this.passwordChangeMetricHasProgressBar = hasProgressBar;
});
this.dataService.enrichedReportData$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((enrichedData) => {
if (enrichedData?.applicationData && enrichedData.applicationData.length > 0) {
this.hasLoadedApplicationData = true;
// Check if all apps have a review date (not null and not undefined)
this.allAppsHaveReviewDate = enrichedData.applicationData.every(
(app) => app.reviewedDate !== null && app.reviewedDate !== undefined,
);
} else {
this.hasLoadedApplicationData = enrichedData !== null;
this.allAppsHaveReviewDate = false;
}
this.updateIsAllCaughtUp();
});
}
}
/**
* Updates the isAllCaughtUp flag based on current state.
* Only shows "All caught up!" when:
* - Data has been loaded (hasLoadedApplicationData is true)
* - No new applications need review
* - All apps have a review date
*/
private updateIsAllCaughtUp(): void {
this.isAllCaughtUp =
this.hasLoadedApplicationData &&
this.newApplicationsCount === 0 &&
this.allAppsHaveReviewDate;
}
/**
* Handles the review new applications button click.
* Opens a dialog showing the list of new applications that can be marked as critical.
*/
async onReviewNewApplications() {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
if (!organizationId) {
return;
}
// Pass organizationId via dialog data instead of having the dialog retrieve it from route.
// This ensures organizationId is immediately available when dialog opens, preventing
// timing issues where the dialog's checkForTasksToAssign() method runs before
// organizationId is populated via async route subscription.
const dialogRef = NewApplicationsDialogComponent.open(this.dialogService, {
newApplications: this.newApplications,
organizationId: organizationId as OrganizationId,
});
await lastValueFrom(dialogRef.closed);
}
/**
* Handles the "View at-risk members" link click.
* Opens the at-risk members drawer for critical applications only.
*/
async onViewAtRiskMembers() {
await this.dataService.setDrawerForCriticalAtRiskMembers("activityTabAtRiskMembers");
}
/**
* Handles the "View at-risk applications" link click.
* Opens the at-risk applications drawer for critical applications only.
*/
async onViewAtRiskApplications() {
await this.dataService.setDrawerForCriticalAtRiskApps("activityTabAtRiskApplications");
}
}