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 { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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 { LogService } from "@bitwarden/logging";
import { import {
@@ -191,6 +192,23 @@ export class RiskInsightsOrchestratorService {
this._generateReportTriggerSubject.next(true); 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 * Initializes the service context for a specific organization
* *

View File

@@ -87,7 +87,9 @@ export class RiskInsightsDataService {
this._destroy$.complete(); this._destroy$.complete();
} }
// ----- UI-triggered methods (delegate to orchestrator) ----- getCipherIcon(cipherId: string) {
return this.orchestrator.getCipherIcon(cipherId);
}
initializeForOrganization(organizationId: OrganizationId) { initializeForOrganization(organizationId: OrganizationId) {
this.orchestrator.initializeForOrganization(organizationId); this.orchestrator.initializeForOrganization(organizationId);
} }

View File

@@ -22,10 +22,10 @@
bitTypography="h3" bitTypography="h3"
class="!tw-mb-0" class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskMembersLabel" aria-describedby="allAppsOrgAtRiskMembersLabel"
>{{ applicationSummary.totalAtRiskMemberCount }}</span >{{ applicationSummary().totalAtRiskMemberCount }}</span
> >
<span bitTypography="body2">{{ <span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary.totalMemberCount "cardMetrics" | i18n: applicationSummary().totalMemberCount
}}</span> }}</span>
</div> </div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2"> <div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
@@ -62,10 +62,10 @@
bitTypography="h3" bitTypography="h3"
class="!tw-mb-0" class="!tw-mb-0"
aria-describedby="allAppsOrgAtRiskApplicationsLabel" aria-describedby="allAppsOrgAtRiskApplicationsLabel"
>{{ applicationSummary.totalAtRiskApplicationCount }}</span >{{ applicationSummary().totalAtRiskApplicationCount }}</span
> >
<span bitTypography="body2">{{ <span bitTypography="body2">{{
"cardMetrics" | i18n: applicationSummary.totalApplicationCount "cardMetrics" | i18n: applicationSummary().totalApplicationCount
}}</span> }}</span>
</div> </div>
<div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2"> <div class="tw-flex tw-items-baseline tw-mt-1 tw-gap-2">
@@ -95,11 +95,11 @@
type="button" type="button"
[buttonType]="'primary'" [buttonType]="'primary'"
bitButton bitButton
[disabled]="!selectedUrls.size" [disabled]="!selectedUrls().size"
[loading]="markingAsCritical" [loading]="markingAsCritical()"
(click)="markAppsAsCritical()" (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 }} {{ "markAppAsCritical" | i18n }}
</button> </button>
</div> </div>
@@ -108,7 +108,7 @@
[dataSource]="dataSource" [dataSource]="dataSource"
[showRowCheckBox]="true" [showRowCheckBox]="true"
[showRowMenuForCriticalApps]="false" [showRowMenuForCriticalApps]="false"
[selectedUrls]="selectedUrls" [selectedUrls]="selectedUrls()"
[openApplication]="drawerDetails.invokerId || ''" [openApplication]="drawerDetails.invokerId || ''"
[checkboxChange]="onCheckboxChange" [checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers" [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 { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { debounceTime } from "rxjs"; import { debounceTime } from "rxjs";
import { Security } from "@bitwarden/assets/svg"; import { Security } from "@bitwarden/assets/svg";
import { import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
ApplicationHealthReportDetailEnriched,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import { import {
OrganizationReportSummary, OrganizationReportSummary,
ReportStatus, ReportStatus,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models"; } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { import {
IconButtonModule, 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 { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; 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"; 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({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-all-applications", selector: "dirt-all-applications",
templateUrl: "./all-applications.component.html", templateUrl: "./all-applications.component.html",
imports: [ imports: [
@@ -51,24 +56,25 @@ import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.co
], ],
}) })
export class AllApplicationsComponent implements OnInit { 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); 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( constructor(
protected i18nService: I18nService, protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute, protected activatedRoute: ActivatedRoute,
protected toastService: ToastService, protected toastService: ToastService,
protected dataService: RiskInsightsDataService, protected dataService: RiskInsightsDataService,
private router: Router,
// protected allActivitiesService: AllActivitiesService,
) { ) {
this.searchControl.valueChanges this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed()) .pipe(debounceTime(200), takeUntilDestroyed())
@@ -78,8 +84,21 @@ export class AllApplicationsComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (report) => { next: (report) => {
this.applicationSummary = report?.summaryData ?? createNewSummaryData(); if (report != null) {
this.dataSource.data = report?.reportData ?? []; 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: () => { error: () => {
this.dataSource.data = []; this.dataSource.data = [];
@@ -88,15 +107,15 @@ export class AllApplicationsComponent implements OnInit {
} }
isMarkedAsCriticalItem(applicationName: string) { isMarkedAsCriticalItem(applicationName: string) {
return this.selectedUrls.has(applicationName); return this.selectedUrls().has(applicationName);
} }
markAppsAsCritical = async () => { markAppsAsCritical = async () => {
this.markingAsCritical = true; this.markingAsCritical.set(true);
const count = this.selectedUrls.size; const count = this.selectedUrls().size;
this.dataService this.dataService
.saveCriticalApplications(Array.from(this.selectedUrls)) .saveCriticalApplications(Array.from(this.selectedUrls()))
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
@@ -105,8 +124,8 @@ export class AllApplicationsComponent implements OnInit {
title: "", title: "",
message: this.i18nService.t("criticalApplicationsMarkedSuccess", count.toString()), message: this.i18nService.t("criticalApplicationsMarkedSuccess", count.toString()),
}); });
this.selectedUrls.clear(); this.selectedUrls.set(new Set<string>());
this.markingAsCritical = false; this.markingAsCritical.set(false);
}, },
error: () => { error: () => {
this.toastService.showToast({ this.toastService.showToast({
@@ -125,9 +144,15 @@ export class AllApplicationsComponent implements OnInit {
onCheckboxChange = (applicationName: string, event: Event) => { onCheckboxChange = (applicationName: string, event: Event) => {
const isChecked = (event.target as HTMLInputElement).checked; const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) { if (isChecked) {
this.selectedUrls.add(applicationName); this.selectedUrls.update((selectedUrls) => {
selectedUrls.add(applicationName);
return selectedUrls;
});
} else { } 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 // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @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 { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; 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 { Security } from "@bitwarden/assets/svg";
import { import {
ApplicationHealthReportDetailEnriched,
CriticalAppsService, CriticalAppsService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService, 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 { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
import { RiskInsightsTabType } from "../models/risk-insights.models"; 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"; 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({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-critical-applications", selector: "dirt-critical-applications",
templateUrl: "./critical-applications.component.html", templateUrl: "./critical-applications.component.html",
imports: [ imports: [
@@ -55,7 +56,7 @@ export class CriticalApplicationsComponent implements OnInit {
protected organizationId: OrganizationId; protected organizationId: OrganizationId;
noItemsIcon = Security; noItemsIcon = Security;
protected dataSource = new TableDataSource<ApplicationHealthReportDetailEnriched>(); protected dataSource = new TableDataSource<ApplicationTableDataSource>();
protected applicationSummary = {} as OrganizationReportSummary; protected applicationSummary = {} as OrganizationReportSummary;
protected selectedIds: Set<number> = new Set<number>(); protected selectedIds: Set<number> = new Set<number>();
@@ -79,9 +80,24 @@ export class CriticalApplicationsComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (criticalReport) => { next: (criticalReport) => {
this.dataSource.data = criticalReport?.reportData ?? []; if (criticalReport != null) {
this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData(); // Map the report data to include the iconCipher for each application
this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0; 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: () => { error: () => {
this.dataSource.data = []; this.dataSource.data = [];

View File

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

View File

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