1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-27739] Fix bug for icons not showing on application tables (#17373)

* Added function to get a cipher icon for application tables. Update all application component to use signal properties

* Fix type

* Handle no ciphers on application
This commit is contained in:
Leslie Tilton
2025-11-18 09:03:05 -06:00
committed by GitHub
parent bd2f6e7566
commit 20d44b5136
7 changed files with 117 additions and 54 deletions

View File

@@ -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
*

View File

@@ -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);
}

View File

@@ -22,10 +22,10 @@
bitTypography="h3"
class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskMembersLabel"
>{{ applicationSummary.totalAtRiskMemberCount }}</span
>{{ applicationSummary().totalAtRiskMemberCount }}</span
>
<span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary.totalMemberCount
"cardMetrics" | i18n: applicationSummary().totalMemberCount
}}</span>
</div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
@@ -62,10 +62,10 @@
bitTypography="h3"
class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskApplicationsLabel"
>{{ applicationSummary.totalAtRiskApplicationCount }}</span
>{{ applicationSummary().totalAtRiskApplicationCount }}</span
>
<span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary.totalApplicationCount
"cardMetrics" | i18n: applicationSummary().totalApplicationCount
}}</span>
</div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
@@ -95,11 +95,11 @@
type="button"
[buttonType]="'primary'"
bitButton
[disabled]="!selectedUrls.size"
[loading]="markingAsCritical"
[disabled]="!selectedUrls().size"
[loading]="markingAsCritical()"
(click)="markAppsAsCritical()"
>
<i class="bwi tw-mr-2" [ngClass]="selectedUrls.size ? 'bwi-star-f' : 'bwi-star'"></i>
<i class="bwi tw-mr-2" [ngClass]="selectedUrls().size ? 'bwi-star-f' : 'bwi-star'"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
@@ -108,7 +108,7 @@
[dataSource]="dataSource"
[showRowCheckBox]="true"
[showRowMenuForCriticalApps]="false"
[selectedUrls]="selectedUrls"
[selectedUrls]="selectedUrls()"
[openApplication]="drawerDetails.invokerId || ''"
[checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers"

View File

@@ -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<ApplicationHealthReportDetailEnriched>();
protected selectedUrls: Set<string> = new Set<string>();
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<ApplicationTableDataSource>();
protected readonly searchControl = new FormControl("", { nonNullable: true });
// Template driven properties
protected readonly selectedUrls = signal(new Set<string>());
protected readonly markingAsCritical = signal(false);
protected readonly applicationSummary = signal<OrganizationReportSummary>(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<string>());
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;
});
}
};
}

View File

@@ -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<ApplicationHealthReportDetailEnriched>();
protected dataSource = new TableDataSource<ApplicationTableDataSource>();
protected applicationSummary = {} as OrganizationReportSummary;
protected selectedIds: Set<number> = new Set<number>();
@@ -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 = [];

View File

@@ -46,10 +46,7 @@
[attr.aria-label]="'viewItem' | i18n"
>
<!-- Passing the first cipher of the application for app-vault-icon cipher input requirement -->
<app-vault-icon
*ngIf="row.cipherIds.length > 0"
[cipher]="row.cipherIds[0]"
></app-vault-icon>
<app-vault-icon *ngIf="row.iconCipher" [cipher]="row.iconCipher"></app-vault-icon>
</td>
<td
class="tw-cursor-pointer"

View File

@@ -3,10 +3,15 @@ import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApplicationHealthReportDetailEnriched } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
export type ApplicationTableDataSource = ApplicationHealthReportDetailEnriched & {
iconCipher: CipherViewLike | undefined;
};
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -18,7 +23,7 @@ export class AppTableRowScrollableComponent {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input()
dataSource!: TableDataSource<ApplicationHealthReportDetailEnriched>;
dataSource!: TableDataSource<ApplicationTableDataSource>;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() showRowMenuForCriticalApps: boolean = false;