mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -65,6 +65,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
enterpriseOrganization$: Observable<boolean>;
|
||||
|
||||
showAccountDeprovisioningBanner$: Observable<boolean>;
|
||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
||||
|
||||
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(
|
||||
|
||||
@@ -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>
|
||||
@@ -31,6 +35,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
[bitAction]="refreshEvents"
|
||||
[disabled]="usePlaceHolderEvents"
|
||||
>
|
||||
{{ "update" | i18n }}
|
||||
</button>
|
||||
@@ -42,7 +47,7 @@
|
||||
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>
|
||||
@@ -50,6 +55,13 @@
|
||||
</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"
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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, string> = {
|
||||
[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<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,
|
||||
@@ -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();
|
||||
|
||||
@@ -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 <span class="tw-text-code">000000</span>`,
|
||||
},
|
||||
{
|
||||
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 <span class="tw-text-code">000000</span>`,
|
||||
},
|
||||
{
|
||||
date: getRandomDateTime(),
|
||||
appName: "Mobile - Android",
|
||||
userName: "Franz",
|
||||
userEmail: "franz@email.com",
|
||||
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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
Reference in New Issue
Block a user