feat: add time range events filter (#7005)

* feat(console): add time range events filter

* deprecate creation_date, use oneof filter

* use range or from

* implement api

* fix timestamp format

* translate

* styles

* lint

* integration tests

* fix until date

* rearrange sorting control

* sort creation date

* fix events e2e test

* Update console/src/app/modules/filter-events/filter-events.component.html

Co-authored-by: Max Peintner <max@caos.ch>

* Update console/src/app/modules/filter-events/filter-events.component.html

Co-authored-by: Max Peintner <max@caos.ch>

* Update console/src/app/modules/filter-events/filter-events.component.html

Co-authored-by: Max Peintner <max@caos.ch>

* lint

* lint

* don't use utc call time

---------

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Elio Bischof 2023-12-07 11:15:53 +01:00 committed by GitHub
parent 2e505f40f9
commit 9da4abd459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 355 additions and 158 deletions

View File

@ -145,7 +145,6 @@
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</div> </div>
<div class="filter-events-section"> <div class="filter-events-section">
<div class="checkbox-wrapper"> <div class="checkbox-wrapper">
<mat-checkbox id="sequenceFilterSet" name="sequenceFilterSet" class="cb" formControlName="sequenceFilterSet" <mat-checkbox id="sequenceFilterSet" name="sequenceFilterSet" class="cb" formControlName="sequenceFilterSet"
@ -153,46 +152,73 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
<div class="filter-events-sub" *ngIf="sequenceFilterSet?.value"> <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-form-field class="filter-input-value">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.SEQUENCE.LABEL' | translate }}</cnsl-label> <cnsl-label>{{ 'IAM.EVENTS.FILTERS.SEQUENCE.LABEL' | translate }}</cnsl-label>
<input cnslInput id="sequence" name="sequence" formControlName="sequence" /> <input cnslInput id="sequence" name="sequence" formControlName="sequence" />
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</div> </div>
<div class="filter-events-section"> <div class="filter-events-section">
<div class="checkbox-wrapper"> <mat-radio-group aria-label="Select an option" class="filter-events-sub" formControlName="creationDateFilterType">
<mat-checkbox <mat-radio-button [value]="CreationDateFilterType.FROM">
id="creationDateFilterSet" {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_FROM' | translate }}
name="creationDateFilterSet" </mat-radio-button>
class="cb" <mat-radio-button [value]="CreationDateFilterType.RANGE">
formControlName="creationDateFilterSet" {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_RANGE' | translate }}
>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.CHECKBOX' | translate }} </mat-radio-button>
</mat-checkbox> </mat-radio-group>
</div> <div class="filter-events-sub">
<div class="filter-events-sub" *ngIf="creationDateFilterSet?.value"> <ng-container *ngIf="creationDateFilterType!.value === CreationDateFilterType.FROM">
<cnsl-form-field class="filter-input-value"> <mat-form-field class="datetime-input">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL' | translate }}</cnsl-label>
<input <input
cnslInput class="datetime"
id="creationDate" matInput
name="creationDate" type="datetime-local"
[matDatepicker]="picker" name="creationDateFrom"
formControlName="creationDate" [value]="creationDateFrom!.value | date: 'yyyy-MM-dd HH:mm:ss'"
(change)="creationDateFrom = $event.target"
/> />
<mat-datepicker-toggle style="top: 0" cnslSuffix [for]="picker"></mat-datepicker-toggle> </mat-form-field>
<mat-datepicker #picker></mat-datepicker> </ng-container>
</cnsl-form-field> <ng-container *ngIf="creationDateFilterType!.value === CreationDateFilterType.RANGE">
<div class="datetime-range">
<mat-form-field class="datetime-input">
<cnsl-label
>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL_SINCE' | translate }}
<input
matInput
type="datetime-local"
name="creationDateSince"
[value]="creationDateSince!.value | date: 'yyyy-MM-dd HH:mm:ss'"
(change)="creationDateSince = $event.target"
/>
</cnsl-label>
</mat-form-field>
<mat-form-field class="datetime-input">
<cnsl-label
>{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL_UNTIL' | translate }}
<input
matInput
type="datetime-local"
name="creationDateUntil"
[value]="creationDateUntil!.value | date: 'yyyy-MM-dd HH:mm:ss'"
(change)="creationDateUntil = $event.target"
/>
</cnsl-label>
</mat-form-field>
</div> </div>
</ng-container>
</div>
</div>
<div class="filter-events-section">
<cnsl-form-field class="aggregate-type-select">
<cnsl-label>{{ 'IAM.EVENTS.FILTERS.SORT' | translate }}</cnsl-label>
<mat-select id="isAsc" name="isAsc" formControlName="isAsc">
<mat-option [value]="false"> {{ 'IAM.EVENTS.FILTERS.DESC' | translate }} </mat-option>
<mat-option [value]="true">{{ 'IAM.EVENTS.FILTERS.ASC' | translate }} </mat-option>
</mat-select>
</cnsl-form-field>
</div> </div>
</form> </form>
</div> </div>

View File

@ -19,7 +19,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.5rem 0; padding: 0.5rem 0;
min-width: 320px; min-width: 360px;
max-width: 360px; max-width: 360px;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
position: relative; position: relative;
@ -55,11 +55,23 @@
align-items: center; align-items: center;
} }
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
background-color: map-get($background, cards);
}
.datetime-input {
width: 100%;
}
.datetime-range {
display: inline-block;
}
.filter-events-sub { .filter-events-sub {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-around;
padding: 0 0.5rem; padding: 0 0.5rem;
background-color: if($is-dark-theme, #00000020, #00000008); background-color: if($is-dark-theme, #00000020, #00000008);
margin: 0 -0.5rem; margin: 0 -0.5rem;

View File

@ -15,13 +15,18 @@ export enum UserTarget {
EXTERNAL = 'external', EXTERNAL = 'external',
} }
enum CreationDateFilterType {
FROM = 'from',
RANGE = 'range',
}
function dateToTs(date: Date): Timestamp { function dateToTs(date: Date): Timestamp {
const ts = new Timestamp(); const ts = new Timestamp();
const milliseconds = date.getTime(); const milliseconds = date.getTime();
const seconds = Math.abs(milliseconds / 1000); const seconds = milliseconds / 1000;
const nanos = (milliseconds - seconds * 1000) * 1000 * 1000; const nanos = (milliseconds - seconds * 1000) * 1000 * 1000;
ts.setSeconds(seconds); ts.setSeconds(Math.round(seconds));
ts.setNanos(nanos); ts.setNanos(Math.round(nanos));
return ts; return ts;
} }
@ -31,6 +36,9 @@ function dateToTs(date: Date): Timestamp {
styleUrls: ['./filter-events.component.scss'], styleUrls: ['./filter-events.component.scss'],
}) })
export class FilterEventsComponent implements OnInit { export class FilterEventsComponent implements OnInit {
// Make enum available in template
public CreationDateFilterType = CreationDateFilterType;
public showFilter: boolean = false; public showFilter: boolean = false;
public ActionKeysType: any = ActionKeysType; public ActionKeysType: any = ActionKeysType;
@ -53,8 +61,11 @@ export class FilterEventsComponent implements OnInit {
sequenceFilterSet: new FormControl(false), sequenceFilterSet: new FormControl(false),
sequence: new FormControl(''), sequence: new FormControl(''),
isAsc: new FormControl<boolean>(false), isAsc: new FormControl<boolean>(false),
creationDateFilterSet: new FormControl(false), creationDateFilterType: new FormControl(CreationDateFilterType.FROM),
creationDate: new FormControl<Date>(new Date()), creationDateFrom: new FormControl<Date>(new Date()),
// creationDateSince is 15 minutes in the past by default
creationDateSince: new FormControl<Date>(new Date(new Date().getTime() - 15 * 60_000)),
creationDateUntil: new FormControl<Date>(new Date()),
userFilterSet: new FormControl(false), userFilterSet: new FormControl(false),
editorUserId: new FormControl(''), editorUserId: new FormControl(''),
aggregateFilterSet: new FormControl(false), aggregateFilterSet: new FormControl(false),
@ -78,20 +89,35 @@ export class FilterEventsComponent implements OnInit {
const { filter } = params; const { filter } = params;
if (filter) { if (filter) {
const stringifiedFilters = filter as string; const stringifiedFilters = filter as string;
const filters = JSON.parse(stringifiedFilters); const filters = JSON.parse(decodeURIComponent(stringifiedFilters));
if (filters.aggregateId) { if (filters.aggregateId) {
this.request.setAggregateId(filters.aggregateId); this.request.setAggregateId(filters.aggregateId);
this.aggregateId?.setValue(filters.aggregateId); this.aggregateId?.setValue(filters.aggregateId);
this.aggregateFilterSet?.setValue(true); this.aggregateFilterSet?.setValue(true);
} }
if (filters.creationDate) { if (filters.creationDateFrom) {
const milliseconds = filters.creationDate; const millisecondsFrom = filters.creationDateFrom;
const date = new Date(milliseconds); const dateFrom = new Date(millisecondsFrom);
const ts = dateToTs(date); const ts = dateToTs(dateFrom);
this.creationDateFrom?.setValue(dateFrom);
this.creationDateFilterType?.setValue(CreationDateFilterType.FROM);
this.request.setCreationDate(ts); this.request.setCreationDate(ts);
this.creationDate?.setValue(date); }
this.creationDateFilterSet?.setValue(true); if (filters.creationDateSince || filters.creationDateUntil) {
const millisecondsFrom = filters.creationDateSince;
const dateSince = new Date(millisecondsFrom);
const tsSince = dateToTs(dateSince);
this.creationDateSince?.setValue(dateSince);
const millisecondsUntil = filters.creationDateUntil;
const dateUntil = new Date(millisecondsUntil);
const tsUntil = dateToTs(dateUntil);
this.creationDateUntil?.setValue(dateUntil);
const range = new ListEventsRequest.creation_date_range();
range.setSince(tsSince);
range.setUntil(tsUntil);
this.request.setRange(range);
this.creationDateFilterType?.setValue(CreationDateFilterType.RANGE);
} }
if (filters.aggregateTypesList && filters.aggregateTypesList.length) { if (filters.aggregateTypesList && filters.aggregateTypesList.length) {
const values = this.aggregateTypes.filter((agg) => filters.aggregateTypesList.includes(agg.type)); const values = this.aggregateTypes.filter((agg) => filters.aggregateTypesList.includes(agg.type));
@ -252,11 +278,28 @@ export class FilterEventsComponent implements OnInit {
constructRequest.setAsc(formValues.isAsc); constructRequest.setAsc(formValues.isAsc);
filterObject.isAsc = formValues.isAsc; filterObject.isAsc = formValues.isAsc;
} }
if (formValues.creationDateFilterSet && formValues.creationDate) { if (formValues.creationDateFilterType === CreationDateFilterType.FROM) {
const date = new Date(formValues.creationDate); const dateFrom = new Date(formValues.creationDateFrom);
const ts = dateToTs(date); const tsFrom = dateToTs(dateFrom);
constructRequest.setCreationDate(ts); constructRequest.setFrom(tsFrom);
filterObject.creationDate = date.getTime(); constructRequest.clearRange();
filterObject.creationDateFrom = dateFrom.getTime();
filterObject.creationDateSince = undefined;
filterObject.creationDateUntil = undefined;
}
if (formValues.creationDateFilterType === CreationDateFilterType.RANGE) {
const range = new ListEventsRequest.creation_date_range();
const dateSince = new Date(formValues.creationDateSince);
const tsSince = dateToTs(dateSince);
range.setSince(tsSince);
filterObject.creationDateSince = dateSince.getTime();
const dateUntil = new Date(formValues.creationDateUntil);
const tsUntil = dateToTs(dateUntil);
range.setUntil(tsUntil);
filterObject.creationDateUntil = dateUntil.getTime();
constructRequest.setRange(range);
constructRequest.clearFrom();
filterObject.creationDateFrom = undefined;
} }
this.requestChanged.emit(constructRequest); this.requestChanged.emit(constructRequest);
@ -265,7 +308,7 @@ export class FilterEventsComponent implements OnInit {
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { queryParams: {
['filter']: JSON.stringify(filterObject), ['filter']: encodeURIComponent(JSON.stringify(filterObject)),
}, },
replaceUrl: true, replaceUrl: true,
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
@ -304,12 +347,32 @@ export class FilterEventsComponent implements OnInit {
return this.form.get('sequenceFilterSet'); return this.form.get('sequenceFilterSet');
} }
public get creationDate(): AbstractControl | null { public get creationDateFilterType(): AbstractControl | null {
return this.form.get('creationDate'); return this.form.get('creationDateFilterType');
} }
public get creationDateFilterSet(): AbstractControl | null { public get creationDateFrom(): AbstractControl | null {
return this.form.get('creationDateFilterSet'); return this.form.get('creationDateFrom');
}
public set creationDateFrom(event: EventTarget | null) {
this.setDate(this.creationDateFrom!, event);
}
public get creationDateSince(): AbstractControl | null {
return this.form.get('creationDateSince');
}
public set creationDateSince(event: EventTarget | null) {
this.setDate(this.creationDateSince!, event);
}
public get creationDateUntil(): AbstractControl | null {
return this.form.get('creationDateUntil');
}
public set creationDateUntil(event: EventTarget | null) {
this.setDate(this.creationDateUntil!, event);
} }
public get resourceOwnerFilterSet(): AbstractControl | null { public get resourceOwnerFilterSet(): AbstractControl | null {
@ -341,9 +404,6 @@ export class FilterEventsComponent implements OnInit {
if (this.userFilterSet?.value && this.editorUserId?.value) { if (this.userFilterSet?.value && this.editorUserId?.value) {
++count; ++count;
} }
if (this.creationDateFilterSet?.value && this.creationDate?.value) {
++count;
}
if (this.aggregateFilterSet?.value && this.aggregateId?.value) { if (this.aggregateFilterSet?.value && this.aggregateId?.value) {
++count; ++count;
} }
@ -361,4 +421,11 @@ export class FilterEventsComponent implements OnInit {
} }
return count; return count;
} }
private setDate(ctrl: AbstractControl<Date>, event: EventTarget | null): void {
if (!(event instanceof HTMLInputElement)) {
throw new Error('wrong target');
}
ctrl.setValue(new Date(event.value || ''));
}
} }

View File

@ -12,6 +12,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ActionKeysModule } from '../action-keys/action-keys.module'; import { ActionKeysModule } from '../action-keys/action-keys.module';
import { InputModule } from '../input/input.module'; import { InputModule } from '../input/input.module';
import { FilterEventsComponent } from './filter-events.component'; import { FilterEventsComponent } from './filter-events.component';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
@NgModule({ @NgModule({
declarations: [FilterEventsComponent], declarations: [FilterEventsComponent],
@ -28,6 +30,8 @@ import { FilterEventsComponent } from './filter-events.component';
MatCheckboxModule, MatCheckboxModule,
MatSelectModule, MatSelectModule,
ActionKeysModule, ActionKeysModule,
MatInputModule,
MatRadioModule,
], ],
exports: [FilterEventsComponent], exports: [FilterEventsComponent],
}) })

View File

@ -70,7 +70,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="sequence"> <ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef mat-sort-header [start]="'desc'" [disableClear]="true"> <th mat-header-cell *matHeaderCellDef>
{{ 'IAM.EVENTS.SEQUENCE' | translate }} {{ 'IAM.EVENTS.SEQUENCE' | translate }}
</th> </th>
<td mat-cell *matCellDef="let event"> <td mat-cell *matCellDef="let event">
@ -81,10 +81,12 @@
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.CREATIONDATE' | translate }}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header [start]="'desc'" [disableClear]="true">
{{ 'IAM.EVENTS.CREATIONDATE' | translate }}
</th>
<td mat-cell *matCellDef="let event"> <td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event"> <ng-container *ngIf="event | toobject as event">
<span>{{ event?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span> <span>{{ event?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm:ss' }}</span>
</ng-container> </ng-container>
</td> </td>
</ng-container> </ng-container>

View File

@ -176,13 +176,13 @@ export class EventsComponent implements OnDestroy {
req.setEditorUserId(filterRequest.getEditorUserId()); req.setEditorUserId(filterRequest.getEditorUserId());
req.setResourceOwner(filterRequest.getResourceOwner()); req.setResourceOwner(filterRequest.getResourceOwner());
req.setSequence(filterRequest.getSequence()); req.setSequence(filterRequest.getSequence());
req.setCreationDate(filterRequest.getCreationDate()); req.setRange(filterRequest.getRange());
req.setFrom(filterRequest.getFrom());
const isAsc: boolean = filterRequest.getAsc(); const isAsc: boolean = filterRequest.getAsc();
req.setAsc(isAsc); req.setAsc(isAsc);
if (this.sortAsc !== isAsc) { if (this.sortAsc !== isAsc) {
this.sort.sort({ id: 'sequence', start: isAsc ? 'asc' : 'desc', disableClear: true }); this.sort.sort({ id: 'creationDate', start: isAsc ? 'asc' : 'desc', disableClear: true });
} }
this.loadEvents(req, true); this.loadEvents(req, true);
} }

View File

@ -865,14 +865,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Последователност", "LABEL": "Последователност",
"CHECKBOX": "Филтриране по последователност", "CHECKBOX": "Филтриране по последователност"
},
"SORT": "Сортиране", "SORT": "Сортиране",
"ASC": "Възходящ", "ASC": "Възходящ",
"DESC": "Спускане" "DESC": "Спускане",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Дата на създаване", "RADIO_FROM": "От",
"CHECKBOX": "Филтриране по дата на създаване" "RADIO_RANGE": "Обхват",
"LABEL_SINCE": "От",
"LABEL_UNTIL": "До"
}, },
"OTHER": "друго", "OTHER": "друго",
"OTHERS": "други" "OTHERS": "други"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sekvence", "LABEL": "Sekvence",
"CHECKBOX": "Filtrovat podle Sekvence", "CHECKBOX": "Filtrovat podle Sekvence"
},
"SORT": "Třídění", "SORT": "Třídění",
"ASC": "Vzestupně", "ASC": "Vzestupně",
"DESC": "Sestupně" "DESC": "Sestupně",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Datum vytvoření", "RADIO_FROM": "Od",
"CHECKBOX": "Filtrovat podle Datumu vytvoření" "RADIO_RANGE": "Rozsah",
"LABEL_SINCE": "Od",
"LABEL_UNTIL": "Do"
}, },
"OTHER": "jiný", "OTHER": "jiný",
"OTHERS": "jiné" "OTHERS": "jiné"

