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..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, filter, 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,17 +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),
- filter((result) => result),
- 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) {
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 474acfe3bfe..b18d14426bd 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -355,6 +355,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 713fdfa414f..e8768c0c404 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
@@ -43,6 +43,9 @@ export class AllActivityComponent implements OnInit {
newApplicationsCount = 0;
newApplications: NewApplicationDetail[] = [];
passwordChangeMetricHasProgressBar = false;
+ allAppsHaveReviewDate = false;
+ isAllCaughtUp = false;
+ hasLoadedApplicationData = false;
destroyRef = inject(DestroyRef);
@@ -80,6 +83,7 @@ export class AllActivityComponent implements OnInit {
.subscribe((newApps) => {
this.newApplications = newApps;
this.newApplicationsCount = newApps.length;
+ this.updateIsAllCaughtUp();
});
this.allActivitiesService.passwordChangeProgressMetricHasProgressBar$
@@ -87,9 +91,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.