diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.html b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.html index d92840c2de6..d7c8c528bb2 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.html @@ -108,42 +108,7 @@ -
- - @if (cipher.weakPassword === true) { - W - } - - @if (cipher.reusedPassword === true) { - R - } - - @if (cipher.exposedPassword === true) { - E - } - - @if ( - cipher.status === RiskInsightsItemStatus.Healthy && - !cipher.weakPassword && - !cipher.reusedPassword && - !cipher.exposedPassword - ) { - - } - - @if (cipher.status === null) { - - } -
+ @@ -179,6 +144,15 @@ } + } @else if (processingPhase() === ProcessingPhase.Error) { + +
+ +

{{ "errorOccurred" | i18n }}

+ @if (error()) { +

{{ error() }}

+ } +
} @else if (processingPhase() === ProcessingPhase.Idle) {
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.ts index f67e641cb34..4de2494fa3f 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/applications/risk-insights-prototype-applications.component.ts @@ -20,6 +20,8 @@ import { import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components"; /* eslint-enable no-restricted-imports */ +import { CipherHealthBadgesComponent } from "../shared/cipher-health-badges.component"; + /** * Applications tab component for the Risk Insights Prototype. * @@ -33,7 +35,7 @@ import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components selector: "app-risk-insights-prototype-applications", templateUrl: "./risk-insights-prototype-applications.component.html", standalone: true, - imports: [CommonModule, JslibModule, TableModule, BadgeModule], + imports: [CommonModule, JslibModule, TableModule, BadgeModule, CipherHealthBadgesComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class RiskInsightsPrototypeApplicationsComponent { @@ -53,6 +55,7 @@ export class RiskInsightsPrototypeApplicationsComponent { // Processing state readonly processingPhase = this.orchestrator.processingPhase; + readonly error = this.orchestrator.error; // Results readonly applications = this.orchestrator.applications; diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.html b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.html index d7a5eafb006..a814387bafa 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.html @@ -116,6 +116,15 @@ + } @else if (processingPhase() === ProcessingPhase.Error) { + +
+ +

{{ "errorOccurred" | i18n }}

+ @if (error()) { +

{{ error() }}

+ } +
} @else if (processingPhase() === ProcessingPhase.Idle) {
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts index 072e11858a4..b440aaa1c0a 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/items/risk-insights-prototype-items.component.ts @@ -42,6 +42,7 @@ export class RiskInsightsPrototypeItemsComponent { // Processing state readonly processingPhase = this.orchestrator.processingPhase; + readonly error = this.orchestrator.error; // Results readonly items = this.orchestrator.items; diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html index 93cc34613b0..d878ed24c3d 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.html @@ -105,42 +105,7 @@ -
- - @if (cipher.weakPassword === true) { - W - } - - @if (cipher.reusedPassword === true) { - R - } - - @if (cipher.exposedPassword === true) { - E - } - - @if ( - cipher.status === RiskInsightsItemStatus.Healthy && - !cipher.weakPassword && - !cipher.reusedPassword && - !cipher.exposedPassword - ) { - - } - - @if (cipher.status === null) { - - } -
+ @@ -163,6 +128,15 @@ } + } @else if (processingPhase() === ProcessingPhase.Error) { + +
+ +

{{ "errorOccurred" | i18n }}

+ @if (error()) { +

{{ error() }}

+ } +
} @else if (processingPhase() === ProcessingPhase.Idle) {
diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts index d1223890e8a..f1253d035e3 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/members/risk-insights-prototype-members.component.ts @@ -20,6 +20,8 @@ import { import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components"; /* eslint-enable no-restricted-imports */ +import { CipherHealthBadgesComponent } from "../shared/cipher-health-badges.component"; + /** * Members tab component for the Risk Insights Prototype. * @@ -33,7 +35,7 @@ import { BadgeModule, TableDataSource, TableModule } from "@bitwarden/components selector: "app-risk-insights-prototype-members", templateUrl: "./risk-insights-prototype-members.component.html", standalone: true, - imports: [CommonModule, JslibModule, TableModule, BadgeModule], + imports: [CommonModule, JslibModule, TableModule, BadgeModule, CipherHealthBadgesComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class RiskInsightsPrototypeMembersComponent { @@ -49,6 +51,7 @@ export class RiskInsightsPrototypeMembersComponent { // Processing state readonly processingPhase = this.orchestrator.processingPhase; readonly memberProgress = this.orchestrator.memberProgress; + readonly error = this.orchestrator.error; // Results readonly members = this.orchestrator.members; @@ -77,9 +80,6 @@ export class RiskInsightsPrototypeMembersComponent { return new Map(items.map((item) => [item.cipherId, item])); }); - /** Whether we've requested member data to be built (lazy loading trigger) */ - private hasRequestedMemberData = false; - // ============================================================================ // Lifecycle // ============================================================================ @@ -92,12 +92,14 @@ export class RiskInsightsPrototypeMembersComponent { }); // Effect to trigger lazy loading of member aggregations when phase is ready + // Check actual state (members array length) instead of a local flag to handle + // component re-creation and state resets correctly effect(() => { const phase = this.processingPhase(); const isReady = phase === ProcessingPhase.Complete || phase === ProcessingPhase.RunningHibp; + const membersEmpty = this.members().length === 0; - if (isReady && !this.hasRequestedMemberData) { - this.hasRequestedMemberData = true; + if (isReady && membersEmpty) { this.orchestrator.ensureMemberAggregationsBuilt(); } }); diff --git a/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts new file mode 100644 index 00000000000..9bf89bebb26 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/reporting/risk-insights-prototype/shared/cipher-health-badges.component.ts @@ -0,0 +1,74 @@ +/* eslint-disable no-restricted-imports -- Prototype feature using licensed services */ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + RiskInsightsItem, + RiskInsightsItemStatus, +} from "@bitwarden/common/dirt/reports/risk-insights"; +import { BadgeModule } from "@bitwarden/components"; +/* eslint-enable no-restricted-imports */ + +/** + * Shared component for displaying cipher health badges (Weak/Reused/Exposed indicators). + * + * Used in the Applications and Members tabs' expanded row views to display + * consistent health status badges for individual ciphers. + * + * Displays: + * - W badge (warning) for weak passwords + * - R badge (warning) for reused passwords + * - E badge (danger) for exposed passwords + * - Check icon for healthy items + * - Spinner for items still loading + */ +@Component({ + selector: "app-cipher-health-badges", + standalone: true, + imports: [CommonModule, JslibModule, BadgeModule], + template: ` +
+ @if (cipher().weakPassword === true) { + W + } + @if (cipher().reusedPassword === true) { + R + } + @if (cipher().exposedPassword === true) { + E + } + @if (isHealthy()) { + + } + @if (cipher().status === null) { + + } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CipherHealthBadgesComponent { + /** The cipher item to display health badges for */ + readonly cipher = input.required(); + + /** Computed signal to determine if the cipher is healthy with no issues */ + protected readonly isHealthy = computed(() => { + const c = this.cipher(); + return ( + c.status === RiskInsightsItemStatus.Healthy && + !c.weakPassword && + !c.reusedPassword && + !c.exposedPassword + ); + }); +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts index 9e96c155f76..95e5b83aa20 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-prototype-orchestration.service.ts @@ -4,6 +4,7 @@ import { from, Observable, of } from "rxjs"; import { catchError, switchMap, tap, last, map } from "rxjs/operators"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { 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"; @@ -48,6 +49,7 @@ export class RiskInsightsPrototypeOrchestrationService { private readonly cipherAccessMappingService = inject(CipherAccessMappingService); private readonly passwordHealthService = inject(PasswordHealthService); private readonly riskInsightsService = inject(RiskInsightsPrototypeService); + private readonly logService = inject(LogService); private readonly destroyRef = inject(DestroyRef); // ============================================================================ @@ -466,7 +468,8 @@ export class RiskInsightsPrototypeOrchestrationService { }), last(), map((): void => undefined), - catchError(() => { + catchError((err: unknown) => { + this.logService.error("[RiskInsightsPrototype] Error loading member counts:", err); return of(undefined); }), );