View File

@ -871,14 +871,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sequenz", "LABEL": "Sequenz",
"CHECKBOX": "Nach Sequenz filtern", "CHECKBOX": "Nach Sequenz filtern"
"SORT": "Sortierung",
"ASC": "aufsteigend",
"DESC": "absteigend"
}, },
"SORT": "Sortierung",
"ASC": "Aufsteigend",
"DESC": "Absteigend",
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Erstelldatum", "RADIO_FROM": "Von",
"CHECKBOX": "Nach Erstelldatum filtern" "RADIO_RANGE": "Zeitraum",
"LABEL_SINCE": "Seit",
"LABEL_UNTIL": "Bis"
}, },
"OTHER": "weiterer", "OTHER": "weiterer",
"OTHERS": "weitere" "OTHERS": "weitere"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sequence", "LABEL": "Sequence",
"CHECKBOX": "Filter by Sequence", "CHECKBOX": "Filter by Sequence"
"SORT": "Sorting",
"ASC": "Ascending",
"DESC": "Descending"
}, },
"SORT": "Sort",
"ASC": "Ascending",
"DESC": "Descending",
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Creation Date", "RADIO_FROM": "From",
"CHECKBOX": "Filter by Creation Date" "RADIO_RANGE": "Range",
"LABEL_SINCE": "Since",
"LABEL_UNTIL": "Until"
}, },
"OTHER": "other", "OTHER": "other",
"OTHERS": "others" "OTHERS": "others"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Secuencia", "LABEL": "Secuencia",
"CHECKBOX": "Filtrar por secuencia", "CHECKBOX": "Filtrar por secuencia"
},
"SORT": "Ordenado", "SORT": "Ordenado",
"ASC": "Ascendente", "ASC": "Ascendente",
"DESC": "Descendente" "DESC": "Descendente",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Fecha de creación", "RADIO_FROM": "Desde",
"CHECKBOX": "Filtrar por fecha de creación" "RADIO_RANGE": "Rango",
"LABEL_SINCE": "Desde",
"LABEL_UNTIL": "Hasta"
}, },
"OTHER": "otro", "OTHER": "otro",
"OTHERS": "otros" "OTHERS": "otros"

