From 98622a3f73cb94d254655fec08d7e12a886a17e8 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 31 Oct 2025 15:46:04 -0400 Subject: [PATCH 1/3] remove unneeded rxjs filter (#17165) --- .../app/admin-console/organizations/manage/events.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 78a6d6c0dac..ba758266059 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, filter, firstValueFrom, lastValueFrom, map, switchMap, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, lastValueFrom, map, switchMap, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -143,7 +143,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe getUserId, switchMap((userId) => this.providerService.get$(this.organization.providerId, userId)), map((provider) => provider != null && provider.canManageUsers), - filter((result) => result), switchMap(() => this.apiService.getProviderUsers(this.organization.id)), map((providerUsersResponse) => providerUsersResponse.data.forEach((u) => { From 2da0b48c3dda5dfd167888d11789c043e7bd27f5 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 31 Oct 2025 16:35:59 -0400 Subject: [PATCH 2/3] [PM-27688] fix events page not loading (#17166) * remove unneeded rxjs filter * add check for canManage * add null check * fix provider ID, clean up --- .../organizations/manage/events.component.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index ba758266059..62f6539cc16 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, firstValueFrom, lastValueFrom, map, switchMap, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, tap } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -143,16 +143,23 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe getUserId, switchMap((userId) => this.providerService.get$(this.organization.providerId, userId)), map((provider) => provider != null && provider.canManageUsers), - switchMap(() => this.apiService.getProviderUsers(this.organization.id)), - map((providerUsersResponse) => - providerUsersResponse.data.forEach((u) => { - const name = this.userNamePipe.transform(u); - this.orgUsersUserIdMap.set(u.userId, { - name: `${name} (${this.organization.providerName})`, - email: u.email, + switchMap((canManage) => { + if (canManage) { + return this.apiService.getProviderUsers(this.organization.providerId); + } + return of(null); + }), + tap((providerUsersResponse) => { + if (providerUsersResponse) { + providerUsersResponse.data.forEach((u) => { + const name = this.userNamePipe.transform(u); + this.orgUsersUserIdMap.set(u.userId, { + name: `${name} (${this.organization.providerName})`, + email: u.email, + }); }); - }), - ), + } + }), ), ); } catch (e) { From 4d1c00a5bc3afd9427bcd85669c5c1a47a09c3d8 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:37:21 -0400 Subject: [PATCH 3/3] [PM-26941] all caught up state for review card (#17164) * add "All caught up!" state for application review card - Display success state when all applications have been reviewed and no new applications need review - Add iconColorClass input to activity-card component to support conditional icon colors (green checkmark for success state) - Add i18n keys: allCaughtUp and noNewApplicationsToReviewAtThisTime - Check if all apps have review dates via enrichedReportData$ to determine when to show the caught up state * fix "Potential Race Condition with State Initialization" from claude issue and replace getter --- apps/web/src/locales/en/messages.json | 6 ++++ .../activity/activity-card.component.html | 2 +- .../activity/activity-card.component.ts | 8 +++++ .../activity/all-activity.component.html | 17 +++++++--- .../activity/all-activity.component.ts | 34 +++++++++++++++++++ 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2e0fd99bbcc..36d68429ac6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -343,6 +343,12 @@ "reviewNow": { "message": "Review now" }, + "allCaughtUp": { + "message": "All caught up!" + }, + "noNewApplicationsToReviewAtThisTime": { + "message": "No new applications to review at this time" + }, "prioritizeCriticalApplications": { "message": "Prioritize critical applications" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html index 756907d24e6..73f98034f0a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html @@ -2,7 +2,7 @@ {{ title }}
@if (iconClass) { - + } {{ cardMetrics }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts index 427e7262f50..24d931165a7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts @@ -58,6 +58,14 @@ export class ActivityCardComponent { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() iconClass: string | null = null; + /** + * CSS class for icon color (e.g., "tw-text-success", "tw-text-muted"). + * Defaults to "tw-text-muted" if not provided. + */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() iconColorClass: string = "tw-text-muted"; + /** * Button text. If provided, a button will be displayed instead of a navigation link. */ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html index 82137c9acca..8cdb927ab65 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html @@ -45,10 +45,19 @@
  • diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts index b12a8fffedb..150c66ad2d4 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts @@ -42,6 +42,9 @@ export class AllActivityComponent implements OnInit { newApplicationsCount = 0; newApplications: string[] = []; passwordChangeMetricHasProgressBar = false; + allAppsHaveReviewDate = false; + isAllCaughtUp = false; + hasLoadedApplicationData = false; destroyRef = inject(DestroyRef); @@ -79,6 +82,7 @@ export class AllActivityComponent implements OnInit { .subscribe((newApps) => { this.newApplications = newApps; this.newApplicationsCount = newApps.length; + this.updateIsAllCaughtUp(); }); this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$ @@ -86,9 +90,39 @@ export class AllActivityComponent implements OnInit { .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.