diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index 38e1237318..51d35570cd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -35,6 +35,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { LogService } from "@bitwarden/logging"; import { @@ -191,6 +192,23 @@ export class RiskInsightsOrchestratorService { this._generateReportTriggerSubject.next(true); } + /** + * Gets the cipher icon for a given cipher ID + * + * @param cipherId The ID of the cipher to get the icon for + * @returns A CipherViewLike if found, otherwise undefined + */ + getCipherIcon(cipherId: string): CipherViewLike | undefined { + const currentCiphers = this._ciphersSubject.value; + if (!currentCiphers) { + return undefined; + } + + const foundCipher = currentCiphers.find((c) => c.id === cipherId); + + return foundCipher; + } + /** * Initializes the service context for a specific organization * diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts index 7b9255ca82..d426a6b09c 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts @@ -87,7 +87,9 @@ export class RiskInsightsDataService { this._destroy$.complete(); } - // ----- UI-triggered methods (delegate to orchestrator) ----- + getCipherIcon(cipherId: string) { + return this.orchestrator.getCipherIcon(cipherId); + } initializeForOrganization(organizationId: OrganizationId) { this.orchestrator.initializeForOrganization(organizationId); } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.html index b1c2faa4f0..73ded40388 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.html @@ -22,10 +22,10 @@ bitTypography="h3" class="!tw-mb-0" aria-describedby="allAppsOrgAtRiskMembersLabel" - >{{ applicationSummary.totalAtRiskMemberCount }}{{ applicationSummary().totalAtRiskMemberCount }} {{ - "cardMetrics" | i18n: applicationSummary.totalMemberCount + "cardMetrics" | i18n: applicationSummary().totalMemberCount }}
@@ -62,10 +62,10 @@ bitTypography="h3" class="!tw-mb-0" aria-describedby="allAppsOrgAtRiskApplicationsLabel" - >{{ applicationSummary.totalAtRiskApplicationCount }}{{ applicationSummary().totalAtRiskApplicationCount }} {{ - "cardMetrics" | i18n: applicationSummary.totalApplicationCount + "cardMetrics" | i18n: applicationSummary().totalApplicationCount }}
@@ -95,11 +95,11 @@ type="button" [buttonType]="'primary'" bitButton - [disabled]="!selectedUrls.size" - [loading]="markingAsCritical" + [disabled]="!selectedUrls().size" + [loading]="markingAsCritical()" (click)="markAppsAsCritical()" > - + {{ "markAppAsCritical" | i18n }}
@@ -108,7 +108,7 @@ [dataSource]="dataSource" [showRowCheckBox]="true" [showRowMenuForCriticalApps]="false" - [selectedUrls]="selectedUrls" + [selectedUrls]="selectedUrls()" [openApplication]="drawerDetails.invokerId || ''" [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts index acad2901ba..3a9159ad68 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts @@ -1,20 +1,23 @@ -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { + Component, + DestroyRef, + inject, + OnInit, + ChangeDetectionStrategy, + signal, +} from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { debounceTime } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; -import { - ApplicationHealthReportDetailEnriched, - RiskInsightsDataService, -} from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { OrganizationReportSummary, ReportStatus, } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { IconButtonModule, @@ -29,12 +32,14 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; +import { + ApplicationTableDataSource, + AppTableRowScrollableComponent, +} from "../shared/app-table-row-scrollable.component"; import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.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({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "dirt-all-applications", templateUrl: "./all-applications.component.html", imports: [ @@ -51,24 +56,25 @@ import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.co ], }) export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); - protected selectedUrls: Set = new Set(); - protected searchControl = new FormControl("", { nonNullable: true }); - protected organization = new Organization(); - noItemsIcon = Security; - protected markingAsCritical = false; - protected applicationSummary: OrganizationReportSummary = createNewSummaryData(); - protected ReportStatusEnum = ReportStatus; - destroyRef = inject(DestroyRef); + protected ReportStatusEnum = ReportStatus; + protected noItemsIcon = Security; + + // Standard properties + protected readonly dataSource = new TableDataSource(); + protected readonly searchControl = new FormControl("", { nonNullable: true }); + + // Template driven properties + protected readonly selectedUrls = signal(new Set()); + protected readonly markingAsCritical = signal(false); + protected readonly applicationSummary = signal(createNewSummaryData()); + constructor( protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, protected dataService: RiskInsightsDataService, - private router: Router, - // protected allActivitiesService: AllActivitiesService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -78,8 +84,21 @@ export class AllApplicationsComponent implements OnInit { async ngOnInit() { this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (report) => { - this.applicationSummary = report?.summaryData ?? createNewSummaryData(); - this.dataSource.data = report?.reportData ?? []; + if (report != null) { + this.applicationSummary.set(report.summaryData); + + // Map the report data to include the iconCipher for each application + const tableDataWithIcon = report.reportData.map((app) => ({ + ...app, + iconCipher: + app.cipherIds.length > 0 + ? this.dataService.getCipherIcon(app.cipherIds[0]) + : undefined, + })); + this.dataSource.data = tableDataWithIcon; + } else { + this.dataSource.data = []; + } }, error: () => { this.dataSource.data = []; @@ -88,15 +107,15 @@ export class AllApplicationsComponent implements OnInit { } isMarkedAsCriticalItem(applicationName: string) { - return this.selectedUrls.has(applicationName); + return this.selectedUrls().has(applicationName); } markAppsAsCritical = async () => { - this.markingAsCritical = true; - const count = this.selectedUrls.size; + this.markingAsCritical.set(true); + const count = this.selectedUrls().size; this.dataService - .saveCriticalApplications(Array.from(this.selectedUrls)) + .saveCriticalApplications(Array.from(this.selectedUrls())) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { @@ -105,8 +124,8 @@ export class AllApplicationsComponent implements OnInit { title: "", message: this.i18nService.t("criticalApplicationsMarkedSuccess", count.toString()), }); - this.selectedUrls.clear(); - this.markingAsCritical = false; + this.selectedUrls.set(new Set()); + this.markingAsCritical.set(false); }, error: () => { this.toastService.showToast({ @@ -125,9 +144,15 @@ export class AllApplicationsComponent implements OnInit { onCheckboxChange = (applicationName: string, event: Event) => { const isChecked = (event.target as HTMLInputElement).checked; if (isChecked) { - this.selectedUrls.add(applicationName); + this.selectedUrls.update((selectedUrls) => { + selectedUrls.add(applicationName); + return selectedUrls; + }); } else { - this.selectedUrls.delete(applicationName); + this.selectedUrls.update((selectedUrls) => { + selectedUrls.delete(applicationName); + return selectedUrls; + }); } }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts index 1ea745929d..b61190df66 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; @@ -8,7 +8,6 @@ import { debounceTime, EMPTY, from, map, switchMap, take } from "rxjs"; import { Security } from "@bitwarden/assets/svg"; import { - ApplicationHealthReportDetailEnriched, CriticalAppsService, RiskInsightsDataService, RiskInsightsReportService, @@ -30,12 +29,14 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; import { RiskInsightsTabType } from "../models/risk-insights.models"; -import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; +import { + ApplicationTableDataSource, + AppTableRowScrollableComponent, +} from "../shared/app-table-row-scrollable.component"; import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "dirt-critical-applications", templateUrl: "./critical-applications.component.html", imports: [ @@ -55,7 +56,7 @@ export class CriticalApplicationsComponent implements OnInit { protected organizationId: OrganizationId; noItemsIcon = Security; - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource(); protected applicationSummary = {} as OrganizationReportSummary; protected selectedIds: Set = new Set(); @@ -79,9 +80,24 @@ export class CriticalApplicationsComponent implements OnInit { async ngOnInit() { this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ next: (criticalReport) => { - this.dataSource.data = criticalReport?.reportData ?? []; - this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData(); - this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0; + if (criticalReport != null) { + // Map the report data to include the iconCipher for each application + const tableDataWithIcon = criticalReport.reportData.map((app) => ({ + ...app, + iconCipher: + app.cipherIds.length > 0 + ? this.dataService.getCipherIcon(app.cipherIds[0]) + : undefined, + })); + this.dataSource.data = tableDataWithIcon; + + this.applicationSummary = criticalReport.summaryData; + this.enableRequestPasswordChange = criticalReport.summaryData.totalAtRiskMemberCount > 0; + } else { + this.dataSource.data = []; + this.applicationSummary = createNewSummaryData(); + this.enableRequestPasswordChange = false; + } }, error: () => { this.dataSource.data = []; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.html index 79af3869d9..edd90eaf97 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.html @@ -46,10 +46,7 @@ [attr.aria-label]="'viewItem' | i18n" > - + ; + dataSource!: TableDataSource; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showRowMenuForCriticalApps: boolean = false;