Skip to content

Commit

Permalink
[PM-13127]Breadcrumb event logs (#13386)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cyprain-okeke authored Feb 25, 2025
1 parent c7315a0 commit 2bb8663
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
<bit-nav-item
[text]="'eventLogs' | i18n"
route="reporting/events"
*ngIf="organization.canAccessEventLogs"
*ngIf="
(organization.canAccessEventLogs && organization.useEvents) ||
(organization.isOwner && (isBreadcrumbEventLogsEnabled$ | async))
"
></bit-nav-item>
<bit-nav-item
[text]="'reports' | i18n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class OrganizationLayoutComponent implements OnInit {
enterpriseOrganization$: Observable<boolean>;

showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;

constructor(
private route: ActivatedRoute,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<app-header></app-header>

@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
<app-header>
<span bitBadge variant="primary" slot="title-suffix" *ngIf="usePlaceHolderEvents">
{{ "upgrade" | i18n }}
</span>
</app-header>
<div class="tw-mb-4" [formGroup]="eventsForm">
<div class="tw-mt-4 tw-flex tw-items-center">
<bit-form-field>
Expand Down Expand Up @@ -31,6 +35,7 @@
bitFormButton
buttonType="primary"
[bitAction]="refreshEvents"
[disabled]="usePlaceHolderEvents"
>
{{ "update" | i18n }}
</button>
Expand All @@ -42,14 +47,21 @@
bitButton
bitFormButton
[bitAction]="exportEvents"
[disabled]="dirtyDates"
[disabled]="dirtyDates || usePlaceHolderEvents"
>
<span>{{ "export" | i18n }}</span>
<i class="bwi bwi-fw bwi-sign-in" aria-hidden="true"></i>
</button>
</form>
</div>
</div>
<bit-callout
type="info"
[title]="'upgradeEventLogTitle' | i18n"
*ngIf="loaded && usePlaceHolderEvents"
>
{{ "upgradeEventLogMessage" | i18n }}
</bit-callout>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
Expand All @@ -59,8 +71,10 @@
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
<bit-table *ngIf="events && events.length">
@let displayedEvents = organization?.useEvents ? events : placeholderEvents;

<p *ngIf="!displayedEvents || !displayedEvents.length">{{ "noEventsInList" | i18n }}</p>
<bit-table *ngIf="displayedEvents && displayedEvents.length">
<ng-container header>
<tr>
<th bitCell>{{ "timestamp" | i18n }}</th>
Expand All @@ -70,8 +84,10 @@
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let e of events" alignContent="top">
<td bitCell class="tw-whitespace-nowrap">{{ e.date | date: "medium" }}</td>
<tr bitRow *ngFor="let e of displayedEvents; index as i" alignContent="top">
<td bitCell class="tw-whitespace-nowrap">
{{ i > 4 && usePlaceHolderEvents ? "******" : (e.date | date: "medium") }}
</td>
<td bitCell>
<span title="{{ e.appName }}, {{ e.ip }}">{{ e.appName }}</span>
</td>
Expand All @@ -92,3 +108,26 @@
{{ "loadMore" | i18n }}
</button>
</ng-container>

<ng-container *ngIf="loaded && usePlaceHolderEvents">
<div
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center"
>
<div
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid mt-5"
>
<i class="bwi bwi-2x bwi-business text-primary"></i>

<p class="tw-font-bold mt-2">
{{ "limitedEventLogs" | i18n: ProductTierType[organization?.productTierType] }}
</p>
<p>
{{ "upgradeForFullEvents" | i18n }}
</p>

<button type="button" class="tw-mt-1" bitButton buttonType="primary" (click)="changePlan()">
{{ "changeBillingPlan" | i18n }}
</button>
</div>
</div>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, string> = {
[EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM
[EventSystemUser.DomainVerification]: "domainVerification",
Expand All @@ -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<string, any>();
private destroy$ = new Subject<void>();

readonly ProductTierType = ProductTierType;

protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.PM12276_BreadcrumbEventLogs,
);

constructor(
private apiService: ApiService,
private route: ActivatedRoute,
Expand All @@ -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,
Expand All @@ -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$),
Expand Down Expand Up @@ -126,7 +156,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
this.logService.warning(e);
}
}

await this.refreshEvents();
this.loaded = true;
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
message: "Logged in",
},
{
date: getRandomDateTime(),
appName: "Mobile - iOS",
userName: "Bob",
message: `Viewed item <span class="tw-text-code">000000</span>`,
},
{
date: getRandomDateTime(),
appName: "Desktop - Linux",
userName: "Carlos",
userEmail: "[email protected]",
message: "Login attempt failed with incorrect password",
},
{
date: getRandomDateTime(),
appName: "Web vault - Chrome",
userName: "Ivan",
userEmail: "[email protected]",
message: `Confirmed user <span class="tw-text-code">000000</span>`,
},
{
date: getRandomDateTime(),
appName: "Mobile - Android",
userName: "Franz",
userEmail: "[email protected]",
message: `Sent item <span class="tw-text-code">000000</span> to trash`,
},
]
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.concat(asteriskPlaceholders);
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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: "",
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
Loading

0 comments on commit 2bb8663

Please sign in to comment.