From e9d5d1dcaf39fc3ca67a3ab1842b33e53fb78894 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Wed, 1 Feb 2023 09:54:00 +0100 Subject: [PATCH] feat(console): events (#5092) Shows the list of events on instance level --- console/src/app/app-routing.module.ts | 8 + console/src/app/app.component.ts | 5 + .../app/modules/changes/changes.component.ts | 13 +- .../display-json-dialog.component.html | 79 +++++ .../display-json-dialog.component.scss | 45 +++ .../display-json-dialog.component.spec.ts | 23 ++ .../display-json-dialog.component.ts | 28 ++ .../display-json-dialog.module.ts | 31 ++ .../filter-events.component.html | 195 +++++++++++ .../filter-events.component.scss | 93 +++++ .../filter-events.component.spec.ts | 23 ++ .../filter-events/filter-events.component.ts | 330 ++++++++++++++++++ .../filter-events/filter-events.module.ts | 34 ++ .../app/modules/header/header.component.ts | 1 + .../src/app/modules/nav/nav.component.html | 11 + .../paginator/paginator.component.html | 6 + .../modules/paginator/paginator.component.ts | 4 + .../app/pages/events/events-routing.module.ts | 17 + .../app/pages/events/events.component.html | 129 +++++++ .../app/pages/events/events.component.scss | 97 +++++ .../app/pages/events/events.component.spec.ts | 24 ++ .../src/app/pages/events/events.component.ts | 198 +++++++++++ console/src/app/pages/events/events.module.ts | 65 ++++ .../app/pipes/to-object/to-object.module.ts | 11 + .../src/app/pipes/to-object/to-object.pipe.ts | 12 + .../app/pipes/to-payload/to-payload.module.ts | 11 + .../app/pipes/to-payload/to-payload.pipe.ts | 17 + console/src/app/services/admin.service.ts | 18 + console/src/assets/i18n/de.json | 54 ++- console/src/assets/i18n/en.json | 54 ++- console/src/assets/i18n/fr.json | 54 ++- console/src/assets/i18n/it.json | 54 ++- console/src/assets/i18n/zh.json | 54 ++- console/src/assets/mdi/arrow-expand.svg | 1 + console/src/component-themes.scss | 4 + 35 files changed, 1790 insertions(+), 13 deletions(-) create mode 100644 console/src/app/modules/display-json-dialog/display-json-dialog.component.html create mode 100644 console/src/app/modules/display-json-dialog/display-json-dialog.component.scss create mode 100644 console/src/app/modules/display-json-dialog/display-json-dialog.component.spec.ts create mode 100644 console/src/app/modules/display-json-dialog/display-json-dialog.component.ts create mode 100644 console/src/app/modules/display-json-dialog/display-json-dialog.module.ts create mode 100644 console/src/app/modules/filter-events/filter-events.component.html create mode 100644 console/src/app/modules/filter-events/filter-events.component.scss create mode 100644 console/src/app/modules/filter-events/filter-events.component.spec.ts create mode 100644 console/src/app/modules/filter-events/filter-events.component.ts create mode 100644 console/src/app/modules/filter-events/filter-events.module.ts create mode 100644 console/src/app/pages/events/events-routing.module.ts create mode 100644 console/src/app/pages/events/events.component.html create mode 100644 console/src/app/pages/events/events.component.scss create mode 100644 console/src/app/pages/events/events.component.spec.ts create mode 100644 console/src/app/pages/events/events.component.ts create mode 100644 console/src/app/pages/events/events.module.ts create mode 100644 console/src/app/pipes/to-object/to-object.module.ts create mode 100644 console/src/app/pipes/to-object/to-object.pipe.ts create mode 100644 console/src/app/pipes/to-payload/to-payload.module.ts create mode 100644 console/src/app/pipes/to-payload/to-payload.pipe.ts create mode 100644 console/src/assets/mdi/arrow-expand.svg diff --git a/console/src/app/app-routing.module.ts b/console/src/app/app-routing.module.ts index 981fab23f1..c1ff7b0c43 100644 --- a/console/src/app/app-routing.module.ts +++ b/console/src/app/app-routing.module.ts @@ -141,6 +141,14 @@ const routes: Routes = [ roles: ['iam.read'], }, }, + { + path: 'events', + loadChildren: () => import('./pages/events/events.module'), + canActivate: [AuthGuard, RoleGuard], + data: { + roles: ['iam.read'], + }, + }, { path: 'settings', loadChildren: () => import('./pages/instance-settings/instance-settings.module'), diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts index 16dbe053a8..d140adaee4 100644 --- a/console/src/app/app.component.ts +++ b/console/src/app/app.component.ts @@ -165,6 +165,11 @@ export class AppComponent implements OnDestroy { this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/shield-alert.svg'), ); + this.matIconRegistry.addSvgIcon( + 'mdi_arrow_expand', + this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/arrow-expand.svg'), + ); + this.matIconRegistry.addSvgIcon( 'mdi_numeric', this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/numeric.svg'), diff --git a/console/src/app/modules/changes/changes.component.ts b/console/src/app/modules/changes/changes.component.ts index 71ed943dcd..615449a14d 100644 --- a/console/src/app/modules/changes/changes.component.ts +++ b/console/src/app/modules/changes/changes.component.ts @@ -63,7 +63,11 @@ export class ChangesComponent implements OnInit, OnDestroy { private _data: BehaviorSubject = new BehaviorSubject([]); loading: Observable = this._loading.asObservable(); - public data!: Observable; + public data: Observable = this._data.asObservable().pipe( + scan((acc, val) => { + return false ? val.concat(acc) : acc.concat(val); + }), + ); public changes!: ListChanges; private destroyed$: Subject = new Subject(); constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {} @@ -106,13 +110,6 @@ export class ChangesComponent implements OnInit, OnDestroy { } this.mapAndUpdate(first); - - // Create the observable array for consumption in components - this.data = this._data.asObservable().pipe( - scan((acc, val) => { - return false ? val.concat(acc) : acc.concat(val); - }), - ); } public more(): void { diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.html b/console/src/app/modules/display-json-dialog/display-json-dialog.component.html new file mode 100644 index 0000000000..54f838f5a4 --- /dev/null +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.html @@ -0,0 +1,79 @@ +

{{ 'IAM.EVENTS.DIALOG.TITLE' | translate }}

+
+
+
+ {{ 'IAM.EVENTS.TYPE' | translate }} + {{ event.type.localized.localizedMessage }} +
+ +
+ {{ 'IAM.EVENTS.CREATIONDATE' | translate }} + {{ event.creationDate | timestampToDate | localizedDate : 'EEE dd. MMM, HH:mm' }} +
+ +
+ {{ 'IAM.EVENTS.AGGREGATEID' | translate }} + {{ event.aggregate.id }} +
+ +
+ {{ 'IAM.EVENTS.RESOURCEOWNER' | translate }} + {{ event.aggregate.resourceOwner }} +
+ +
+ {{ 'IAM.EVENTS.AGGREGATETYPE' | translate }} + {{ event.aggregate.type.localized.localizedMessage }} +
+ +
+ {{ 'IAM.EVENTS.EDITORID' | translate }} + {{ event.editor?.userId }} +
+ +
+ {{ 'IAM.EVENTS.EDITOR' | translate }} + {{ event.editor?.displayName }} +
+ +
+ {{ 'IAM.EVENTS.SEQUENCE' | translate }} + {{ event.sequence }} +
+
+ +
+ +
+
+
+ +
diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.scss b/console/src/app/modules/display-json-dialog/display-json-dialog.component.scss new file mode 100644 index 0000000000..acc920da1c --- /dev/null +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.scss @@ -0,0 +1,45 @@ +.title { + margin-bottom: 2rem; + display: block; +} + +.event-data-column { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 2rem; + grid-row-gap: 1rem; + margin-bottom: 2rem; + + .data-row { + display: flex; + flex-direction: column; + overflow: hidden; + + .label { + font-size: 12px; + margin-bottom: 2px; + } + + .aggregate-type { + align-self: flex-start; + } + + span { + text-overflow: ellipsis; + text-overflow: ellipsis; + } + } +} + +.code { + width: 100%; +} + +.action { + display: flex; + justify-content: flex-end; + + .ok-button { + margin-left: 0.5rem; + } +} diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.spec.ts b/console/src/app/modules/display-json-dialog/display-json-dialog.component.spec.ts new file mode 100644 index 0000000000..2c5b75621e --- /dev/null +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DisplayJsonDialogComponent } from './display-json-dialog.component'; + +describe('DisplayJsonDialogComponent', () => { + let component: DisplayJsonDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DisplayJsonDialogComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DisplayJsonDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts b/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts new file mode 100644 index 0000000000..c893e5880d --- /dev/null +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.component.ts @@ -0,0 +1,28 @@ +import { Component, Inject } from '@angular/core'; +import { + MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA, + MatLegacyDialogRef as MatDialogRef, +} from '@angular/material/legacy-dialog'; +import { mapTo } from 'rxjs'; +import { Event } from 'src/app/proto/generated/zitadel/event_pb'; + +@Component({ + selector: 'cnsl-display-json-dialog', + templateUrl: './display-json-dialog.component.html', + styleUrls: ['./display-json-dialog.component.scss'], +}) +export class DisplayJsonDialogComponent { + public event?: Event; + public payload: any = ''; + public opened$ = this.dialogRef.afterOpened().pipe(mapTo(true)); + + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any) { + this.event = data.event; + if ((data.event as Event) && data.event.payload) { + } + } + + public closeDialog(): void { + this.dialogRef.close(false); + } +} diff --git a/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts b/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts new file mode 100644 index 0000000000..dc8725d46b --- /dev/null +++ b/console/src/app/modules/display-json-dialog/display-json-dialog.module.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslateModule } from '@ngx-translate/core'; +import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; + +import { DisplayJsonDialogComponent } from './display-json-dialog.component'; +import { CodemirrorModule } from '@ctrl/ngx-codemirror'; +import { FormsModule } from '@angular/forms'; +import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; +import { ToPayloadPipeModule } from 'src/app/pipes/to-payload/to-payload.module'; +import { ToObjectPipeModule } from 'src/app/pipes/to-object/to-object.module'; + +@NgModule({ + declarations: [DisplayJsonDialogComponent], + imports: [ + CommonModule, + FormsModule, + TranslateModule, + MatButtonModule, + MatIconModule, + CodemirrorModule, + TimestampToDatePipeModule, + ToObjectPipeModule, + ToPayloadPipeModule, + LocalizedDatePipeModule, + ], + exports: [DisplayJsonDialogComponent], +}) +export class DisplayJsonDialogModule {} diff --git a/console/src/app/modules/filter-events/filter-events.component.html b/console/src/app/modules/filter-events/filter-events.component.html new file mode 100644 index 0000000000..7f73b8eb17 --- /dev/null +++ b/console/src/app/modules/filter-events/filter-events.component.html @@ -0,0 +1,195 @@ +
+ + +
+
+ + {{ 'FILTER.TITLE' | translate }} + +
+
+
+
+
+ {{ 'IAM.EVENTS.FILTERS.USER.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.USER.IDLABEL' | translate }} + + +
+
+ +
+
+ {{ 'IAM.EVENTS.FILTERS.AGGREGATE.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.AGGREGATE.TYPELABEL' | translate }} + + + + {{ + aggregateTypesList?.value[0]?.localized?.localizedMessage || '' + }} + + (+{{ (aggregateTypesList?.value?.length || 0) - 1 }} + {{ aggregateTypesList?.value?.length === 2 ? 'other' : 'others' }}) + + + + + {{ aggregate.localized.localizedMessage }} + + + + + + + {{ 'IAM.EVENTS.FILTERS.AGGREGATE.IDLABEL' | translate }} + + +
+
+ +
+
+ {{ 'IAM.EVENTS.FILTERS.TYPE.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.TYPE.TYPELABEL' | translate }} + + + + {{ + eventTypesList?.value[0]?.localized?.localizedMessage || '' + }} + + (+{{ (eventTypesList?.value?.length || 0) - 1 }} + {{ eventTypesList?.value?.length === 2 ? 'other' : 'others' }}) + + + + + {{ eventType.localized.localizedMessage }} + + + + +
+
+ +
+
+ {{ 'IAM.EVENTS.FILTERS.RESOURCEOWNER.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.RESOURCEOWNER.LABEL' | translate }} + + +
+
+ +
+
+ {{ 'IAM.EVENTS.FILTERS.SEQUENCE.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.SEQUENCE.SORT' | translate }} + + + {{ 'IAM.EVENTS.FILTERS.SEQUENCE.DESC' | translate }} + {{ 'IAM.EVENTS.FILTERS.SEQUENCE.ASC' | translate }} + + + + + {{ 'IAM.EVENTS.FILTERS.SEQUENCE.LABEL' | translate }} + + +
+
+ +
+
+ {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.CHECKBOX' | translate }} + +
+
+ + {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL' | translate }} + + + + +
+
+
+
+
+
diff --git a/console/src/app/modules/filter-events/filter-events.component.scss b/console/src/app/modules/filter-events/filter-events.component.scss new file mode 100644 index 0000000000..40ff03e4c1 --- /dev/null +++ b/console/src/app/modules/filter-events/filter-events.component.scss @@ -0,0 +1,93 @@ +@use '@angular/material' as mat; + +@mixin filter-events-theme($theme) { + $primary: map-get($theme, primary); + $primary-color: mat.get-color-from-palette($primary, 500); + $lighter-primary-color: mat.get-color-from-palette($primary, 300); + $darker-primary-color: mat.get-color-from-palette($primary, 700); + + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $secondary-text: map-get($foreground, secondary-text); + $is-dark-theme: map-get($theme, is-dark); + + $link-hover-color: if($is-dark-theme, mat.get-color-from-palette($primary, 200), $primary-color); + $link-color: if($is-dark-theme, $lighter-primary-color, $primary-color); + + .filter-events-wrapper { + border-radius: 0.5rem; + z-index: 200; + border: 1px solid #ffffff30; + display: flex; + flex-direction: column; + padding: 0.5rem 0; + min-width: 320px; + max-width: 360px; + padding-bottom: 0.5rem; + position: relative; + color: map-get($foreground, text); + background: map-get($background, cards); + + .sp_wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 0; + } + + .filter-events-top { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 0.5rem 0.5rem 0.5rem; + border-bottom: 2px solid if($is-dark-theme, #ffffff15, #00000015); + + .filter-middle { + margin: 0 1rem; + } + } + } + + .filter-events-section { + padding: 0 0.5rem; + + .checkbox-wrapper { + height: 40px; + display: flex; + align-items: center; + } + + .filter-events-sub { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0 0.5rem; + background-color: if($is-dark-theme, #00000020, #00000008); + margin: 0 -0.5rem; + + .aggregate-type-select .mat-select { + height: 36px; + padding: 7px 10px; + } + + .aggregate-type-select { + min-width: 100px; + margin-right: 0.5rem; + } + + .event-types-select { + width: 100%; + } + + .filter-input-value { + flex: 1; + + input { + height: 36px; + font-size: 15px; + } + } + } + } +} diff --git a/console/src/app/modules/filter-events/filter-events.component.spec.ts b/console/src/app/modules/filter-events/filter-events.component.spec.ts new file mode 100644 index 0000000000..c399b62c18 --- /dev/null +++ b/console/src/app/modules/filter-events/filter-events.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FilterEventsComponent } from './filter-events.component'; + +describe('FilterOrgComponent', () => { + let component: FilterEventsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FilterEventsComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterEventsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/modules/filter-events/filter-events.component.ts b/console/src/app/modules/filter-events/filter-events.component.ts new file mode 100644 index 0000000000..1a6ad78c7c --- /dev/null +++ b/console/src/app/modules/filter-events/filter-events.component.ts @@ -0,0 +1,330 @@ +import { ConnectedPosition, ConnectionPositionPair } from '@angular/cdk/overlay'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +import { MatLegacyOptionSelectionChange } from '@angular/material/legacy-core'; +import { MatLegacySelectChange } from '@angular/material/legacy-select'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; +import { take } from 'rxjs'; +import { ListAggregateTypesRequest, ListEventsRequest } from 'src/app/proto/generated/zitadel/admin_pb'; +import { AggregateType, EventType } from 'src/app/proto/generated/zitadel/event_pb'; +import { AdminService } from 'src/app/services/admin.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { ActionKeysType } from '../action-keys/action-keys.component'; + +export enum UserTarget { + SELF = 'self', + EXTERNAL = 'external', +} + +function dateToTs(date: Date): Timestamp { + const ts = new Timestamp(); + const milliseconds = date.getTime(); + const seconds = Math.abs(milliseconds / 1000); + const nanos = (milliseconds - seconds * 1000) * 1000 * 1000; + ts.setSeconds(seconds); + ts.setNanos(nanos); + return ts; +} + +@Component({ + selector: 'cnsl-filter-events', + templateUrl: './filter-events.component.html', + styleUrls: ['./filter-events.component.scss'], +}) +export class FilterEventsComponent implements OnInit { + public showFilter: boolean = false; + public ActionKeysType: any = ActionKeysType; + + public positions: ConnectedPosition[] = [ + new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }, 0, 10), + new ConnectionPositionPair({ originX: 'end', originY: 'bottom' }, { overlayX: 'end', overlayY: 'top' }, 0, 10), + ]; + + private request: ListEventsRequest = new ListEventsRequest(); + + public aggregateTypes: AggregateType.AsObject[] = []; + public eventTypes: Array = []; + + public isLoading: boolean = false; + + @Output() public requestChanged: EventEmitter = new EventEmitter(); + public form: FormGroup = new FormGroup({ + resourceOwnerFilterSet: new FormControl(false), + resourceOwner: new FormControl(''), + sequenceFilterSet: new FormControl(false), + sequence: new FormControl(''), + isAsc: new FormControl(false), + creationDateFilterSet: new FormControl(false), + creationDate: new FormControl(new Date()), + userFilterSet: new FormControl(false), + editorUserId: new FormControl(''), + aggregateFilterSet: new FormControl(false), + aggregateId: new FormControl(''), + aggregateTypesList: new FormControl([]), + eventTypesFilterSet: new FormControl(false), + eventTypesList: new FormControl([]), + }); + + constructor( + private adminService: AdminService, + private toast: ToastService, + private route: ActivatedRoute, + private router: Router, + ) {} + + public ngOnInit(): void { + this.loadAvailableTypes().then(() => { + this.route.queryParams.pipe(take(1)).subscribe((params) => { + this.loadAvailableTypes().then(() => { + const { filter } = params; + if (filter) { + const stringifiedFilters = filter as string; + const filters = JSON.parse(stringifiedFilters); + + if (filters.aggregateId) { + this.request.setAggregateId(filters.aggregateId); + this.aggregateId?.setValue(filters.aggregateId); + this.aggregateFilterSet?.setValue(true); + } + if (filters.creationDate) { + const milliseconds = filters.creationDate; + const date = new Date(milliseconds); + const ts = dateToTs(date); + this.request.setCreationDate(ts); + this.creationDate?.setValue(date); + this.creationDateFilterSet?.setValue(true); + } + if (filters.aggregateTypesList && filters.aggregateTypesList.length) { + const values = this.aggregateTypes.filter((agg) => filters.aggregateTypesList.includes(agg.type)); + this.request.setAggregateTypesList(filters.aggregateTypesList); + this.aggregateTypesList?.setValue(values); + this.aggregateFilterSet?.setValue(true); + } + if (filters.editorUserId) { + this.request.setEditorUserId(filters.editorUserId); + this.editorUserId?.setValue(filters.editorUserId); + this.userFilterSet?.setValue(true); + } + if (filters.resourceOwner) { + this.request.setResourceOwner(filters.resourceOwner); + this.resourceOwner?.setValue(filters.resourceOwner); + this.resourceOwnerFilterSet?.setValue(true); + } + if (filters.sequence) { + this.request.setSequence(filters.sequence); + this.sequence?.setValue(filters.sequence); + this.sequenceFilterSet?.setValue(true); + } + if (filters.isAsc) { + this.request.setAsc(filters.isAsc); + this.isAsc?.setValue(filters.isAsc); + } + if (filters.eventTypesList && filters.eventTypesList.length) { + const values = this.eventTypes.filter((ev) => filters.eventTypesList.includes(ev.type)); + this.request.setEventTypesList(filters.eventTypesList); + this.eventTypesList?.setValue(values); + this.eventTypesFilterSet?.setValue(true); + } + this.emitChange(); + } + }); + }); + }); + } + + private loadAvailableTypes(): Promise { + this.isLoading = true; + const aT = this.getAggregateTypes(); + const eT = this.getEventTypes(); + return Promise.all([aT, eT]) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + }); + } + + public reset(): void { + this.form.reset(); + this.emitChange(); + } + + public finish(): void { + this.showFilter = false; + this.emitChange(); + } + + private getAggregateTypes(): Promise { + const req = new ListAggregateTypesRequest(); + + return this.adminService + .listAggregateTypes(req) + .then((list) => { + this.aggregateTypes = list.aggregateTypesList ?? []; + }) + .catch((error) => { + this.toast.showError(error); + }); + } + + private getEventTypes(): Promise { + const req = new ListAggregateTypesRequest(); + + return this.adminService + .listEventTypes(req) + .then((list) => { + this.eventTypes = list.eventTypesList ?? []; + }) + .catch((error) => { + this.toast.showError(error); + }); + } + + public emitChange(): void { + const formValues = this.form.value; + + const constructRequest = new ListEventsRequest(); + let filterObject: any = {}; + + if (formValues.userFilterSet && formValues.editorUserId) { + constructRequest.setEditorUserId(formValues.editorUserId); + filterObject.editorUserId = formValues.editorUserId; + } + if (formValues.aggregateFilterSet && formValues.aggregateTypesList && formValues.aggregateTypesList.length) { + constructRequest.setAggregateTypesList( + formValues.aggregateTypesList.map((aggType: AggregateType.AsObject) => aggType.type), + ); + filterObject.aggregateTypesList = formValues.aggregateTypesList.map((aggType: AggregateType.AsObject) => aggType.type); + } + if (formValues.aggregateFilterSet && formValues.aggregateId) { + constructRequest.setAggregateId(formValues.aggregateId); + filterObject.aggregateId = formValues.aggregateId; + } + if (formValues.eventTypesFilterSet && formValues.eventTypesList && formValues.eventTypesList.length) { + constructRequest.setEventTypesList(formValues.eventTypesList.map((eventType: EventType.AsObject) => eventType.type)); + filterObject.eventTypesList = formValues.eventTypesList.map((eventType: EventType.AsObject) => eventType.type); + } + if (formValues.resourceOwnerFilterSet && formValues.resourceOwner) { + constructRequest.setResourceOwner(formValues.resourceOwner); + filterObject.resourceOwner = formValues.resourceOwner; + } + if (formValues.sequenceFilterSet && formValues.sequence) { + constructRequest.setSequence(formValues.sequence); + filterObject.sequence = formValues.sequence; + } + if (formValues.isAsc) { + constructRequest.setAsc(formValues.isAsc); + filterObject.isAsc = formValues.isAsc; + } + if (formValues.creationDateFilterSet && formValues.creationDate) { + const date = new Date(formValues.creationDate); + const ts = dateToTs(date); + constructRequest.setCreationDate(ts); + filterObject.creationDate = date.getTime(); + } + + this.requestChanged.emit(constructRequest); + + if (Object.keys(filterObject).length) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + ['filter']: JSON.stringify(filterObject), + }, + replaceUrl: true, + queryParamsHandling: 'merge', + skipLocationChange: false, + }); + } else { + this.router.navigate([], { + relativeTo: this.route, + replaceUrl: true, + skipLocationChange: false, + }); + } + } + + public get userFilterSet(): AbstractControl | null { + return this.form.get('userFilterSet'); + } + + public get aggregateFilterSet(): AbstractControl | null { + return this.form.get('aggregateFilterSet'); + } + + public get eventTypesFilterSet(): AbstractControl | null { + return this.form.get('eventTypesFilterSet'); + } + + public get sequence(): AbstractControl | null { + return this.form.get('sequence'); + } + + public get isAsc(): AbstractControl | null { + return this.form.get('isAsc'); + } + + public get sequenceFilterSet(): AbstractControl | null { + return this.form.get('sequenceFilterSet'); + } + + public get creationDate(): AbstractControl | null { + return this.form.get('creationDate'); + } + + public get creationDateFilterSet(): AbstractControl | null { + return this.form.get('creationDateFilterSet'); + } + + public get resourceOwnerFilterSet(): AbstractControl | null { + return this.form.get('resourceOwnerFilterSet'); + } + + public get resourceOwner(): AbstractControl | null { + return this.form.get('resourceOwner'); + } + + public get editorUserId(): AbstractControl | null { + return this.form.get('editorUserId'); + } + + public get aggregateId(): AbstractControl | null { + return this.form.get('aggregateId'); + } + + public get aggregateTypesList(): AbstractControl | null { + return this.form.get('aggregateTypesList'); + } + + public get eventTypesList(): AbstractControl | null { + return this.form.get('eventTypesList'); + } + + public get queryCount(): number { + let count = 0; + if (this.userFilterSet?.value && this.editorUserId?.value) { + ++count; + } + if (this.creationDateFilterSet?.value && this.creationDate?.value) { + ++count; + } + if (this.aggregateFilterSet?.value && this.aggregateId?.value) { + ++count; + } + if (this.sequenceFilterSet?.value && this.sequence?.value) { + ++count; + } + if (this.resourceOwnerFilterSet?.value && this.resourceOwner?.value) { + ++count; + } + if (this.aggregateFilterSet?.value && this.aggregateTypesList?.value && this.aggregateTypesList.value.length) { + ++count; + } + if (this.eventTypesFilterSet?.value && this.eventTypesList?.value && this.eventTypesList.value.length) { + ++count; + } + return count; + } +} diff --git a/console/src/app/modules/filter-events/filter-events.module.ts b/console/src/app/modules/filter-events/filter-events.module.ts new file mode 100644 index 0000000000..222ed63d18 --- /dev/null +++ b/console/src/app/modules/filter-events/filter-events.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; +import { TranslateModule } from '@ngx-translate/core'; + +import { InputModule } from '../input/input.module'; +import { FilterEventsComponent } from './filter-events.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatLegacyProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatLegacyCheckboxModule } from '@angular/material/legacy-checkbox'; +import { ActionKeysModule } from '../action-keys/action-keys.module'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { MatDatepickerModule } from '@angular/material/datepicker'; + +@NgModule({ + declarations: [FilterEventsComponent], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + InputModule, + ReactiveFormsModule, + OverlayModule, + MatDatepickerModule, + MatLegacyProgressSpinnerModule, + TranslateModule, + MatLegacyCheckboxModule, + MatSelectModule, + ActionKeysModule, + ], + exports: [FilterEventsComponent], +}) +export class FilterEventsModule {} diff --git a/console/src/app/modules/header/header.component.ts b/console/src/app/modules/header/header.component.ts index b276ba71cd..282f43c6fd 100644 --- a/console/src/app/modules/header/header.component.ts +++ b/console/src/app/modules/header/header.component.ts @@ -80,6 +80,7 @@ export class HeaderComponent implements OnDestroy { '/instance', '/settings', '/views', + '/events', '/orgs', '/settings', '/failed-events', diff --git a/console/src/app/modules/nav/nav.component.html b/console/src/app/modules/nav/nav.component.html index f13848f968..a957852c0f 100644 --- a/console/src/app/modules/nav/nav.component.html +++ b/console/src/app/modules/nav/nav.component.html @@ -37,6 +37,17 @@ + +
+ {{ 'MENU.EVENTS' | translate }} +
+
+ + +
+ +
diff --git a/console/src/app/modules/paginator/paginator.component.ts b/console/src/app/modules/paginator/paginator.component.ts index 2338283a08..764ed7c8f6 100644 --- a/console/src/app/modules/paginator/paginator.component.ts +++ b/console/src/app/modules/paginator/paginator.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { EventManager } from '@angular/platform-browser'; import { Timestamp } from 'src/app/proto/generated/google/protobuf/timestamp_pb'; export interface PageEvent { @@ -20,6 +21,9 @@ export class PaginatorComponent { @Input() public pageIndex: number = 0; @Input() public pageSizeOptions: Array = [10, 25, 50]; @Input() public hidePagination: boolean = false; + @Input() public showMoreButton: boolean = false; + @Input() public disableShowMore: boolean | null = false; + @Output() public moreRequested: EventEmitter = new EventEmitter(); @Output() public page: EventEmitter = new EventEmitter(); constructor() {} diff --git a/console/src/app/pages/events/events-routing.module.ts b/console/src/app/pages/events/events-routing.module.ts new file mode 100644 index 0000000000..6338715e66 --- /dev/null +++ b/console/src/app/pages/events/events-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EventsComponent } from './events.component'; + +const routes: Routes = [ + { + path: '', + component: EventsComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class EventsRoutingModule {} diff --git a/console/src/app/pages/events/events.component.html b/console/src/app/pages/events/events.component.html new file mode 100644 index 0000000000..4c0033188d --- /dev/null +++ b/console/src/app/pages/events/events.component.html @@ -0,0 +1,129 @@ +
+

{{ 'IAM.EVENTS.TITLE' | translate }}

+

{{ 'IAM.EVENTS.DESCRIPTION' | translate }}

+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'IAM.EVENTS.EDITOR' | translate }} + +
+ + {{ editor.displayName }} + {{ editor.service }} +
+
+
{{ 'IAM.EVENTS.AGGREGATE' | translate }} + +
+ {{ event.aggregate.id }}{{ + event.aggregate.type.localized.localizedMessage + }} +
+
+
{{ 'IAM.EVENTS.RESOURCEOWNER' | translate }} + + {{ event.aggregate.resourceOwner }} + + + {{ 'IAM.EVENTS.SEQUENCE' | translate }} + + + {{ event.sequence }} + + {{ 'IAM.EVENTS.CREATIONDATE' | translate }} + + {{ event?.creationDate | timestampToDate | localizedDate : 'EEE dd. MMM, HH:mm' }} + + {{ 'IAM.EVENTS.TYPE' | translate }} + + {{ event.type.localized.localizedMessage }} + + {{ 'IAM.EVENTS.PAYLOAD' | translate }} + + {{ payload | json }} +
+ +
+
+
+ + +
+
diff --git a/console/src/app/pages/events/events.component.scss b/console/src/app/pages/events/events.component.scss new file mode 100644 index 0000000000..d80824d327 --- /dev/null +++ b/console/src/app/pages/events/events.component.scss @@ -0,0 +1,97 @@ +@use '@angular/material' as mat; + +@mixin events-theme($theme) { + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $is-dark-theme: map-get($theme, is-dark); + $card-background-color: mat.get-color-from-palette($background, cards); + $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2)); + $primary: map-get($theme, primary); + $primary-color: mat.get-color-from-palette($primary, 500); + + .mat-column-payload { + position: relative; + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: hidden; + + .btn-wrapper { + background-color: $card-background-color; + transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + border: 1px solid $border-color; + box-sizing: border-box; + border-radius: 0.5rem; + outline: none; + box-shadow: 0 0 3px #0000001a; + height: 36px; + width: 36px; + position: absolute; + display: none; + justify-content: center; + align-items: center; + right: 0; + top: 50%; + transform: translateY(-50%); + + .open-in-dialog-btn { + width: 36px; + height: 36px; + line-height: initial; + } + } + + &:hover { + .btn-wrapper { + display: flex; + } + } + } + + .filter-button-wrapper { + display: flex; + justify-content: space-between; + position: relative; + user-select: none; + margin-left: 1rem; + + .filter-count { + font-size: 14px; + color: $primary-color; + margin-left: 0.5rem; + } + } +} + +.events-title { + margin: 2rem 0 0 0; +} + +.events-desc { + font-size: 14px; +} + +.editor-row { + display: flex; + flex-direction: column; + padding: 2px 0; + + .name, + .id { + margin-bottom: 0.25rem; + } + + .state { + align-self: flex-start; + } +} + +.aggregate-row { + display: flex; + flex-direction: column; + align-items: flex-start; + + .id { + margin-bottom: 0.25rem; + } +} diff --git a/console/src/app/pages/events/events.component.spec.ts b/console/src/app/pages/events/events.component.spec.ts new file mode 100644 index 0000000000..54ee8a2918 --- /dev/null +++ b/console/src/app/pages/events/events.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { EventsComponent } from './events.component'; + +describe('EventsComponent', () => { + let component: EventsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [EventsComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EventsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/console/src/app/pages/events/events.component.ts b/console/src/app/pages/events/events.component.ts new file mode 100644 index 0000000000..28f1f0ce82 --- /dev/null +++ b/console/src/app/pages/events/events.component.ts @@ -0,0 +1,198 @@ +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MatSort, Sort } from '@angular/material/sort'; +import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; +import { BehaviorSubject, distinctUntilChanged, Observable, Subject, takeUntil } from 'rxjs'; +import { ListEventsRequest, ListEventsResponse } from 'src/app/proto/generated/zitadel/admin_pb'; +import { AdminService } from 'src/app/services/admin.service'; +import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service'; +import { Event } from 'src/app/proto/generated/zitadel/event_pb'; +import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { ToastService } from 'src/app/services/toast.service'; +import { DisplayJsonDialogComponent } from 'src/app/modules/display-json-dialog/display-json-dialog.component'; +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; + +enum EventFieldName { + EDITOR = 'editor', + AGGREGATE = 'aggregate', + RESOURCEOWNER = 'resourceOwner', + SEQUENCE = 'sequence', + CREATIONDATE = 'creationDate', + TYPE = 'type', + PAYLOAD = 'payload', +} + +type LoadRequest = { + req: ListEventsRequest; + override: boolean; +}; + +@Component({ + selector: 'cnsl-events', + templateUrl: './events.component.html', + styleUrls: ['./events.component.scss'], +}) +export class EventsComponent implements OnDestroy { + public INITPAGESIZE = 20; + public sortAsc = false; + private destroy$: Subject = new Subject(); + + public displayedColumns: string[] = [ + EventFieldName.TYPE, + EventFieldName.AGGREGATE, + EventFieldName.RESOURCEOWNER, + EventFieldName.EDITOR, + EventFieldName.SEQUENCE, + EventFieldName.CREATIONDATE, + EventFieldName.PAYLOAD, + ]; + + public currentRequest$: BehaviorSubject = new BehaviorSubject({ + req: new ListEventsRequest().setLimit(this.INITPAGESIZE), + override: true, + }); + + @ViewChild(MatSort) public sort!: MatSort; + @ViewChild(PaginatorComponent) public paginator!: PaginatorComponent; + public dataSource: MatTableDataSource = new MatTableDataSource([]); + + public _done: BehaviorSubject = new BehaviorSubject(false); + public done: Observable = this._done.asObservable(); + + public _loading: BehaviorSubject = new BehaviorSubject(false); + + private _data: BehaviorSubject = new BehaviorSubject([]); + + constructor( + private adminService: AdminService, + private breadcrumbService: BreadcrumbService, + private _liveAnnouncer: LiveAnnouncer, + private toast: ToastService, + private dialog: MatDialog, + ) { + const breadcrumbs = [ + new Breadcrumb({ + type: BreadcrumbType.INSTANCE, + name: 'Instance', + routerLink: ['/instance'], + }), + ]; + this.breadcrumbService.setBreadcrumb(breadcrumbs); + + this.currentRequest$ + .pipe( + // this would compare the requests if a duplicate and redundant request would be made + // distinctUntilChanged(({ req: prev }, { req: next }) => { + // return JSON.stringify(prev.toObject()) === JSON.stringify(next.toObject()); + // }), + takeUntil(this.destroy$), + ) + .subscribe(({ req, override }) => { + this._loading.next(true); + this.adminService + .listEvents(req) + .then((res: ListEventsResponse) => { + if (override) { + this._data = new BehaviorSubject([]); + this.dataSource = new MatTableDataSource([]); + } + + const eventList = res.getEventsList(); + this._data.next(eventList); + + const concat = this.dataSource.data.concat(eventList); + this.dataSource = new MatTableDataSource(concat); + + this._loading.next(false); + + if (eventList.length === 0) { + this._done.next(true); + } else { + this._done.next(false); + } + }) + .catch((error) => { + this.toast.showError(error); + this._loading.next(false); + this._data.next([]); + }); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + public loadEvents(filteredRequest: ListEventsRequest, override: boolean = false): void { + this.currentRequest$.next({ req: filteredRequest, override }); + } + + public refresh(): void { + const req = new ListEventsRequest(); + req.setLimit(this.paginator.pageSize); + } + + public sortChange(sortState: Sort) { + if (sortState.direction && sortState.active) { + this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`); + this.sortAsc = sortState.direction === 'asc'; + + const { req } = this.currentRequest$.value; + + req.setLimit(this.INITPAGESIZE); + req.setAsc(this.sortAsc ? true : false); + + this.loadEvents(req, true); + } else { + this._liveAnnouncer.announce('Sorting cleared'); + } + } + + public openDialog(event: Event): void { + this.dialog.open(DisplayJsonDialogComponent, { + data: { + event: event, + }, + width: '450px', + }); + } + + public more(): void { + const sequence = this.getCursor(); + const { req } = this.currentRequest$.value; + req.setSequence(sequence); + this.loadEvents(req); + } + + public filterChanged(filterRequest: ListEventsRequest) { + const req = new ListEventsRequest(); + req.setLimit(this.INITPAGESIZE); + req.setAsc(this.sortAsc ? true : false); + + req.setAggregateTypesList(filterRequest.getAggregateTypesList()); + req.setAggregateId(filterRequest.getAggregateId()); + req.setEventTypesList(filterRequest.getEventTypesList()); + req.setEditorUserId(filterRequest.getEditorUserId()); + req.setResourceOwner(filterRequest.getResourceOwner()); + req.setSequence(filterRequest.getSequence()); + req.setCreationDate(filterRequest.getCreationDate()); + const isAsc: boolean = filterRequest.getAsc(); + req.setAsc(isAsc); + if (this.sortAsc !== isAsc) { + this.sort.sort({ id: 'sequence', start: isAsc ? 'asc' : 'desc', disableClear: true }); + } + + this.loadEvents(req, true); + } + + private getCursor(): number { + const current = this._data.value; + + if (current.length) { + const sequence = current[current.length - 1].toObject().sequence; + return sequence; + } + return 0; + } +} diff --git a/console/src/app/pages/events/events.module.ts b/console/src/app/pages/events/events.module.ts new file mode 100644 index 0000000000..1a1de9bbc3 --- /dev/null +++ b/console/src/app/pages/events/events.module.ts @@ -0,0 +1,65 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner'; +import { MatSortModule } from '@angular/material/sort'; +import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; +import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module'; +import { CardModule } from 'src/app/modules/card/card.module'; +import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; +import { InputModule } from 'src/app/modules/input/input.module'; +import { PaginatorModule } from 'src/app/modules/paginator/paginator.module'; +import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module'; +import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; +import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module'; +import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module'; + +import { EventsComponent } from './events.component'; +import { EventsRoutingModule } from './events-routing.module'; +import { FilterEventsModule } from 'src/app/modules/filter-events/filter-events.module'; +import { AvatarModule } from 'src/app/modules/avatar/avatar.module'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module'; +import { DisplayJsonDialogModule } from 'src/app/modules/display-json-dialog/display-json-dialog.module'; +import { MatLegacyDialogModule } from '@angular/material/legacy-dialog'; +import { ToObjectPipeModule } from 'src/app/pipes/to-object/to-object.module'; +import { ToPayloadPipeModule } from 'src/app/pipes/to-payload/to-payload.module'; + +@NgModule({ + declarations: [EventsComponent], + imports: [ + EventsRoutingModule, + CommonModule, + TableActionsModule, + MatIconModule, + CardModule, + FilterEventsModule, + ToObjectPipeModule, + ToPayloadPipeModule, + HasRolePipeModule, + MatLegacyDialogModule, + MatButtonModule, + CopyToClipboardModule, + InputModule, + TranslateModule, + InfoSectionModule, + AvatarModule, + MatTooltipModule, + MatProgressSpinnerModule, + RefreshTableModule, + ActionKeysModule, + PaginatorModule, + TimestampToDatePipeModule, + LocalizedDatePipeModule, + DisplayJsonDialogModule, + MatTableModule, + MatSortModule, + OverlayModule, + ], + exports: [], +}) +export default class IamViewsModule {} diff --git a/console/src/app/pipes/to-object/to-object.module.ts b/console/src/app/pipes/to-object/to-object.module.ts new file mode 100644 index 0000000000..235d2607c7 --- /dev/null +++ b/console/src/app/pipes/to-object/to-object.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ToObjectPipe } from './to-object.pipe'; + +@NgModule({ + declarations: [ToObjectPipe], + imports: [CommonModule], + exports: [ToObjectPipe], +}) +export class ToObjectPipeModule {} diff --git a/console/src/app/pipes/to-object/to-object.pipe.ts b/console/src/app/pipes/to-object/to-object.pipe.ts new file mode 100644 index 0000000000..d9159a1fb7 --- /dev/null +++ b/console/src/app/pipes/to-object/to-object.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Struct } from 'google-protobuf/google/protobuf/struct_pb'; +import { Event } from 'src/app/proto/generated/zitadel/event_pb'; + +@Pipe({ + name: 'toobject', +}) +export class ToObjectPipe implements PipeTransform { + public transform(value: Event | Struct): any { + return value.toObject(); + } +} diff --git a/console/src/app/pipes/to-payload/to-payload.module.ts b/console/src/app/pipes/to-payload/to-payload.module.ts new file mode 100644 index 0000000000..1c70966f6e --- /dev/null +++ b/console/src/app/pipes/to-payload/to-payload.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { ToPayloadPipe } from './to-payload.pipe'; + +@NgModule({ + declarations: [ToPayloadPipe], + imports: [CommonModule], + exports: [ToPayloadPipe], +}) +export class ToPayloadPipeModule {} diff --git a/console/src/app/pipes/to-payload/to-payload.pipe.ts b/console/src/app/pipes/to-payload/to-payload.pipe.ts new file mode 100644 index 0000000000..feb8480e50 --- /dev/null +++ b/console/src/app/pipes/to-payload/to-payload.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { JavaScriptValue, Struct } from 'google-protobuf/google/protobuf/struct_pb'; +import { Event } from 'src/app/proto/generated/zitadel/event_pb'; + +@Pipe({ + name: 'topayload', +}) +export class ToPayloadPipe implements PipeTransform { + public transform(value: Event): JavaScriptValue | string { + const pl = value.getPayload(); + if (pl) { + return pl.toJavaScript(); + } else { + return ''; + } + } +} diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index 93f42177ef..2244372bf0 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -204,6 +204,12 @@ import { GetSecurityPolicyResponse, SetSecurityPolicyRequest, SetSecurityPolicyResponse, + ListEventsResponse, + ListEventsRequest, + ListEventTypesRequest, + ListEventTypesResponse, + ListAggregateTypesRequest, + ListAggregateTypesResponse, GetNotificationPolicyRequest, GetNotificationPolicyResponse, UpdateNotificationPolicyRequest, @@ -227,6 +233,18 @@ import { GrpcService } from './grpc.service'; export class AdminService { constructor(private readonly grpcService: GrpcService) {} + public listEvents(req: ListEventsRequest): Promise { + return this.grpcService.admin.listEvents(req, null).then((resp) => resp); + } + + public listEventTypes(req: ListEventTypesRequest): Promise { + return this.grpcService.admin.listEventTypes(req, null).then((resp) => resp.toObject()); + } + + public listAggregateTypes(req: ListAggregateTypesRequest): Promise { + return this.grpcService.admin.listAggregateTypes(req, null).then((resp) => resp.toObject()); + } + public getSupportedLanguages(): Promise { const req = new GetSupportedLanguagesRequest(); return this.grpcService.admin.getSupportedLanguages(req, null).then((resp) => resp.toObject()); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 0af5183ba6..12f3504f19 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -3,7 +3,8 @@ "PAGINATOR": { "PREVIOUS": "Zurück", "NEXT": "Weiter", - "COUNT": "Ergebnisse" + "COUNT": "Ergebnisse", + "MORE": "mehr" }, "FOOTER": { "LINKS": { @@ -49,6 +50,7 @@ "INSTANCEOVERVIEW": "Instanz", "ORGS": "Organisationen", "VIEWS": "Views", + "EVENTS": "Events", "FAILEDEVENTS": "Failed Events", "ORGANIZATION": "Organisation", "DOMAINS": "Domains", @@ -758,6 +760,56 @@ "DELETE": "Entfernen", "DELETESUCCESS": "Gescheiterte Events entfernt." }, + "EVENTS": { + "TITLE": "Events", + "DESCRIPTION": "Diese Ansicht zeigt alle vorhandenen Events.", + "EDITOR": "Editor", + "EDITORID": "Editor ID", + "AGGREGATE": "Aggregate", + "AGGREGATEID": "Aggregate ID", + "AGGREGATETYPE": "Aggregate Typ", + "RESOURCEOWNER": "Resource Owner", + "SEQUENCE": "Sequenz", + "CREATIONDATE": "Erstelldatum", + "TYPE": "Typ", + "PAYLOAD": "Payload", + "FILTERS": { + "BTN": "Filter", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Nach Editor filtern" + }, + "AGGREGATE": { + "TYPELABEL": "Aggregate Typ", + "IDLABEL": "ID", + "CHECKBOX": "Nach Aggregate filtern" + }, + "TYPE": { + "TYPELABEL": "Typ", + "CHECKBOX": "Nach Typ filtern" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Nach Resource Owner filtern" + }, + "SEQUENCE": { + "LABEL": "Sequenz", + "CHECKBOX": "Nach Sequenz filtern", + "SORT": "Sortierung", + "ASC": "aufsteigend", + "DESC": "absteigend" + }, + "CREATIONDATE": { + "LABEL": "Erstelldatum", + "CHECKBOX": "Nach Erstelldatum filtern" + }, + "OTHER": "weiterer", + "OTHERS": "weitere" + }, + "DIALOG": { + "TITLE": "Event Detail" + } + }, "TOAST": { "MEMBERREMOVED": "Manager entfernt.", "MEMBERSADDED": "Manager hinzugefügt.", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 33e80560d5..5250906fdc 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -3,7 +3,8 @@ "PAGINATOR": { "PREVIOUS": "Previous", "NEXT": "Next", - "COUNT": "Total Results" + "COUNT": "Total Results", + "MORE": "More" }, "FOOTER": { "LINKS": { @@ -49,6 +50,7 @@ "INSTANCEOVERVIEW": "Instance", "ORGS": "Organizations", "VIEWS": "Views", + "EVENTS": "Events", "FAILEDEVENTS": "Failed Events", "ORGANIZATION": "Organization", "DOMAINS": "Domains", @@ -758,6 +760,56 @@ "DELETE": "Remove", "DELETESUCCESS": "Failed events removed." }, + "EVENTS": { + "TITLE": "Events", + "DESCRIPTION": "This view shows all occured events.", + "EDITOR": "Editor", + "EDITORID": "Editor ID", + "AGGREGATE": "Aggregate", + "AGGREGATEID": "Aggregate ID", + "AGGREGATETYPE": "Aggregate Type", + "RESOURCEOWNER": "Resource Owner", + "SEQUENCE": "Sequence", + "CREATIONDATE": "Created At", + "TYPE": "Type", + "PAYLOAD": "Payload", + "FILTERS": { + "BTN": "Filter", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Filter by Editor" + }, + "AGGREGATE": { + "TYPELABEL": "Aggregate Type", + "IDLABEL": "ID", + "CHECKBOX": "Filter by Aggregate" + }, + "TYPE": { + "TYPELABEL": "Type", + "CHECKBOX": "Filter by Type" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Filter by Resource Owner" + }, + "SEQUENCE": { + "LABEL": "Sequence", + "CHECKBOX": "Filter by Sequence", + "SORT": "Sorting", + "ASC": "Ascending", + "DESC": "Descending" + }, + "CREATIONDATE": { + "LABEL": "Creation Date", + "CHECKBOX": "Filter by Creation Date" + }, + "OTHER": "other", + "OTHERS": "others" + }, + "DIALOG": { + "TITLE": "Event Detail" + } + }, "TOAST": { "MEMBERREMOVED": "Manager removed.", "MEMBERSADDED": "Managers added.", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 249f4f3001..e581daa5fc 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -3,7 +3,8 @@ "PAGINATOR": { "PREVIOUS": "Précédent", "NEXT": "Suivant", - "COUNT": "Résultats totaux" + "COUNT": "Résultats totaux", + "MORE": "plus" }, "FOOTER": { "LINKS": { @@ -49,6 +50,7 @@ "INSTANCEOVERVIEW": "Instance", "ORGS": "Organisations", "VIEWS": "Vues", + "EVENTS": "Événements", "FAILEDEVENTS": "Événements échoués", "ORGANIZATION": "Organisation", "DOMAINS": "Domaines", @@ -758,6 +760,56 @@ "DELETE": "Supprimer", "DELETESUCCESS": "Événements échoués supprimés." }, + "EVENTS": { + "TITLE": "Événements", + "DESCRIPTION": "Cette vue montre les événements de ZITADEL.", + "EDITOR": "Éditeur", + "EDITORID": "Editor ID", + "AGGREGATE": "agrégat", + "AGGREGATEID": "agrégat ID", + "AGGREGATETYPE": "type d'agrégat", + "RESOURCEOWNER": "Propriétaire", + "SEQUENCE": "séquence", + "CREATIONDATE": "Créé à", + "TYPE": "Type", + "PAYLOAD": "Payload", + "FILTERS": { + "BTN": "Filtre", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Filtrer par éditeur" + }, + "AGGREGATE": { + "TYPELABEL": "Aggregate Type", + "IDLABEL": "ID", + "CHECKBOX": "Filtrer par agrégat" + }, + "TYPE": { + "TYPELABEL": "Type", + "CHECKBOX": "Filtrer par type" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Filtrer par propriétaire" + }, + "SEQUENCE": { + "LABEL": "séquence", + "CHECKBOX": "Filtrer par séquence", + "SORT": "Triage", + "ASC": "Ascendant", + "DESC": "Descendant" + }, + "CREATIONDATE": { + "LABEL": "Date de création", + "CHECKBOX": "Filtrer par date de création" + }, + "OTHER": "autre", + "OTHERS": "autres" + }, + "DIALOG": { + "TITLE": "Détail de l'événement" + } + }, "TOAST": { "MEMBERREMOVED": "Gestionnaire supprimé.", "MEMBERSADDED": "Gestionnaires ajoutés.", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 4f599798bd..f1ed6612e2 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -3,7 +3,8 @@ "PAGINATOR": { "PREVIOUS": "Precedente", "NEXT": "Avanti", - "COUNT": "Risultati totali" + "COUNT": "Risultati totali", + "MORE": "avanti" }, "FOOTER": { "LINKS": { @@ -49,6 +50,7 @@ "INSTANCEOVERVIEW": "Istanza", "ORGS": "Organizzazioni", "VIEWS": "Views", + "EVENTS": "Eventi", "FAILEDEVENTS": "Eventi falliti", "ORGANIZATION": "Organizzazione", "DOMAINS": "Domini", @@ -758,6 +760,56 @@ "DELETE": "Rimuovi", "DELETESUCCESS": "Eventi falliti rimossi." }, + "EVENTS": { + "TITLE": "Eventi", + "DESCRIPTION": "Questa vista mostra tutti gli eventi in arrivo", + "EDITOR": "Editore", + "EDITORID": "ID Editore", + "AGGREGATE": "Aggregato", + "AGGREGATEID": "ID aggregato", + "AGGREGATETYPE": "Tipo aggregato", + "RESOURCEOWNER": "Resouce owner", + "SEQUENCE": "Sequenza", + "CREATIONDATE": "Creato", + "TYPE": "Tipo", + "PAYLOAD": "Payload", + "FILTERS": { + "BTN": "Filtra", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "filtra per editore" + }, + "AGGREGATE": { + "TYPELABEL": "Aggregate Type", + "IDLABEL": "ID", + "CHECKBOX": "filtra per aggregato" + }, + "TYPE": { + "TYPELABEL": "Type", + "CHECKBOX": "Filtra per tipo" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Filter per Resource Owner" + }, + "SEQUENCE": { + "LABEL": "Sequence", + "CHECKBOX": "Filter per sequenza", + "SORT": "", + "ASC": "Ascending", + "DESC": "Descending" + }, + "CREATIONDATE": { + "LABEL": "Creation Date", + "CHECKBOX": "Filter by Creation Date" + }, + "OTHER": "altro", + "OTHERS": "altri" + }, + "DIALOG": { + "TITLE": "Dettaglio dell'evento" + } + }, "TOAST": { "MEMBERREMOVED": "Manager rimosso.", "MEMBERSADDED": "I manager sono stati aggiunti con successo.", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 93ed1bd6d6..e520daa767 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -3,7 +3,8 @@ "PAGINATOR": { "PREVIOUS": "上一页", "NEXT": "下一页", - "COUNT": "总数" + "COUNT": "总数", + "MORE": "显示更多" }, "FOOTER": { "LINKS": { @@ -49,6 +50,7 @@ "INSTANCEOVERVIEW": "实例", "ORGS": "组织", "VIEWS": "数据表", + "EVENTS": "活动", "FAILEDEVENTS": "失败事件", "ORGANIZATION": "组织", "DOMAINS": "域名", @@ -758,6 +760,56 @@ "DELETE": "删除", "DELETESUCCESS": "失败的事件被移除。" }, + "EVENTS": { + "TITLE": "活动", + "DESCRIPTION": "该视图显示所有传入的事件", + "EDITOR": "编辑", + "EDITORID": "编者号", + "AGGREGATE": "总数", + "AGGREGATEID": "汇总的ID", + "AGGREGATETYPE": "骨料类型", + "RESOURCEOWNER": "所有者", + "SEQUENCE": "序列", + "CREATIONDATE": "创建日期", + "TYPE": "类型", + "PAYLOAD": "有效载荷", + "FILTERS": { + "BTN": "过滤器", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "按用户过滤" + }, + "AGGREGATE": { + "TYPELABEL": "骨料类型", + "IDLABEL": "ID", + "CHECKBOX": "按总量过滤" + }, + "TYPE": { + "TYPELABEL": "类型", + "CHECKBOX": "按类型过滤" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "按资源所有者过滤" + }, + "SEQUENCE": { + "LABEL": "序列", + "CHECKBOX": "按顺序过滤", + "SORT": "分拣", + "ASC": "上升中", + "DESC": "下降" + }, + "CREATIONDATE": { + "LABEL": "创建日期", + "CHECKBOX": "按创建日期过滤" + }, + "OTHER": "其他", + "OTHERS": "其他" + }, + "DIALOG": { + "TITLE": "事件的细节" + } + }, "TOAST": { "MEMBERREMOVED": "管理者已删除。", "MEMBERSADDED": "已添加多个管理者。", diff --git a/console/src/assets/mdi/arrow-expand.svg b/console/src/assets/mdi/arrow-expand.svg new file mode 100644 index 0000000000..6e1f0076c9 --- /dev/null +++ b/console/src/assets/mdi/arrow-expand.svg @@ -0,0 +1 @@ +arrow-expand \ No newline at end of file diff --git a/console/src/component-themes.scss b/console/src/component-themes.scss index 86e2cfaebc..a0756bcaee 100644 --- a/console/src/component-themes.scss +++ b/console/src/component-themes.scss @@ -25,6 +25,7 @@ @import 'src/app/pages/projects/granted-projects/granted-project-detail/granted-project-detail.component'; @import 'src/app/pages/projects/apps/app-detail/app-detail.component'; @import 'src/app/pages/projects/apps/redirect-uris/redirect-uris.component'; +@import 'src/app/modules/filter-events/filter-events.component'; @import 'src/app/modules/top-view/top-view.component'; @import 'src/app/pages/projects/projects.component'; @import 'src/app/modules/edit-text/edit-text.component.scss'; @@ -52,6 +53,7 @@ @import 'src/app/modules/idp-create/idp-type-radio/idp-type-radio.component.scss'; @import 'src/app/pages/actions/add-action-dialog/add-action-dialog.component'; @import 'src/app/modules/project-role-chip/project-role-chip.component'; +@import 'src/app/pages/events/events.component'; @import 'src/app/pages/home/home.component.scss'; @import 'src/app/modules/policies/security-policy/security-policy.component.scss'; @import 'src/app/modules/search-user-autocomplete/search-user-autocomplete.component.scss'; @@ -67,6 +69,7 @@ @include nav-toggle-theme($theme); @include header-theme($theme); @include app-type-radio-theme($theme); + @include events-theme($theme); @include projects-theme($theme); @include idp-type-radio-theme($theme); @include top-view-theme($theme); @@ -76,6 +79,7 @@ @include search-user-autocomplete-theme($theme); @include project-role-chips-theme($theme); @include card-theme($theme); + @include filter-events-theme($theme); @include footer-theme($theme); @include table-theme($theme); @include detail-layout-theme($theme);