mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-28 01:52:16 +00:00
feat(console): events (#5092)
Shows the list of events on instance level
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -80,6 +80,7 @@ export class HeaderComponent implements OnDestroy {
|
||||
'/instance',
|
||||
'/settings',
|
||||
'/views',
|
||||
'/events',
|
||||
'/orgs',
|
||||
'/settings',
|
||||
'/failed-events',
|
||||
|
||||
@@ -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 }"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user