View File

@ -871,14 +871,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "séquence", "LABEL": "séquence",
"CHECKBOX": "Filtrer par séquence", "CHECKBOX": "Filtrer par séquence"
},
"SORT": "Triage", "SORT": "Triage",
"ASC": "Ascendant", "ASC": "Ascendant",
"DESC": "Descendant" "DESC": "Descendant",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Date de création", "RADIO_FROM": "De",
"CHECKBOX": "Filtrer par date de création" "RADIO_RANGE": "Gamme",
"LABEL_SINCE": "Depuis",
"LABEL_UNTIL": "Jusqu'à"
}, },
"OTHER": "autre", "OTHER": "autre",
"OTHERS": "autres" "OTHERS": "autres"

View File

@ -870,14 +870,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sequence", "LABEL": "Sequence",
"CHECKBOX": "Filter per sequenza", "CHECKBOX": "Filter per sequenza"
"SORT": "",
"ASC": "Ascending",
"DESC": "Descending"
}, },
"SORT": "Ordina per",
"ASC": "Ascendente",
"DESC": "Discendente",
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Creation Date", "RADIO_FROM": "Da",
"CHECKBOX": "Filter by Creation Date" "RADIO_RANGE": "Intervallo",
"LABEL_SINCE": "Da",
"LABEL_UNTIL": "A"
}, },
"OTHER": "altro", "OTHER": "altro",
"OTHERS": "altri" "OTHERS": "altri"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "シーケンス", "LABEL": "シーケンス",
"CHECKBOX": "シーケンスで絞り込み", "CHECKBOX": "シーケンスで絞り込み"
},
"SORT": "ソート", "SORT": "ソート",
"ASC": "昇順", "ASC": "昇順",
"DESC": "降順" "DESC": "降順",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "作成日", "RADIO_FROM": "から",
"CHECKBOX": "作成日で絞り込み" "RADIO_RANGE": "範囲",
"LABEL_SINCE": "以降",
"LABEL_UNTIL": "まで"
}, },
"OTHER": "その他", "OTHER": "その他",
"OTHERS": "その他" "OTHERS": "その他"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Секвенца", "LABEL": "Секвенца",
"CHECKBOX": "Филтер според секвенцата", "CHECKBOX": "Филтер според секвенцата"
},
"SORT": "Сортирање", "SORT": "Сортирање",
"ASC": "Растечки", "ASC": "Растечки",
"DESC": "Опаѓачки" "DESC": "Опаѓачки",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Датум на креирање", "RADIO_FROM": "Од",
"CHECKBOX": "Филтер според датумот на креирање" "RADIO_RANGE": "Ранг",
"LABEL_SINCE": "Од",
"LABEL_UNTIL": "До"
}, },
"OTHER": "друго", "OTHER": "друго",
"OTHERS": "други" "OTHERS": "други"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Reeks", "LABEL": "Reeks",
"CHECKBOX": "Filter op Reeks", "CHECKBOX": "Filter op Reeks"
},
"SORT": "Sortering", "SORT": "Sortering",
"ASC": "Oplopend", "ASC": "Oplopend",
"DESC": "Aflopend" "DESC": "Aflopend",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Aanmaakdatum", "RADIO_FROM": "Van",
"CHECKBOX": "Filter op Aanmaakdatum" "RADIO_RANGE": "Reeks",
"LABEL_SINCE": "Sinds",
"LABEL_UNTIL": "Tot"
}, },
"OTHER": "ander", "OTHER": "ander",
"OTHERS": "anderen" "OTHERS": "anderen"

