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>
</div>
</div>
<div class="filter-events-section">
<div class="checkbox-wrapper">
<mat-checkbox id="sequenceFilterSet" name="sequenceFilterSet" class="cb" formControlName="sequenceFilterSet"
@ -153,46 +152,73 @@
</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>
<mat-radio-group aria-label="Select an option" class="filter-events-sub" formControlName="creationDateFilterType">
<mat-radio-button [value]="CreationDateFilterType.FROM">
{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_FROM' | translate }}
</mat-radio-button>
<mat-radio-button [value]="CreationDateFilterType.RANGE">
{{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_RANGE' | translate }}
</mat-radio-button>
</mat-radio-group>
<div class="filter-events-sub">
<ng-container *ngIf="creationDateFilterType!.value === CreationDateFilterType.FROM">
<mat-form-field class="datetime-input">
<input
cnslInput
id="creationDate"
name="creationDate"
[matDatepicker]="picker"
formControlName="creationDate"
class="datetime"
matInput
type="datetime-local"
name="creationDateFrom"
[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-datepicker #picker></mat-datepicker>
</cnsl-form-field>
</mat-form-field>
</ng-container>
<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>
</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>
</form>
</div>

View File

@ -19,7 +19,7 @@
display: flex;
flex-direction: column;
padding: 0.5rem 0;
min-width: 320px;
min-width: 360px;
max-width: 360px;
padding-bottom: 0.5rem;
position: relative;
@ -55,11 +55,23 @@
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 {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
justify-content: space-around;
padding: 0 0.5rem;
background-color: if($is-dark-theme, #00000020, #00000008);
margin: 0 -0.5rem;

View File

@ -15,13 +15,18 @@ export enum UserTarget {
EXTERNAL = 'external',
}
enum CreationDateFilterType {
FROM = 'from',
RANGE = 'range',
}
function dateToTs(date: Date): Timestamp {
const ts = new Timestamp();
const milliseconds = date.getTime();
const seconds = Math.abs(milliseconds / 1000);
const seconds = milliseconds / 1000;
const nanos = (milliseconds - seconds * 1000) * 1000 * 1000;
ts.setSeconds(seconds);
ts.setNanos(nanos);
ts.setSeconds(Math.round(seconds));
ts.setNanos(Math.round(nanos));
return ts;
}
@ -31,6 +36,9 @@ function dateToTs(date: Date): Timestamp {
styleUrls: ['./filter-events.component.scss'],
})
export class FilterEventsComponent implements OnInit {
// Make enum available in template
public CreationDateFilterType = CreationDateFilterType;
public showFilter: boolean = false;
public ActionKeysType: any = ActionKeysType;
@ -53,8 +61,11 @@ export class FilterEventsComponent implements OnInit {
sequenceFilterSet: new FormControl(false),
sequence: new FormControl(''),
isAsc: new FormControl<boolean>(false),
creationDateFilterSet: new FormControl(false),
creationDate: new FormControl<Date>(new Date()),
creationDateFilterType: new FormControl(CreationDateFilterType.FROM),
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),
editorUserId: new FormControl(''),
aggregateFilterSet: new FormControl(false),
@ -78,20 +89,35 @@ export class FilterEventsComponent implements OnInit {
const { filter } = params;
if (filter) {
const stringifiedFilters = filter as string;
const filters = JSON.parse(stringifiedFilters);
const filters = JSON.parse(decodeURIComponent(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);
if (filters.creationDateFrom) {
const millisecondsFrom = filters.creationDateFrom;
const dateFrom = new Date(millisecondsFrom);
const ts = dateToTs(dateFrom);
this.creationDateFrom?.setValue(dateFrom);
this.creationDateFilterType?.setValue(CreationDateFilterType.FROM);
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) {
const values = this.aggregateTypes.filter((agg) => filters.aggregateTypesList.includes(agg.type));
@ -252,11 +278,28 @@ export class FilterEventsComponent implements OnInit {
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();
if (formValues.creationDateFilterType === CreationDateFilterType.FROM) {
const dateFrom = new Date(formValues.creationDateFrom);
const tsFrom = dateToTs(dateFrom);
constructRequest.setFrom(tsFrom);
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);
@ -265,7 +308,7 @@ export class FilterEventsComponent implements OnInit {
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
['filter']: JSON.stringify(filterObject),
['filter']: encodeURIComponent(JSON.stringify(filterObject)),
},
replaceUrl: true,
queryParamsHandling: 'merge',
@ -304,12 +347,32 @@ export class FilterEventsComponent implements OnInit {
return this.form.get('sequenceFilterSet');
}
public get creationDate(): AbstractControl | null {
return this.form.get('creationDate');
public get creationDateFilterType(): AbstractControl | null {
return this.form.get('creationDateFilterType');
}
public get creationDateFilterSet(): AbstractControl | null {
return this.form.get('creationDateFilterSet');
public get creationDateFrom(): AbstractControl | null {
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 {
@ -341,9 +404,6 @@ export class FilterEventsComponent implements OnInit {
if (this.userFilterSet?.value && this.editorUserId?.value) {
++count;
}
if (this.creationDateFilterSet?.value && this.creationDate?.value) {
++count;
}
if (this.aggregateFilterSet?.value && this.aggregateId?.value) {
++count;
}
@ -361,4 +421,11 @@ export class FilterEventsComponent implements OnInit {
}
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 { InputModule } from '../input/input.module';
import { FilterEventsComponent } from './filter-events.component';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
@NgModule({
declarations: [FilterEventsComponent],
@ -28,6 +30,8 @@ import { FilterEventsComponent } from './filter-events.component';
MatCheckboxModule,
MatSelectModule,
ActionKeysModule,
MatInputModule,
MatRadioModule,
],
exports: [FilterEventsComponent],
})

View File

@ -70,7 +70,7 @@
</ng-container>
<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 }}
</th>
<td mat-cell *matCellDef="let event">
@ -81,10 +81,12 @@
</ng-container>
<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">
<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>
</td>
</ng-container>

View File

@ -176,13 +176,13 @@ export class EventsComponent implements OnDestroy {
req.setEditorUserId(filterRequest.getEditorUserId());
req.setResourceOwner(filterRequest.getResourceOwner());
req.setSequence(filterRequest.getSequence());
req.setCreationDate(filterRequest.getCreationDate());
req.setRange(filterRequest.getRange());
req.setFrom(filterRequest.getFrom());
const isAsc: boolean = filterRequest.getAsc();
req.setAsc(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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package admin
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/authz"
"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) {
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))
for i, eventType := range req.EventTypes {
eventTypes[i] = eventstore.EventType(eventType)
@ -60,7 +80,9 @@ func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest)
AwaitOpenTransactions().
ResourceOwner(req.ResourceOwner).
EditorUser(req.EditorUserId).
SequenceGreater(req.Sequence)
SequenceGreater(req.Sequence).
CreationDateAfter(sinceTime).
CreationDateBefore(untilTime)
if len(aggregateIDs) > 0 || len(aggregateTypes) > 0 || len(eventTypes) > 0 {
builder.AddQuery().
@ -72,10 +94,9 @@ func eventRequestToFilter(ctx context.Context, req *admin_pb.ListEventsRequest)
if req.GetAsc() {
builder.OrderAsc()
builder.CreationDateAfter(req.CreationDate.AsTime())
builder.CreationDateAfter(fromTime)
} else {
builder.CreationDateBefore(req.CreationDate.AsTime())
builder.CreationDateBefore(fromTime)
}
return builder, nil
}

View File

@ -4,6 +4,7 @@ package system_test
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"math/rand"
"sync"
"testing"
@ -23,6 +24,7 @@ func TestServer_Limits_AuditLogRetention(t *testing.T) {
_, instanceID, iamOwnerCtx := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
userID, projectID, appID, projectGrantID := seedObjects(iamOwnerCtx, t)
beforeTime := time.Now()
farPast := timestamppb.New(beforeTime.Add(-10 * time.Hour).UTC())
zeroCounts := &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)
@ -36,10 +38,22 @@ func TestServer_Limits_AuditLogRetention(t *testing.T) {
AuditLogRetention: durationpb.New(time.Now().Sub(beforeTime)),
})
require.NoError(t, err)
var limitedCounts *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 > 0", assert.Greater, zeroCounts)
limitedCounts = counts
}, "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{
InstanceId: instanceID,
})

View File

@ -7972,11 +7972,35 @@ message ListEventsRequest {
}
];
google.protobuf.Timestamp creation_date = 9 [
deprecated = true,
(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 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 {