feat(console): events (#5092)

Shows the list of events on instance level
This commit is contained in:
Max Peintner 2023-02-01 09:54:00 +01:00 committed by GitHub
parent 5ca7e83f0a
commit e9d5d1dcaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1790 additions and 13 deletions

View File

@ -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'),

View File

@ -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'),

View File

@ -63,7 +63,11 @@ export class ChangesComponent implements OnInit, OnDestroy {
private _data: BehaviorSubject<any> = new BehaviorSubject([]);
loading: Observable<boolean> = this._loading.asObservable();
public data!: Observable<MappedChange[]>;
public data: Observable<MappedChange[]> = this._data.asObservable().pipe(
scan((acc, val) => {
return false ? val.concat(acc) : acc.concat(val);
}),
);
public changes!: ListChanges;
private destroyed$: Subject<void> = 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 {

View File

@ -0,0 +1,79 @@
<h2 class="title" mat-dialog-title>{{ 'IAM.EVENTS.DIALOG.TITLE' | translate }}</h2>
<div mat-dialog-content>
<div class="event-data-column" *ngIf="event && (event | toobject) as event">
<div class="data-row" *ngIf="event && event.type && event.type.localized && event.type.localized.localizedMessage">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.TYPE' | translate }}</span>
<span>{{ event.type.localized.localizedMessage }}</span>
</div>
<div class="data-row" *ngIf="event && event.creationDate">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.CREATIONDATE' | translate }}</span>
<span>{{ event.creationDate | timestampToDate | localizedDate : 'EEE dd. MMM, HH:mm' }}</span>
</div>
<div class="data-row" *ngIf="event && event.aggregate && event.aggregate.id">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.AGGREGATEID' | translate }}</span>
<span>{{ event.aggregate.id }}</span>
</div>
<div class="data-row" *ngIf="event && event.aggregate && event.aggregate.resourceOwner">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.RESOURCEOWNER' | translate }}</span>
<span>{{ event.aggregate.resourceOwner }}</span>
</div>
<div
class="data-row"
*ngIf="
event &&
event.aggregate &&
event.aggregate.type &&
event.aggregate.type.localized &&
event.aggregate.type.localized.localizedMessage
"
>
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.AGGREGATETYPE' | translate }}</span>
<span class="state aggregate-type">{{ event.aggregate.type.localized.localizedMessage }}</span>
</div>
<div class="data-row" *ngIf="event && event.editor?.userId">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.EDITORID' | translate }}</span>
<span>{{ event.editor?.userId }}</span>
</div>
<div class="data-row" *ngIf="event && event.editor?.displayName">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.EDITOR' | translate }}</span>
<span>{{ event.editor?.displayName }}</span>
</div>
<div class="data-row" *ngIf="event && event.sequence">
<span class="label cnsl-secondary-text">{{ 'IAM.EVENTS.SEQUENCE' | translate }}</span>
<span>{{ event.sequence }}</span>
</div>
</div>
<div class="code" *ngIf="opened$ | async">
<ngx-codemirror
*ngIf="event"
[ngModel]="event | topayload | json"
[options]="{
height: 'auto',
readOnly: true,
lineNumbers: true,
lineWrapping: true,
indentWithTabs: false,
tabSize: 2,
theme: 'material',
mode: {
name: 'javascript',
json: true,
statementIndent: 2
}
}"
></ngx-codemirror>
</div>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
{{ 'ACTIONS.CANCEL' | translate }}
</button>
</div>

View File

@ -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;
}
}

View File

@ -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<DisplayJsonDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DisplayJsonDialogComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DisplayJsonDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<DisplayJsonDialogComponent>, @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);
}
}

View File

@ -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 {}

View File