View File

@ -871,14 +871,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sekwencja", "LABEL": "Sekwencja",
"CHECKBOX": "Filtruj według sekwencji", "CHECKBOX": "Filtruj według sekwencji"
},
"SORT": "Sortowanie", "SORT": "Sortowanie",
"ASC": "Rosnące", "ASC": "Rosnące",
"DESC": "Malejące" "DESC": "Malejące",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Data utworzenia", "RADIO_FROM": "Od",
"CHECKBOX": "Filtruj według daty utworzenia" "RADIO_RANGE": "Zakres",
"LABEL_SINCE": "Od",
"LABEL_UNTIL": "Do"
}, },
"OTHER": "inne", "OTHER": "inne",
"OTHERS": "inni" "OTHERS": "inni"

View File

@ -872,14 +872,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Sequência", "LABEL": "Sequência",
"CHECKBOX": "Filtrar por Sequência", "CHECKBOX": "Filtrar por Sequência"
},
"SORT": "Ordenação", "SORT": "Ordenação",
"ASC": "Crescente", "ASC": "Crescente",
"DESC": "Decrescente" "DESC": "Decrescente",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Data de Criação", "RADIO_FROM": "Desde",
"CHECKBOX": "Filtrar por Data de Criação" "RADIO_RANGE": "Intervalo",
"LABEL_SINCE": "Desde",
"LABEL_UNTIL": "Até"
}, },
"OTHER": "outro", "OTHER": "outro",
"OTHERS": "outros" "OTHERS": "outros"

