From 2bb86631f3dc179e623c8891287c8ba49df66cec Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:49:50 +0100 Subject: [PATCH] [PM-13127]Breadcrumb event logs (#13386) * Changes for the dummy event * Add the request UI changes * refactoring the code * swapping out the datasources * Put the changes behind a feature flag * Rename the Feature flag to lowercase * Rename the feature flag to epic * Changes to resolve the pr comments * Merge the two tables * commit changes * Remove unused code * Add the suggested of content projection * Resolve the failing ui issues * remove unused code * Resolve the repeated code --- .../organization-layout.component.html | 5 +- .../layouts/organization-layout.component.ts | 4 ++ .../manage/events.component.html | 53 +++++++++++++--- .../organizations/manage/events.component.ts | 58 +++++++++++++++-- .../manage/placeholder-events.ts | 63 +++++++++++++++++++ .../organization-reporting-routing.module.ts | 27 +++++++- apps/web/src/locales/en/messages.json | 18 ++++++ libs/common/src/enums/feature-flag.enum.ts | 2 + 8 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/manage/placeholder-events.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index ae0972a6828..c515722fbef 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -40,7 +40,10 @@ ; showAccountDeprovisioningBanner$: Observable; + protected isBreadcrumbEventLogsEnabled$: Observable; constructor( private route: ActivatedRoute, @@ -78,6 +79,9 @@ export class OrganizationLayoutComponent implements OnInit { ) {} async ngOnInit() { + this.isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); document.body.classList.remove("layout_frontend"); this.organization$ = this.route.params.pipe( diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 654020ec263..4aa0bc8b296 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,5 +1,9 @@ - - +@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async); + + + {{ "upgrade" | i18n }} + +
@@ -31,6 +35,7 @@ bitFormButton buttonType="primary" [bitAction]="refreshEvents" + [disabled]="usePlaceHolderEvents" > {{ "update" | i18n }} @@ -42,7 +47,7 @@ bitButton bitFormButton [bitAction]="exportEvents" - [disabled]="dirtyDates" + [disabled]="dirtyDates || usePlaceHolderEvents" > {{ "export" | i18n }} @@ -50,6 +55,13 @@
+ + {{ "upgradeEventLogMessage" | i18n }} + {{ "loading" | i18n }} -

{{ "noEventsInList" | i18n }}

- + @let displayedEvents = organization?.useEvents ? events : placeholderEvents; + +

{{ "noEventsInList" | i18n }}

+ {{ "timestamp" | i18n }} @@ -70,8 +84,10 @@ - - {{ e.date | date: "medium" }} + + + {{ i > 4 && usePlaceHolderEvents ? "******" : (e.date | date: "medium") }} + {{ e.appName }} @@ -92,3 +108,26 @@ {{ "loadMore" | i18n }}
+ + +
+
+ + +

+ {{ "limitedEventLogs" | i18n: ProductTierType[organization?.productTierType] }} +

+

+ {{ "upgradeForFullEvents" | i18n }} +

+ + +
+
+
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 c6969f5b55e..737a38ee2ab 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,11 +2,12 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { getOrganizationById, OrganizationService, @@ -15,18 +16,29 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { EventSystemUser } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; +import { EventView } from "@bitwarden/common/models/view/event.view"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../../../billing/organizations/change-plan-dialog.component"; import { EventService } from "../../../core"; import { EventExportService } from "../../../tools/event-export"; import { BaseEventsComponent } from "../../common/base.events.component"; +import { placeholderEvents } from "./placeholder-events"; + const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM [EventSystemUser.DomainVerification]: "domainVerification", @@ -41,10 +53,19 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe exportFileName = "org-events"; organizationId: string; organization: Organization; + organizationSubscription: OrganizationSubscriptionResponse; + + placeholderEvents = placeholderEvents as EventView[]; private orgUsersUserIdMap = new Map(); private destroy$ = new Subject(); + readonly ProductTierType = ProductTierType; + + protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.PM12276_BreadcrumbEventLogs, + ); + constructor( private apiService: ApiService, private route: ActivatedRoute, @@ -57,10 +78,13 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe private userNamePipe: UserNamePipe, private organizationService: OrganizationService, private organizationUserApiService: OrganizationUserApiService, + private organizationApiService: OrganizationApiServiceAbstraction, private providerService: ProviderService, fileDownloadService: FileDownloadService, toastService: ToastService, private accountService: AccountService, + private dialogService: DialogService, + private configService: ConfigService, ) { super( eventService, @@ -84,10 +108,16 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe .organizations$(userId) .pipe(getOrganizationById(this.organizationId)), ); - if (this.organization == null || !this.organization.useEvents) { - await this.router.navigate(["/organizations", this.organizationId]); - return; + + if (!this.organization.useEvents) { + this.eventsForm.get("start").disable(); + this.eventsForm.get("end").disable(); + + this.organizationSubscription = await this.organizationApiService.getSubscription( + this.organizationId, + ); } + await this.load(); }), takeUntil(this.destroy$), @@ -126,7 +156,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe this.logService.warning(e); } } - await this.refreshEvents(); this.loaded = true; } @@ -186,6 +215,23 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe return id?.substring(0, 8); } + async changePlan() { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + subscription: this.organizationSubscription, + productTierType: this.organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Closed) { + return; + } + await this.load(); + } + ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/apps/web/src/app/admin-console/organizations/manage/placeholder-events.ts b/apps/web/src/app/admin-console/organizations/manage/placeholder-events.ts new file mode 100644 index 00000000000..3b13ee060bf --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/manage/placeholder-events.ts @@ -0,0 +1,63 @@ +function getRandomDateTime() { + const now = new Date(); + const past24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const randomTime = + past24Hours.getTime() + Math.random() * (now.getTime() - past24Hours.getTime()); + const randomDate = new Date(randomTime); + + return randomDate.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); +} + +const asteriskPlaceholders = new Array(6).fill({ + appName: "***", + userName: "**********", + userEmail: "**********", + message: "**********", +}); + +export const placeholderEvents = [ + { + date: getRandomDateTime(), + appName: "Extension - Firefox", + userName: "Alice", + userEmail: "alice@email.com", + message: "Logged in", + }, + { + date: getRandomDateTime(), + appName: "Mobile - iOS", + userName: "Bob", + message: `Viewed item 000000`, + }, + { + date: getRandomDateTime(), + appName: "Desktop - Linux", + userName: "Carlos", + userEmail: "carlos@email.com", + message: "Login attempt failed with incorrect password", + }, + { + date: getRandomDateTime(), + appName: "Web vault - Chrome", + userName: "Ivan", + userEmail: "ivan@email.com", + message: `Confirmed user 000000`, + }, + { + date: getRandomDateTime(), + appName: "Mobile - Android", + userName: "Franz", + userEmail: "franz@email.com", + message: `Sent item 000000 to trash`, + }, +] + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + .concat(asteriskPlaceholders); diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts index 2de5b83c40a..635053dd1e2 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts @@ -1,12 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; +import { inject, NgModule } from "@angular/core"; +import { CanMatchFn, RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { canAccessReportingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -/* eslint no-restricted-imports: "off" -- Normally prohibited by Tools Team eslint rules but required here */ import { ExposedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/exposed-passwords-report.component"; import { InactiveTwoFactorReportComponent } from "../../../tools/reports/pages/organizations/inactive-two-factor-report.component"; import { ReusedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/reused-passwords-report.component"; @@ -20,6 +22,11 @@ import { EventsComponent } from "../manage/events.component"; import { ReportsHomeComponent } from "./reports-home.component"; +const breadcrumbEventLogsPermission$: CanMatchFn = () => + inject(ConfigService) + .getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs) + .pipe(map((breadcrumbEventLogs) => breadcrumbEventLogs === true)); + const routes: Routes = [ { path: "", @@ -81,6 +88,20 @@ const routes: Routes = [ }, ], }, + // Event routing is temporarily duplicated + { + path: "events", + component: EventsComponent, + canMatch: [breadcrumbEventLogsPermission$], // if this matches, the flag is ON + canActivate: [ + organizationPermissionsGuard( + (org) => (org.canAccessEventLogs && org.useEvents) || org.isOwner, + ), + ], + data: { + titleId: "eventLogs", + }, + }, { path: "events", component: EventsComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 352dfd1fc72..3d50e842a81 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10513,5 +10513,23 @@ }, "removeUnlockWithPinPolicyDesc": { "message": "Do not allow members to unlock their account with a PIN." + }, + "limitedEventLogs": { + "message": "$PRODUCT_TYPE$ plans do not have access to real event logs", + "placeholders": { + "product_type": { + "content": "$1", + "example": "Teams" + } + } + }, + "upgradeForFullEvents": { + "message": "Get full access to organization event logs by upgrading to a Teams or Enterprise plan." + }, + "upgradeEventLogTitle" : { + "message" : "Upgrade for real event log data" + }, + "upgradeEventLogMessage":{ + "message" : "These events are examples only and do not reflect real events within your Bitwarden organization." } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ca6a26bc531..2f3e6bb724b 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -48,6 +48,7 @@ export enum FeatureFlag { NewDeviceVerification = "new-device-verification", PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", RecoveryCodeLogin = "pm-17128-recovery-code-login", + PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -106,6 +107,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NewDeviceVerification]: FALSE, [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.RecoveryCodeLogin]: FALSE, + [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;