mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-06 14:17:17 +00:00
feat(console): events (#5092)
Shows the list of events on instance level
This commit is contained in:
parent
5ca7e83f0a
commit
e9d5d1dcaf
@ -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'),
|
||||
|
@ -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'),
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
330
console/src/app/modules/filter-events/filter-events.component.ts
Normal file
330
console/src/app/modules/filter-events/filter-events.component.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
|
||||
|
17
console/src/app/pages/events/events-routing.module.ts
Normal file
17
console/src/app/pages/events/events-routing.module.ts
Normal 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 {}
|
129
console/src/app/pages/events/events.component.html
Normal file
129
console/src/app/pages/events/events.component.html
Normal 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>
|
97
console/src/app/pages/events/events.component.scss
Normal file
97
console/src/app/pages/events/events.component.scss
Normal 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;
|
||||
}
|
||||
}
|
24
console/src/app/pages/events/events.component.spec.ts
Normal file
24
console/src/app/pages/events/events.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
198
console/src/app/pages/events/events.component.ts
Normal file
198
console/src/app/pages/events/events.component.ts
Normal 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;
|
||||
}
|
||||
}
|
65
console/src/app/pages/events/events.module.ts
Normal file
65
console/src/app/pages/events/events.module.ts
Normal 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 {}
|
11
console/src/app/pipes/to-object/to-object.module.ts
Normal file
11
console/src/app/pipes/to-object/to-object.module.ts
Normal 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 {}
|
12
console/src/app/pipes/to-object/to-object.pipe.ts
Normal file
12
console/src/app/pipes/to-object/to-object.pipe.ts
Normal 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();
|
||||
}
|
||||
}
|
11
console/src/app/pipes/to-payload/to-payload.module.ts
Normal file
11
console/src/app/pipes/to-payload/to-payload.module.ts
Normal 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 {}
|
17
console/src/app/pipes/to-payload/to-payload.pipe.ts
Normal file
17
console/src/app/pipes/to-payload/to-payload.pipe.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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.",
|
||||
|
@ -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": "已添加多个管理者。",
|
||||
|
1
console/src/assets/mdi/arrow-expand.svg
Normal file
1
console/src/assets/mdi/arrow-expand.svg
Normal 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 |
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user