View File

@ -868,14 +868,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "Последовательность", "LABEL": "Последовательность",
"CHECKBOX": "Фильтровать по последовательности", "CHECKBOX": "Фильтровать по последовательности"
},
"SORT": "Сортировка", "SORT": "Сортировка",
"ASC": "Восходящий", "ASC": "Восходящий",
"DESC": "По убыванию" "DESC": "По убыванию",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "Дата создания", "RADIO_FROM": "От",
"CHECKBOX": "Фильтровать по дате создания" "RADIO_RANGE": "Диапазон",
"LABEL_SINCE": "С",
"LABEL_UNTIL": "К"
}, },
"OTHER": "другой", "OTHER": "другой",
"OTHERS": "другие" "OTHERS": "другие"

View File

@ -871,14 +871,16 @@
}, },
"SEQUENCE": { "SEQUENCE": {
"LABEL": "序列", "LABEL": "序列",
"CHECKBOX": "按顺序过滤", "CHECKBOX": "按顺序过滤"
},
"SORT": "分拣", "SORT": "分拣",
"ASC": "上升中", "ASC": "上升中",
"DESC": "下降" "DESC": "下降",
},
"CREATIONDATE": { "CREATIONDATE": {
"LABEL": "创建日期", "RADIO_FROM": "从",
"CHECKBOX": "按创建日期过滤" "RADIO_RANGE": "范围",
"LABEL_SINCE": "自从",
"LABEL_UNTIL": "直到"
}, },
"OTHER": "其他", "OTHER": "其他",
"OTHERS": "其他" "OTHERS": "其他"