@ -0,0 +1,195 @@
<div class="filter-button-wrapper">
<button
mat-stroked-button
cdkOverlayOrigin
(click)="showFilter = !showFilter"
class="cnsl-action-button"
#triggereventfilter="cdkOverlayOrigin"
>
<i class="las la-filter no-margin"></i>
<span>{{ 'ACTIONS.FILTER' | translate }}</span>
<span *ngIf="queryCount" class="filter-count">{{ queryCount }}</span>
<cnsl-action-keys [doNotUseContrast]="true" [type]="ActionKeysType.FILTER" (actionTriggered)="showFilter = !showFilter">
</cnsl-action-keys>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayHasBackdrop]="true"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayOrigin]="triggereventfilter"
[cdkConnectedOverlayOpen]="showFilter"
cdkConnectedOverlayBackdropClass="transparent-backdrop"
(backdropClick)="showFilter = false"
(detach)="showFilter = false"
>
<div class="filter-events-wrapper">
<div class="filter-events-top">
<button mat-stroked-button type="button" (click)="reset()">{{ 'ACTIONS.RESET' | translate }}</button>
<span class="filter-middle">{{ 'FILTER.TITLE' | translate }}</span>
<button mat-raised-button color="primary" type="button" (click)="finish()">
{{ 'ACTIONS.FINISH' | translate }}
</button>
</div>
<form *ngIf="form" [formGroup]="form" (ngSubmit)="emitChange()">
<div *ngIf="isLoading" class="sp_wrapper"><mat-spinner diameter="20"></mat-spinner></div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox id="userFilterSet" name="userFilterSet" class="cb" formControlName="userFilterSet"
>{{ 'IAM.EVENTS.FILTERS.USER.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="userFilterSet?.value">
<cnsl-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.USER.IDLABEL' | translate }}</cnsl-label>
<input cnslInput id="editorUserId" name="editorUserId" formControlName="editorUserId" />
</cnsl-form-field>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox id="aggregateFilterSet" name="aggregateFilterSet" class="cb" formControlName="aggregateFilterSet"
>{{ 'IAM.EVENTS.FILTERS.AGGREGATE.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="aggregateFilterSet?.value">
<cnsl-form-field class="aggregate-type-select">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.AGGREGATE.TYPELABEL' | translate }}</cnsl-label>
<mat-select
id="aggregateTypesList"
name="aggregateTypesList"
formControlName="aggregateTypesList"
[multiple]="true"
>
<mat-select-trigger>
<span *ngIf="aggregateTypesList?.value && aggregateTypesList?.value.length">{{
aggregateTypesList?.value[0]?.localized?.localizedMessage || ''
}}</span>
<small *ngIf="(aggregateTypesList?.value?.length || 0) > 1" class="cnsl-secondary-text">
(+{{ (aggregateTypesList?.value?.length || 0) - 1 }}
{{ aggregateTypesList?.value?.length === 2 ? 'other' : 'others' }})
</small>
</mat-select-trigger>
<mat-option *ngFor="let aggregate of aggregateTypes" [value]="aggregate">
<span *ngIf="aggregate.localized && aggregate.localized.localizedMessage">
{{ aggregate.localized.localizedMessage }}
</span>
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.AGGREGATE.IDLABEL' | translate }}</cnsl-label>
<input id="aggregateId" formControlName="aggregateId" cnslInput name="aggregateId" />
</cnsl-form-field>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox
id="eventTypesFilterSet"
name="eventTypesFilterSet"
class="cb"
formControlName="eventTypesFilterSet"
>{{ 'IAM.EVENTS.FILTERS.TYPE.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="eventTypesFilterSet?.value">
<cnsl-form-field class="event-types-select">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.TYPE.TYPELABEL' | translate }}</cnsl-label>
<mat-select id="eventTypesList" name="eventTypesList" formControlName="eventTypesList" [multiple]="true">
<mat-select-trigger>
<span *ngIf="eventTypesList?.value && eventTypesList?.value.length">{{
eventTypesList?.value[0]?.localized?.localizedMessage || ''
}}</span>
<small *ngIf="(eventTypesList?.value?.length || 0) > 1" class="cnsl-secondary-text">
(+{{ (eventTypesList?.value?.length || 0) - 1 }}
{{ eventTypesList?.value?.length === 2 ? 'other' : 'others' }})
</small>
</mat-select-trigger>
<mat-option *ngFor="let eventType of eventTypes" [value]="eventType">
<ng-container *ngIf="eventType.localized && eventType.localized.localizedMessage">
{{ eventType.localized.localizedMessage }}
</ng-container>
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox
id="resourceOwnerFilterSet"
name="resourceOwnerFilterSet"
class="cb"
formControlName="resourceOwnerFilterSet"
>{{ 'IAM.EVENTS.FILTERS.RESOURCEOWNER.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="resourceOwnerFilterSet?.value">
<cnsl-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.RESOURCEOWNER.LABEL' | translate }}</cnsl-label>
<input cnslInput id="resourceOwner" name="resourceOwner" formControlName="resourceOwner" />
</cnsl-form-field>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox id="sequenceFilterSet" name="sequenceFilterSet" class="cb" formControlName="sequenceFilterSet"
>{{ 'IAM.EVENTS.FILTERS.SEQUENCE.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="sequenceFilterSet?.value">
<cnsl-form-field class="aggregate-type-select">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.SEQUENCE.SORT' | translate }}</cnsl-label>
<mat-select id="isAsc" name="isAsc" formControlName="isAsc">
<mat-option [value]="false"> {{ 'IAM.EVENTS.FILTERS.SEQUENCE.DESC' | translate }} </mat-option>
<mat-option [value]="true">{{ 'IAM.EVENTS.FILTERS.SEQUENCE.ASC' | translate }} </mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.SEQUENCE.LABEL' | translate }}</cnsl-label>
<input cnslInput id="sequence" name="sequence" formControlName="sequence" />
</cnsl-form-field>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox
id="creationDateFilterSet"
name="creationDateFilterSet"
class="cb"
formControlName="creationDateFilterSet"
>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.CHECKBOX' | translate }}
</mat-checkbox>
</div>
<div class="filter-events-sub" *ngIf="creationDateFilterSet?.value">
<cnsl-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL' | translate }}</cnsl-label>
<input
cnslInput
id="creationDate"
name="creationDate"
[matDatepicker]="picker"
formControlName="creationDate"
/>
<mat-datepicker-toggle style="top: 0" cnslSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</cnsl-form-field>
</div>
</div>
</form>
</div>
</ng-template>
</div>

View File

@ -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;
}
}
}
}
}

View File

@ -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<FilterEventsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilterEventsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterEventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<EventType.AsObject> = [];
public isLoading: boolean = false;
@Output() public requestChanged: EventEmitter<ListEventsRequest> = new EventEmitter();
public form: FormGroup = new FormGroup({
resourceOwnerFilterSet: new FormControl(false),
resourceOwner: new FormControl(''),
sequenceFilterSet: new FormControl(false),
sequence: new FormControl(''),
isAsc: new FormControl<boolean>(false),
creationDateFilterSet: new FormControl(false),
creationDate: new FormControl<Date>(new Date()),
userFilterSet: new FormControl(false),
editorUserId: new FormControl(''),
aggregateFilterSet: new FormControl(false),
aggregateId: new FormControl(''),
aggregateTypesList: new FormControl<AggregateType.AsObject[]>([]),
eventTypesFilterSet: new FormControl(false),
eventTypesList: new FormControl<EventType.AsObject[]>([]),
});
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<void> {
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<void> {
const req = new ListAggregateTypesRequest();
return this.adminService
.listAggregateTypes(req)
.then((list) => {
this.aggregateTypes = list.aggregateTypesList ?? [];
})
.catch((error) => {
this.toast.showError(error);
});
}
private getEventTypes(): Promise<void> {
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;
}
}

View File

@ -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 {}

View File

@ -80,6 +80,7 @@ export class HeaderComponent implements OnDestroy {
'/instance',
'/settings',
'/views',
'/events',
'/orgs',
'/settings',
'/failed-events',

View File

@ -37,6 +37,17 @@
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"
[routerLinkActive]="['active']"
[routerLink]="['/events']"
>
<div class="c_label">
<span> {{ 'MENU.EVENTS' | translate }} </span>
</div>
</a>
<a
class="nav-item"
[routerLinkActiveOptions]="{ exact: false }"

View File

@ -26,4 +26,10 @@
{{ 'PAGINATOR.NEXT' | translate }}
</button>
</div>
<div class="row" *ngIf="showMoreButton">
<button (click)="moreRequested.emit()" mat-stroked-button [disabled]="disableShowMore">
{{ 'PAGINATOR.MORE' | translate }}
</button>
</div>
</div>

View File

@ -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<number> = [10, 25, 50];
@Input() public hidePagination: boolean = false;
@Input() public showMoreButton: boolean = false;
@Input() public disableShowMore: boolean | null = false;
@Output() public moreRequested: EventEmitter<void> = new EventEmitter();
@Output() public page: EventEmitter<PageEvent> = new EventEmitter();
constructor() {}

View File

@ -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 {}

View File

@ -0,0 +1,129 @@
<div class="max-width-container">
<h1 class="events-title">{{ 'IAM.EVENTS.TITLE' | translate }}</h1>
<p class="events-desc cnsl-secondary-text">{{ 'IAM.EVENTS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="_loading | async"
>
<div actions>
<cnsl-filter-events (requestChanged)="filterChanged($event)"></cnsl-filter-events>
</div>
<table
[dataSource]="dataSource"
mat-table
class="table views-table"
aria-label="Views"
matSort
(matSortChange)="sortChange($event)"
>
<ng-container matColumnDef="editor">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.EDITOR' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="editor-row" *ngIf="event.editor as editor">
<!-- <cnsl-avatar
*ngIf="editor && editor.displayName; else cog"
class="avatar"
[name]="editor.displayName"
[avatarUrl]="editor.avatarUrl || ''"
[forColor]="editor.preferredLoginName ?? editor.displayName"
[size]="32"
>
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="editor?.preferredLoginName ?? 'franz'" [isMachine]="true">
<i class="las la-robot"></i>
</cnsl-avatar> </ng-template
> -->
<span class="name" *ngIf="editor.displayName">{{ editor.displayName }}</span>
<span class="state" *ngIf="editor.service">{{ editor.service }}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="aggregate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.AGGREGATE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="aggregate-row">
<span class="id">{{ event.aggregate.id }}</span
><span class="state" *ngIf="event.aggregate?.type?.localized?.localizedMessage">{{
event.aggregate.type.localized.localizedMessage
}}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="resourceOwner">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.RESOURCEOWNER' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.aggregate.resourceOwner">{{ event.aggregate.resourceOwner }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef mat-sort-header [start]="'desc'" [disableClear]="true">
{{ 'IAM.EVENTS.SEQUENCE' | translate }}
</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
{{ event.sequence }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.CREATIONDATE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span>{{ event?.creationDate | timestampToDate | localizedDate : 'EEE dd. MMM, HH:mm' }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.TYPE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.type?.localized?.localizedMessage">{{ event.type.localized.localizedMessage }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="payload">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.PAYLOAD' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | topayload as payload">
<span>{{ payload | json }}</span>
<div class="btn-wrapper">
<button class="open-in-dialog-btn" mat-icon-button (click)="openDialog(event)">
<mat-icon svgIcon="mdi_arrow_expand"></mat-icon>
</button>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cnsl-paginator
#paginator
class="paginator"
[hidePagination]="true"
[showMoreButton]="true"
[disableShowMore]="_done | async"
(moreRequested)="more()"
[length]="dataSource.data.length"
>
</cnsl-paginator>
</cnsl-refresh-table>
</div>

View File

@ -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;
}
}

View File

@ -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<EventsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EventsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<void> = new Subject();
public displayedColumns: string[] = [
EventFieldName.TYPE,
EventFieldName.AGGREGATE,
EventFieldName.RESOURCEOWNER,
EventFieldName.EDITOR,
EventFieldName.SEQUENCE,
EventFieldName.CREATIONDATE,
EventFieldName.PAYLOAD,
];
public currentRequest$: BehaviorSubject<LoadRequest> = new BehaviorSubject<LoadRequest>({
req: new ListEventsRequest().setLimit(this.INITPAGESIZE),
override: true,
});
@ViewChild(MatSort) public sort!: MatSort;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<Event> = new MatTableDataSource<Event>([]);
public _done: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public done: Observable<boolean> = this._done.asObservable();
public _loading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _data: BehaviorSubject<Event[]> = new BehaviorSubject<Event[]>([]);
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<Event[]>([]);
this.dataSource = new MatTableDataSource<Event>([]);
}
const eventList = res.getEventsList();
this._data.next(eventList);
const concat = this.dataSource.data.concat(eventList);
this.dataSource = new MatTableDataSource<Event>(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;
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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 '';
}
}
}

View File

@ -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<ListEventsResponse> {
return this.grpcService.admin.listEvents(req, null).then((resp) => resp);
}
public listEventTypes(req: ListEventTypesRequest): Promise<ListEventTypesResponse.AsObject> {
return this.grpcService.admin.listEventTypes(req, null).then((resp) => resp.toObject());
}
public listAggregateTypes(req: ListAggregateTypesRequest): Promise<ListAggregateTypesResponse.AsObject> {
return this.grpcService.admin.listAggregateTypes(req, null).then((resp) => resp.toObject());
}
public getSupportedLanguages(): Promise<GetSupportedLanguagesResponse.AsObject> {
const req = new GetSupportedLanguagesRequest();
return this.grpcService.admin.getSupportedLanguages(req, null).then((resp) => resp.toObject());

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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": "已添加多个管理者。",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-expand</title><path d="M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z" /></svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -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);