1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-02 18:17:46 +01:00

[PM-2049] Update entity events dialog (#5417)

* [AC-1145] Update entity-events.component.ts to a CL dialog

- Add EntityEventsDialogParams
- Add static helper method to open the dialog with the dialog service
- Update existing usages of the entity-events.component.ts

* [AC-1145] Update entity-events.component.ts to use CL components and form actions

- Use bit-table and TableDataSource
- Update to reactive form for date filter
- Make dialog component standalone
- Use bitAction in-place of component promises
- Remove redundant try/catch that is now handled by bitAction and bitSubmit
- Add new try/catch on first load to catch any errors during initial dialog open

* [PM-2049] Make dataSource and filterFormGroup protected

* [PM-2049] Remove bit-form-field container

Remove the bit-form-field tags that wrapped the date inputs to avoid additional styling that is not applicable to inline form elements.

Add back the missing `-` that was removed by mistake.

* [PM-2049] Remove entity events dialog component selector
This commit is contained in:
Shane Melton 2023-06-14 13:09:56 -07:00 committed by GitHub
parent a7f9984ddd
commit ed04907300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 217 additions and 215 deletions

View File

@ -1,118 +1,89 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="eventLogsTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="eventLogsTitle">
<bit-dialog dialogSize="large" [loading]="loading">
<span bitDialogTitle>
{{ "eventLogs" | i18n }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="modal-body" *ngIf="loaded">
<div class="d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
<small class="tw-text-muted" *ngIf="name">{{ name }}</small>
</span>
<div bitDialogContent>
<form [formGroup]="filterFormGroup" [bitSubmit]="refreshEvents">
<div class="tw-flex tw-items-center tw-space-x-2">
<div>
<label class="tw-sr-only" for="start">{{ "startDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
class="form-control form-control-sm"
id="start"
placeholder="{{ 'startDate' | i18n }}"
[(ngModel)]="start"
placeholder="YYYY-MM-DDTHH:MM"
formControlName="start"
/>
<span class="mx-2">-</span>
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
</span>
</div>
<span class="tw-mx-2">-</span>
<div>
<label class="tw-sr-only" for="end">{{ "endDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
class="form-control form-control-sm"
id="end"
placeholder="{{ 'endDate' | i18n }}"
[(ngModel)]="end"
placeholder="YYYY-MM-DDTHH:MM"
formControlName="end"
/>
</span>
</div>
<button
#refreshBtn
[appApiAction]="refreshPromise"
type="button"
class="btn btn-sm btn-outline-primary ml-3"
(click)="loadEvents(true)"
[disabled]="loaded && $any(refreshBtn).loading"
>
<i
class="bwi bwi-refresh bwi-fw"
[ngClass]="{ 'bwi-spin': loaded && $any(refreshBtn).loading }"
aria-hidden="true"
></i>
<button type="submit" bitButton buttonType="primary" bitFormButton>
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
{{ "refresh" | i18n }}
</button>
</div>
</form>
<hr />
<div *ngIf="!events || !events.length">
<div *ngIf="!dataSource.data || !dataSource.data.length">
{{ "noEventsInList" | i18n }}
</div>
<table class="table table-hover mb-0" *ngIf="events && events.length">
<thead>
<bit-table [dataSource]="dataSource" *ngIf="dataSource?.data?.length">
<ng-container header>
<tr>
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
<th class="border-top-0" width="40">
<span class="sr-only">{{ "device" | i18n }}</span>
<th bitCell>{{ "timestamp" | i18n }}</th>
<th bitCell>
<span class="tw-sr-only">{{ "client" | i18n }}</span>
</th>
<th class="border-top-0" width="150" *ngIf="showUser">{{ "user" | i18n }}</th>
<th class="border-top-0">{{ "event" | i18n }}</th>
<th bitCell *ngIf="showUser">{{ "member" | i18n }}</th>
<th bitCell>{{ "event" | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of events">
<td>{{ e.date | date : "medium" }}</td>
<td>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>{{ r.date | date : "medium" }}</td>
<td bitCell>
<i
class="text-muted bwi bwi-lg {{ e.appIcon }}"
title="{{ e.appName }}, {{ e.ip }}"
class="tw-text-muted bwi bwi-lg {{ r.appIcon }}"
title="{{ r.appName }}, {{ r.ip }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
<span class="tw-sr-only">{{ r.appName }}, {{ r.ip }}</span>
</td>
<td *ngIf="showUser">
<span appA11yTitle="{{ e.userEmail }}">{{ e.userName }}</span>
<td bitCell *ngIf="showUser">
<span appA11yTitle="{{ r.userEmail }}">{{ r.userName }}</span>
</td>
<td [innerHTML]="e.message"></td>
<td bitCell [innerHTML]="r.message"></td>
</tr>
</tbody>
</table>
</ng-template>
</bit-table>
<button
#moreBtn
[appApiAction]="morePromise"
bitButton
buttonType="secondary"
block
[bitAction]="loadMoreEvents"
type="button"
class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)"
[disabled]="loaded && $any(moreBtn).loading"
*ngIf="continuationToken"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
{{ "loadMore" | i18n }}
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
<ng-container bitDialogFooter>
<button bitButton buttonType="secondary" type="button" bitDialogClose>
{{ "close" | i18n }}
</button>
</div>
</div>
</div>
</div>
</ng-container>
</bit-dialog>

View File

@ -1,60 +1,84 @@
import { Component, Input, OnInit } from "@angular/core";
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { TableDataSource } from "@bitwarden/components";
import { EventService } from "../../../core";
import { SharedModule } from "../../../shared";
export interface EntityEventsDialogParams {
entity: "user" | "cipher";
entityId: string;
organizationId?: string;
providerId?: string;
showUser?: boolean;
name?: string;
}
@Component({
selector: "app-entity-events",
imports: [SharedModule],
templateUrl: "entity-events.component.html",
standalone: true,
})
export class EntityEventsComponent implements OnInit {
@Input() name: string;
@Input() entity: "user" | "cipher";
@Input() entityId: string;
@Input() organizationId: string;
@Input() providerId: string;
@Input() showUser = false;
loading = true;
loaded = false;
events: any[];
start: string;
end: string;
continuationToken: string;
refreshPromise: Promise<any>;
morePromise: Promise<any>;
protected dataSource = new TableDataSource<EventView>();
protected filterFormGroup = this.formBuilder.group({
start: [""],
end: [""],
});
private orgUsersUserIdMap = new Map<string, any>();
private orgUsersIdMap = new Map<string, any>();
get name() {
return this.params.name;
}
get showUser() {
return this.params.showUser ?? false;
}
constructor(
@Inject(DIALOG_DATA) private params: EntityEventsDialogParams,
private apiService: ApiService,
private i18nService: I18nService,
private eventService: EventService,
private platformUtilsService: PlatformUtilsService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private organizationUserService: OrganizationUserService
private organizationUserService: OrganizationUserService,
private formBuilder: FormBuilder,
private validationService: ValidationService
) {}
async ngOnInit() {
const defaultDates = this.eventService.getDefaultDateFilters();
this.start = defaultDates[0];
this.end = defaultDates[1];
this.filterFormGroup.setValue({
start: defaultDates[0],
end: defaultDates[1],
});
await this.load();
}
async load() {
try {
if (this.showUser) {
const response = await this.organizationUserService.getAllUsers(this.organizationId);
const response = await this.organizationUserService.getAllUsers(this.params.organizationId);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
@ -62,17 +86,29 @@ export class EntityEventsComponent implements OnInit {
});
}
await this.loadEvents(true);
this.loaded = true;
} catch (e) {
this.logService.error(e);
this.validationService.showError(e);
}
async loadEvents(clearExisting: boolean) {
if (this.refreshPromise != null || this.morePromise != null) {
return;
this.loading = false;
}
loadMoreEvents = async () => {
await this.loadEvents(false);
};
refreshEvents = async () => {
await this.loadEvents(true);
};
private async loadEvents(clearExisting: boolean) {
let dates: string[] = null;
try {
dates = this.eventService.formatDateFilters(this.start, this.end);
dates = this.eventService.formatDateFilters(
this.filterFormGroup.value.start,
this.filterFormGroup.value.end
);
} catch (e) {
this.platformUtilsService.showToast(
"error",
@ -82,46 +118,34 @@ export class EntityEventsComponent implements OnInit {
return;
}
this.loading = true;
let response: ListResponse<EventResponse>;
try {
let promise: Promise<any>;
if (this.entity === "user" && this.providerId) {
promise = this.apiService.getEventsProviderUser(
this.providerId,
this.entityId,
if (this.params.entity === "user" && this.params.providerId) {
response = await this.apiService.getEventsProviderUser(
this.params.providerId,
this.params.entityId,
dates[0],
dates[1],
clearExisting ? null : this.continuationToken
);
} else if (this.entity === "user") {
promise = this.apiService.getEventsOrganizationUser(
this.organizationId,
this.entityId,
} else if (this.params.entity === "user") {
response = await this.apiService.getEventsOrganizationUser(
this.params.organizationId,
this.params.entityId,
dates[0],
dates[1],
clearExisting ? null : this.continuationToken
);
} else {
promise = this.apiService.getEventsCipher(
this.entityId,
response = await this.apiService.getEventsCipher(
this.params.entityId,
dates[0],
dates[1],
clearExisting ? null : this.continuationToken
);
}
if (clearExisting) {
this.refreshPromise = promise;
} else {
this.morePromise = promise;
}
response = await promise;
} catch (e) {
this.logService.error(e);
}
this.continuationToken = response.continuationToken;
const events = await Promise.all(
const events: EventView[] = await Promise.all(
response.data.map(async (r) => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r);
@ -129,8 +153,10 @@ export class EntityEventsComponent implements OnInit {
this.showUser && userId != null && this.orgUsersUserIdMap.has(userId)
? this.orgUsersUserIdMap.get(userId)
: null;
return {
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
@ -139,18 +165,29 @@ export class EntityEventsComponent implements OnInit {
date: r.date,
ip: r.ipAddress,
type: r.type,
};
installationId: r.installationId,
systemUser: r.systemUser,
serviceAccountId: r.serviceAccountId,
});
})
);
if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);
if (!clearExisting && this.dataSource.data != null && this.dataSource.data.length > 0) {
this.dataSource.data = this.dataSource.data.concat(events);
} else {
this.events = events;
this.dataSource.data = events;
}
}
}
this.loading = false;
this.morePromise = null;
this.refreshPromise = null;
}
}
/**
* Strongly typed helper to open a EntityEventsComponent as a dialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export const openEntityEventsDialog = (
dialogService: DialogServiceAbstraction,
config: DialogConfig<EntityEventsDialogParams>
) => {
return dialogService.open<void, EntityEventsDialogParams>(EntityEventsComponent, config);
};

View File

@ -317,7 +317,6 @@
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #groupsTemplate></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #resetPasswordTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>

View File

@ -16,10 +16,10 @@ import {
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import {
SimpleDialogType,
DialogServiceAbstraction,
SimpleDialogCloseType,
SimpleDialogOptions,
SimpleDialogType,
} from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -55,7 +55,7 @@ import { CollectionData } from "@bitwarden/common/vault/models/data/collection.d
import { Collection } from "@bitwarden/common/vault/models/domain/collection";
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { EntityEventsComponent } from "../../../admin-console/organizations/manage/entity-events.component";
import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component";
import { BasePeopleComponent } from "../../../common/base.people.component";
import { GroupService } from "../core";
import { OrganizationUserView } from "../core/views/organization-user.view";
@ -81,8 +81,6 @@ export class PeopleComponent
{
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef;
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
@ -513,12 +511,14 @@ export class PeopleComponent
}
async events(user: OrganizationUserView) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user);
comp.organizationId = this.organization.id;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = "user";
await openEntityEventsDialog(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
organizationId: this.organization.id,
entityId: user.id,
showUser: false,
entity: "user",
},
});
}

View File

@ -3,7 +3,6 @@ import { NgModule } from "@angular/core";
import { OrganizationSwitcherComponent } from "../admin-console/components/organization-switcher.component";
import { OrganizationCreateModule } from "../admin-console/organizations/create/organization-create.module";
import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component";
import { EntityEventsComponent as OrgEntityEventsComponent } from "../admin-console/organizations/manage/entity-events.component";
import { EventsComponent as OrgEventsComponent } from "../admin-console/organizations/manage/events.component";
import { UserConfirmComponent as OrgUserConfirmComponent } from "../admin-console/organizations/manage/user-confirm.component";
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
@ -166,7 +165,6 @@ import { SharedModule } from "./shared.module";
OrganizationPlansComponent,
OrgAttachmentsComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEventsComponent,
OrgExposedPasswordsReportComponent,
OrgInactiveTwoFactorReportComponent,
@ -271,7 +269,6 @@ import { SharedModule } from "./shared.module";
OrganizationPlansComponent,
OrgAttachmentsComponent,
OrgCollectionsComponent,
OrgEntityEventsComponent,
OrgEventsComponent,
OrgExposedPasswordsReportComponent,
OrgInactiveTwoFactorReportComponent,

View File

@ -96,5 +96,4 @@
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collectionsModal></ng-template>
<ng-template #eventsTemplate></ng-template>
</div>

View File

@ -56,7 +56,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { Icons } from "@bitwarden/components";
import { GroupService, GroupView } from "../../admin-console/organizations/core";
import { EntityEventsComponent } from "../../admin-console/organizations/manage/entity-events.component";
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
import {
@ -109,8 +109,6 @@ export class VaultComponent implements OnInit, OnDestroy {
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
@ -885,12 +883,14 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async viewEvents(cipher: CipherView) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = cipher.name;
comp.organizationId = this.organization.id;
comp.entityId = cipher.id;
comp.showUser = true;
comp.entity = "cipher";
await openEntityEventsDialog(this.dialogService, {
data: {
name: cipher.name,
organizationId: this.organization.id,
entityId: cipher.id,
showUser: true,
entity: "cipher",
},
});
}

View File

@ -209,7 +209,6 @@
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>

View File

@ -21,7 +21,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { EntityEventsComponent } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component";
import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component";
@ -41,8 +41,6 @@ export class PeopleComponent
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
bulkStatusModalRef: ViewContainerRef;
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
@ -167,12 +165,14 @@ export class PeopleComponent
}
async events(user: ProviderUserUserDetailsResponse) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = "user";
await openEntityEventsDialog(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
providerId: this.providerId,
entityId: user.id,
showUser: false,
entity: "user",
},
});
}