View File

@ -9,9 +9,8 @@ describe('events', () => {
cy.get('[data-e2e="event-type-cell"]').should('have.length', 20); cy.get('[data-e2e="event-type-cell"]').should('have.length', 20);
cy.get('[data-e2e="open-filter-button"]').click(); cy.get('[data-e2e="open-filter-button"]').click();
cy.get('[data-e2e="event-type-filter-checkbox"]').click(); cy.get('[data-e2e="event-type-filter-checkbox"]').click();
cy.get('#mat-select-value-1').click(); cy.contains('mat-select', 'Descending').click();
cy.contains('mat-option', eventTypeEnglish).click(); cy.contains('mat-option', 'Ascending').click();
cy.get('body').click();
cy.get('[data-e2e="filter-finish-button"]').click(); cy.get('[data-e2e="filter-finish-button"]').click();
cy.contains('[data-e2e="event-type-cell"]', eventTypeEnglish).should('have.length.at.least', 1); cy.contains('[data-e2e="event-type-cell"]', eventTypeEnglish).should('have.length.at.least', 1);
}); });

View File

@ -2,6 +2,7 @@ package admin
import ( import (
"context" "context"
"time"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
@ -36,6 +37,25 @@ func (s *Server) ListAggregateTypes(ctx context.Context, in *admin_pb.ListAggreg
} }
func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest) (*eventstore.SearchQueryBuilder, error) { func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest) (*eventstore.SearchQueryBuilder, error) {
var fromTime, sinceTime, untilTime time.Time
// We ignore the deprecation warning here because we still need to support the deprecated field.
//nolint:staticcheck
if creationDatePb := req.GetCreationDate(); creationDatePb != nil {
fromTime = creationDatePb.AsTime()
}
if fromTimePb := req.GetFrom(); fromTimePb != nil {
fromTime = fromTimePb.AsTime()
}
if timeRange := req.GetRange(); timeRange != nil {
// If range is set, we ignore the from and the deprecated creation_date fields
fromTime = time.Time{}
if timeSincePb := timeRange.GetSince(); timeSincePb != nil {
sinceTime = timeSincePb.AsTime()
}
if timeUntilPb := timeRange.GetUntil(); timeUntilPb != nil {
untilTime = timeUntilPb.AsTime()
}
}
eventTypes := make([]eventstore.EventType, len(req.EventTypes)) eventTypes := make([]eventstore.EventType, len(req.EventTypes))
for i, eventType := range req.EventTypes { for i, eventType := range req.EventTypes {
eventTypes[i] = eventstore.EventType(eventType) eventTypes[i] = eventstore.EventType(eventType)
@ -60,7 +80,9 @@ func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest)
AwaitOpenTransactions(). AwaitOpenTransactions().
ResourceOwner(req.ResourceOwner). ResourceOwner(req.ResourceOwner).
EditorUser(req.EditorUserId). EditorUser(req.EditorUserId).
SequenceGreater(req.Sequence) SequenceGreater(req.Sequence).
CreationDateAfter(sinceTime).
CreationDateBefore(untilTime)
if len(aggregateIDs) > 0 || len(aggregateTypes) > 0 || len(eventTypes) > 0 { if len(aggregateIDs) > 0 || len(aggregateTypes) > 0 || len(eventTypes) > 0 {
builder.AddQuery(). builder.AddQuery().
@ -72,10 +94,9 @@ func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest)
if req.GetAsc() { if req.GetAsc() {
builder.OrderAsc() builder.OrderAsc()
builder.CreationDateAfter(req.CreationDate.AsTime()) builder.CreationDateAfter(fromTime)
} else { } else {
builder.CreationDateBefore(req.CreationDate.AsTime()) builder.CreationDateBefore(fromTime)
} }
return builder, nil return builder, nil
} }

View File

@ -4,6 +4,7 @@ package system_test
import ( import (
"context" "context"
"google.golang.org/protobuf/types/known/timestamppb"
"math/rand" "math/rand"
"sync" "sync"
"testing" "testing"
@ -23,6 +24,7 @@ func TestServer_Limits_AuditLogRetention(t *testing.T) {
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX) _, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t) userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t)
beforeTime := time.Now() beforeTime := time.Now()
farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC())
zeroCounts := &eventCounts{} zeroCounts := &eventCounts{}
seededCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { seededCount := requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
counts.assertAll(t, c, "seeded events are > 0", assert.Greater, zeroCounts) counts.assertAll(t, c, "seeded events are > 0", assert.Greater, zeroCounts)
@ -36,10 +38,22 @@ func TestServer_Limits_AuditLogRetention(t *testing.T) {
AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)), AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)),
}) })
require.NoError(t, err) require.NoError(t, err)
var limitedCounts *eventCounts
requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) { requireEventually(t, iamOwnerCtx, userID, projectID, appID, projectGrantID, func(c assert.TestingT, counts *eventCounts) {
counts.assertAll(t, c, "limited events < added events", assert.Less, addedCount) counts.assertAll(t, c, "limited events < added events", assert.Less, addedCount)
counts.assertAll(t, c, "limited events > 0", assert.Greater, zeroCounts) counts.assertAll(t, c, "limited events > 0", assert.Greater, zeroCounts)
limitedCounts = counts
}, "wait for limited event assertions to pass") }, "wait for limited event assertions to pass")
listedEvents, err := Tester.Client.Admin.ListEvents(iamOwnerCtx, &admin.ListEventsRequest{CreationDateFilter: &admin.ListEventsRequest_From{
From: farPast,
}})
require.NoError(t, err)
assert.LessOrEqual(t, len(listedEvents.GetEvents()), limitedCounts.all, "ListEvents with from query older than retention doesn't return more events")
listedEvents, err = Tester.Client.Admin.ListEvents(iamOwnerCtx, &admin.ListEventsRequest{CreationDateFilter: &admin.ListEventsRequest_Range{Range: &admin.ListEventsRequestCreationDateRange{
Since: farPast,
}}})
require.NoError(t, err)
assert.LessOrEqual(t, len(listedEvents.GetEvents()), limitedCounts.all, "ListEvents with since query older than retention doesn't return more events")
_, err = Tester.Client.System.ResetLimits(SystemCTX, &system.ResetLimitsRequest{ _, err = Tester.Client.System.ResetLimits(SystemCTX, &system.ResetLimitsRequest{
InstanceId: instanceID, InstanceId: instanceID,
}) })

View File

@ -7972,11 +7972,35 @@ message ListEventsRequest {
} }
]; ];
google.protobuf.Timestamp creation_date = 9 [ google.protobuf.Timestamp creation_date = 9 [
deprecated = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2019-04-01T08:45:00.000000Z\""; example: "\"2019-04-01T08:45:00.000000Z\"";
description: "If asc is false, the events returned are older than creation_date. If asc is true, the events returned are younger than creation_date. If creation_date is not set the field is ignored."; description: "Use from instead.";
} }
]; ];
message creation_date_range {
google.protobuf.Timestamp since = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2019-04-01T08:45:00.000000Z\"";
description: "The events returned are younger than the UTC since date";
}
];
google.protobuf.Timestamp until = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2019-04-01T08:45:00.000000Z\"";
description: "The events returned are older than the UTC until date.";
}
];
}
oneof creation_date_filter {
creation_date_range range = 10;
google.protobuf.Timestamp from = 11 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2019-04-01T08:45:00.000000Z\"";
description: "If asc is false, the events returned are older than the UTC from date. If asc is true, the events returned are younger than from.";
}
];
}
} }
message ListEventsResponse { message ListEventsResponse {