From 9da4abd45931e561d66268193360ca3cddbda9cc Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 7 Dec 2023 11:15:53 +0100 Subject: [PATCH 01/23] 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 * Update console/src/app/modules/filter-events/filter-events.component.html Co-authored-by: Max Peintner * Update console/src/app/modules/filter-events/filter-events.component.html Co-authored-by: Max Peintner * lint * lint * don't use utc call time --------- Co-authored-by: Max Peintner Co-authored-by: Silvan --- .../filter-events.component.html | 92 +++++++++----- .../filter-events.component.scss | 16 ++- .../filter-events/filter-events.component.ts | 117 ++++++++++++++---- .../filter-events/filter-events.module.ts | 4 + .../app/pages/events/events.component.html | 8 +- .../src/app/pages/events/events.component.ts | 6 +- console/src/assets/i18n/bg.json | 14 ++- console/src/assets/i18n/cs.json | 14 ++- console/src/assets/i18n/de.json | 14 ++- console/src/assets/i18n/en.json | 14 ++- console/src/assets/i18n/es.json | 14 ++- console/src/assets/i18n/fr.json | 14 ++- console/src/assets/i18n/it.json | 14 ++- console/src/assets/i18n/ja.json | 14 ++- console/src/assets/i18n/mk.json | 14 ++- console/src/assets/i18n/nl.json | 14 ++- console/src/assets/i18n/pl.json | 14 ++- console/src/assets/i18n/pt.json | 14 ++- console/src/assets/i18n/ru.json | 14 ++- console/src/assets/i18n/zh.json | 14 ++- e2e/cypress/e2e/events/events.cy.ts | 5 +- internal/api/grpc/admin/event.go | 29 ++++- .../grpc/system/limits_integration_test.go | 14 +++ proto/zitadel/admin.proto | 26 +++- 24 files changed, 355 insertions(+), 158 deletions(-) diff --git a/console/src/app/modules/filter-events/filter-events.component.html b/console/src/app/modules/filter-events/filter-events.component.html index 5d835c32b5..ed1943b514 100644 --- a/console/src/app/modules/filter-events/filter-events.component.html +++ b/console/src/app/modules/filter-events/filter-events.component.html @@ -145,7 +145,6 @@ -
- - {{ 'IAM.EVENTS.FILTERS.SEQUENCE.SORT' | translate }} - - - {{ 'IAM.EVENTS.FILTERS.SEQUENCE.DESC' | translate }} - {{ 'IAM.EVENTS.FILTERS.SEQUENCE.ASC' | translate }} - - - {{ 'IAM.EVENTS.FILTERS.SEQUENCE.LABEL' | translate }}
-
-
- {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.CHECKBOX' | translate }} - -
-
- - {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL' | translate }} - - - - + + + {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_FROM' | translate }} + + + {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.RADIO_RANGE' | translate }} + + +
+ + + + + + +
+ + {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL_SINCE' | translate }} + + + + + {{ 'IAM.EVENTS.FILTERS.CREATIONDATE.LABEL_UNTIL' | translate }} + + + +
+
+
+ + {{ 'IAM.EVENTS.FILTERS.SORT' | translate }} + + + {{ 'IAM.EVENTS.FILTERS.DESC' | translate }} + {{ 'IAM.EVENTS.FILTERS.ASC' | translate }} + + +
diff --git a/console/src/app/modules/filter-events/filter-events.component.scss b/console/src/app/modules/filter-events/filter-events.component.scss index 8e2e9558c2..3d76435018 100644 --- a/console/src/app/modules/filter-events/filter-events.component.scss +++ b/console/src/app/modules/filter-events/filter-events.component.scss @@ -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; diff --git a/console/src/app/modules/filter-events/filter-events.component.ts b/console/src/app/modules/filter-events/filter-events.component.ts index 158a767ce3..186436ddf2 100644 --- a/console/src/app/modules/filter-events/filter-events.component.ts +++ b/console/src/app/modules/filter-events/filter-events.component.ts @@ -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(false), - creationDateFilterSet: new FormControl(false), - creationDate: new FormControl(new Date()), + creationDateFilterType: new FormControl(CreationDateFilterType.FROM), + creationDateFrom: new FormControl(new Date()), + // creationDateSince is 15 minutes in the past by default + creationDateSince: new FormControl(new Date(new Date().getTime() - 15 * 60_000)), + creationDateUntil: new FormControl(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, event: EventTarget | null): void { + if (!(event instanceof HTMLInputElement)) { + throw new Error('wrong target'); + } + ctrl.setValue(new Date(event.value || '')); + } } diff --git a/console/src/app/modules/filter-events/filter-events.module.ts b/console/src/app/modules/filter-events/filter-events.module.ts index 9aff53d7d6..d4dd102ae3 100644 --- a/console/src/app/modules/filter-events/filter-events.module.ts +++ b/console/src/app/modules/filter-events/filter-events.module.ts @@ -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], }) diff --git a/console/src/app/pages/events/events.component.html b/console/src/app/pages/events/events.component.html index acc8c3677a..39c567b4d3 100644 --- a/console/src/app/pages/events/events.component.html +++ b/console/src/app/pages/events/events.component.html @@ -70,7 +70,7 @@ - + {{ 'IAM.EVENTS.SEQUENCE' | translate }} @@ -81,10 +81,12 @@ - {{ 'IAM.EVENTS.CREATIONDATE' | translate }} + + {{ 'IAM.EVENTS.CREATIONDATE' | translate }} + - {{ event?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }} + {{ event?.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm:ss' }} diff --git a/console/src/app/pages/events/events.component.ts b/console/src/app/pages/events/events.component.ts index 41f520a7a4..c6dd22fc8d 100644 --- a/console/src/app/pages/events/events.component.ts +++ b/console/src/app/pages/events/events.component.ts @@ -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); } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index e3a1e7876d..349a8496a6 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -865,14 +865,16 @@ }, "SEQUENCE": { "LABEL": "Последователност", - "CHECKBOX": "Филтриране по последователност", - "SORT": "Сортиране", - "ASC": "Възходящ", - "DESC": "Спускане" + "CHECKBOX": "Филтриране по последователност" }, + "SORT": "Сортиране", + "ASC": "Възходящ", + "DESC": "Спускане", "CREATIONDATE": { - "LABEL": "Дата на създаване", - "CHECKBOX": "Филтриране по дата на създаване" + "RADIO_FROM": "От", + "RADIO_RANGE": "Обхват", + "LABEL_SINCE": "От", + "LABEL_UNTIL": "До" }, "OTHER": "друго", "OTHERS": "други" diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index ead54bb53c..2da0d463e4 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "Sekvence", - "CHECKBOX": "Filtrovat podle Sekvence", - "SORT": "Třídění", - "ASC": "Vzestupně", - "DESC": "Sestupně" + "CHECKBOX": "Filtrovat podle Sekvence" }, + "SORT": "Třídění", + "ASC": "Vzestupně", + "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é" diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 1afe195e08..c2c28c7512 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -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" diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 5b96e6e8da..38066e7226 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -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" diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 7be679000b..c2444c2282 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "Secuencia", - "CHECKBOX": "Filtrar por secuencia", - "SORT": "Ordenado", - "ASC": "Ascendente", - "DESC": "Descendente" + "CHECKBOX": "Filtrar por secuencia" }, + "SORT": "Ordenado", + "ASC": "Ascendente", + "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" diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 3659d9000e..bcb2a9d501 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -871,14 +871,16 @@ }, "SEQUENCE": { "LABEL": "séquence", - "CHECKBOX": "Filtrer par séquence", - "SORT": "Triage", - "ASC": "Ascendant", - "DESC": "Descendant" + "CHECKBOX": "Filtrer par séquence" }, + "SORT": "Triage", + "ASC": "Ascendant", + "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" diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 97c890eed3..e3feb17fdd 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -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" diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 0977ea2be2..036eef2237 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "シーケンス", - "CHECKBOX": "シーケンスで絞り込み", - "SORT": "ソート", - "ASC": "昇順", - "DESC": "降順" + "CHECKBOX": "シーケンスで絞り込み" }, + "SORT": "ソート", + "ASC": "昇順", + "DESC": "降順", "CREATIONDATE": { - "LABEL": "作成日", - "CHECKBOX": "作成日で絞り込み" + "RADIO_FROM": "から", + "RADIO_RANGE": "範囲", + "LABEL_SINCE": "以降", + "LABEL_UNTIL": "まで" }, "OTHER": "その他", "OTHERS": "その他" diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 34810be3c3..54d1053ae5 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "Секвенца", - "CHECKBOX": "Филтер според секвенцата", - "SORT": "Сортирање", - "ASC": "Растечки", - "DESC": "Опаѓачки" + "CHECKBOX": "Филтер според секвенцата" }, + "SORT": "Сортирање", + "ASC": "Растечки", + "DESC": "Опаѓачки", "CREATIONDATE": { - "LABEL": "Датум на креирање", - "CHECKBOX": "Филтер според датумот на креирање" + "RADIO_FROM": "Од", + "RADIO_RANGE": "Ранг", + "LABEL_SINCE": "Од", + "LABEL_UNTIL": "До" }, "OTHER": "друго", "OTHERS": "други" diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index cb8c5dc555..9a1614799d 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "Reeks", - "CHECKBOX": "Filter op Reeks", - "SORT": "Sortering", - "ASC": "Oplopend", - "DESC": "Aflopend" + "CHECKBOX": "Filter op Reeks" }, + "SORT": "Sortering", + "ASC": "Oplopend", + "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" diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index aa48a434a7..4c5c20068c 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -871,14 +871,16 @@ }, "SEQUENCE": { "LABEL": "Sekwencja", - "CHECKBOX": "Filtruj według sekwencji", - "SORT": "Sortowanie", - "ASC": "Rosnące", - "DESC": "Malejące" + "CHECKBOX": "Filtruj według sekwencji" }, + "SORT": "Sortowanie", + "ASC": "Rosną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" diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index 69984728b2..6535c5b950 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -872,14 +872,16 @@ }, "SEQUENCE": { "LABEL": "Sequência", - "CHECKBOX": "Filtrar por Sequência", - "SORT": "Ordenação", - "ASC": "Crescente", - "DESC": "Decrescente" + "CHECKBOX": "Filtrar por Sequência" }, + "SORT": "Ordenação", + "ASC": "Crescente", + "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" diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 02c023f5b7..f147f8fb39 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -868,14 +868,16 @@ }, "SEQUENCE": { "LABEL": "Последовательность", - "CHECKBOX": "Фильтровать по последовательности", - "SORT": "Сортировка", - "ASC": "Восходящий", - "DESC": "По убыванию" + "CHECKBOX": "Фильтровать по последовательности" }, + "SORT": "Сортировка", + "ASC": "Восходящий", + "DESC": "По убыванию", "CREATIONDATE": { - "LABEL": "Дата создания", - "CHECKBOX": "Фильтровать по дате создания" + "RADIO_FROM": "От", + "RADIO_RANGE": "Диапазон", + "LABEL_SINCE": "С", + "LABEL_UNTIL": "К" }, "OTHER": "другой", "OTHERS": "другие" diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index d1eb3551d5..e6cb353a84 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -871,14 +871,16 @@ }, "SEQUENCE": { "LABEL": "序列", - "CHECKBOX": "按顺序过滤", - "SORT": "分拣", - "ASC": "上升中", - "DESC": "下降" + "CHECKBOX": "按顺序过滤" }, + "SORT": "分拣", + "ASC": "上升中", + "DESC": "下降", "CREATIONDATE": { - "LABEL": "创建日期", - "CHECKBOX": "按创建日期过滤" + "RADIO_FROM": "从", + "RADIO_RANGE": "范围", + "LABEL_SINCE": "自从", + "LABEL_UNTIL": "直到" }, "OTHER": "其他", "OTHERS": "其他" diff --git a/e2e/cypress/e2e/events/events.cy.ts b/e2e/cypress/e2e/events/events.cy.ts index 65362b6129..facd298b12 100644 --- a/e2e/cypress/e2e/events/events.cy.ts +++ b/e2e/cypress/e2e/events/events.cy.ts @@ -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); }); diff --git a/internal/api/grpc/admin/event.go b/internal/api/grpc/admin/event.go index 72adfa0151..576fce8af1 100644 --- a/internal/api/grpc/admin/event.go +++ b/internal/api/grpc/admin/event.go @@ -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 } diff --git a/internal/api/grpc/system/limits_integration_test.go b/internal/api/grpc/system/limits_integration_test.go index 2557d72c22..96ceafcdc3 100644 --- a/internal/api/grpc/system/limits_integration_test.go +++ b/internal/api/grpc/system/limits_integration_test.go @@ -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, }) diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index dc0dc962e9..e85ccea8a0 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -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 { From 3842319d07c54645064fa24674d19e568bd4d8a7 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 7 Dec 2023 13:12:21 +0100 Subject: [PATCH 02/23] fix(console): reset events filter to initial values (#7037) --- .../src/app/modules/filter-events/filter-events.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/console/src/app/modules/filter-events/filter-events.component.ts b/console/src/app/modules/filter-events/filter-events.component.ts index 186436ddf2..15bffd10b5 100644 --- a/console/src/app/modules/filter-events/filter-events.component.ts +++ b/console/src/app/modules/filter-events/filter-events.component.ts @@ -75,6 +75,8 @@ export class FilterEventsComponent implements OnInit { eventTypesList: new FormControl([]), }); + private initialValues = this.form.getRawValue(); + constructor( private adminService: AdminService, private toast: ToastService, @@ -172,6 +174,7 @@ export class FilterEventsComponent implements OnInit { public reset(): void { this.form.reset(); + this.form.setValue(this.initialValues); this.emitChange(); } From d639c5200a7af2aff468c46757c4bfa89f29b5b8 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Thu, 7 Dec 2023 13:31:01 +0100 Subject: [PATCH 03/23] feat: manage restrictions in console (#6965) * feat: return 404 or 409 if org reg disallowed * fix: system limit permissions * feat: add iam limits api * feat: disallow public org registrations on default instance * add integration test * test: integration * fix test * docs: describe public org registrations * avoid updating docs deps * fix system limits integration test * silence integration tests * fix linting * ignore strange linter complaints * review * improve reset properties naming * redefine the api * use restrictions aggregate * test query * simplify and test projection * test commands * fix unit tests * move integration test * support restrictions on default instance * also test GetRestrictions * self review * lint * abstract away resource owner * fix tests * configure supported languages * fix allowed languages * fix tests * default lang must not be restricted * preferred language must be allowed * change preferred languages * check languages everywhere * lint * test command side * lint * add integration test * add integration test * restrict supported ui locales * lint * lint * cleanup * lint * allow undefined preferred language * fix integration tests * update main * fix env var * ignore linter * ignore linter * improve integration test config * reduce cognitive complexity * compile * fix(console): switch back to saved language * feat(API): get allowed languages * fix(console): only make allowed languages selectable * warn when editing not allowed languages * feat: manage restrictions in console * check for duplicates * remove useless restriction checks * review * revert restriction renaming * manage languages * fix language restrictions * lint * generate * allow custom texts for supported langs for now * fix tests * cleanup * cleanup * cleanup * lint * unsupported preferred lang is allowed * fix integration test * allow unsupported preferred languages * lint * fix languages lists * simplify default language selection * translate * discard * lint * load languages for tests * load languages * lint * cleanup * lint * cleanup * get allowed only on admin * cleanup * reduce flakiness on very limited postgres * simplify langSvc * refactor according to suggestions in pr * lint * improve ux * update central allowed languages * set first allowed language as default * readd lost translations * disable sorting disallowed languages * fix permissions * lint * selectionchange for language in msg texts * initialize login texts * init message texts * lint * fix drag and drop list styles * start from 1 * cleanup * prettier * correct orgdefaultlabel * unsubscribe * lint * docs: describe language settings --------- Co-authored-by: peintnermax --- .../general-settings.component.html | 27 ---- .../general-settings.component.scss | 18 --- .../general-settings.component.ts | 56 -------- .../general-settings.module.ts | 29 ---- .../language-settings.component.html | 135 ++++++++++++++++++ .../language-settings.component.scss | 112 +++++++++++++++ .../language-settings.component.spec.ts} | 12 +- .../language-settings.component.ts | 116 +++++++++++++++ .../language-settings.module.ts | 46 ++++++ .../login-policy/login-policy.component.html | 21 ++- .../login-policy/login-policy.component.ts | 118 +++++++++------ .../settings-list.component.html | 6 +- .../settings-list/settings-list.module.ts | 4 +- .../src/app/modules/settings-list/settings.ts | 6 +- .../instance-settings.component.ts | 4 +- console/src/app/services/admin.service.ts | 27 ++++ console/src/app/services/languages.service.ts | 1 - console/src/app/services/toast.service.ts | 4 +- console/src/assets/i18n/bg.json | 47 +++--- console/src/assets/i18n/cs.json | 46 +++--- console/src/assets/i18n/de.json | 47 +++--- console/src/assets/i18n/en.json | 47 +++--- console/src/assets/i18n/es.json | 47 +++--- console/src/assets/i18n/fr.json | 47 +++--- console/src/assets/i18n/it.json | 47 +++--- console/src/assets/i18n/ja.json | 47 +++--- console/src/assets/i18n/mk.json | 47 +++--- console/src/assets/i18n/nl.json | 47 +++--- console/src/assets/i18n/pl.json | 47 +++--- console/src/assets/i18n/pt.json | 47 +++--- console/src/assets/i18n/ru.json | 45 +++--- console/src/assets/i18n/zh.json | 47 +++--- docs/docs/guides/manage/customize/texts.md | 13 +- docs/static/img/guides/console/languages.png | Bin 0 -> 130792 bytes 34 files changed, 961 insertions(+), 449 deletions(-) delete mode 100644 console/src/app/modules/policies/general-settings/general-settings.component.html delete mode 100644 console/src/app/modules/policies/general-settings/general-settings.component.scss delete mode 100644 console/src/app/modules/policies/general-settings/general-settings.component.ts delete mode 100644 console/src/app/modules/policies/general-settings/general-settings.module.ts create mode 100644 console/src/app/modules/policies/language-settings/language-settings.component.html create mode 100644 console/src/app/modules/policies/language-settings/language-settings.component.scss rename console/src/app/modules/policies/{general-settings/general-settings.component.spec.ts => language-settings/language-settings.component.spec.ts} (50%) create mode 100644 console/src/app/modules/policies/language-settings/language-settings.component.ts create mode 100644 console/src/app/modules/policies/language-settings/language-settings.module.ts create mode 100644 docs/static/img/guides/console/languages.png diff --git a/console/src/app/modules/policies/general-settings/general-settings.component.html b/console/src/app/modules/policies/general-settings/general-settings.component.html deleted file mode 100644 index a465cf3466..0000000000 --- a/console/src/app/modules/policies/general-settings/general-settings.component.html +++ /dev/null @@ -1,27 +0,0 @@ -

{{ 'SETTING.DEFAULTLANGUAGE' | translate }}

- -
- -
- - - {{ 'SETTING.DEFAULTLANGUAGE' | translate }} - - - {{ lang }} - {{ 'SETTING.LANGUAGE.' + lang | translate }} - - - - -
- -
diff --git a/console/src/app/modules/policies/general-settings/general-settings.component.scss b/console/src/app/modules/policies/general-settings/general-settings.component.scss deleted file mode 100644 index 1e41e2908a..0000000000 --- a/console/src/app/modules/policies/general-settings/general-settings.component.scss +++ /dev/null @@ -1,18 +0,0 @@ -.spinner-wr { - margin: 0.5rem 0; -} - -.default-language { - max-width: 400px; - display: block; -} - -.general-btn-container { - display: flex; - justify-content: flex-start; - margin-top: 1rem; - - .save-button { - display: block; - } -} diff --git a/console/src/app/modules/policies/general-settings/general-settings.component.ts b/console/src/app/modules/policies/general-settings/general-settings.component.ts deleted file mode 100644 index a3c8c399d7..0000000000 --- a/console/src/app/modules/policies/general-settings/general-settings.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { SetDefaultLanguageResponse } from 'src/app/proto/generated/zitadel/admin_pb'; -import { AdminService } from 'src/app/services/admin.service'; -import { ToastService } from 'src/app/services/toast.service'; - -@Component({ - selector: 'cnsl-general-settings', - templateUrl: './general-settings.component.html', - styleUrls: ['./general-settings.component.scss'], -}) -export class GeneralSettingsComponent implements OnInit { - public defaultLanguage: string = ''; - public defaultLanguageOptions: string[] = []; - - public loading: boolean = false; - constructor( - private service: AdminService, - private toast: ToastService, - ) {} - - ngOnInit(): void { - this.fetchData(); - } - - private fetchData(): void { - this.service.getDefaultLanguage().then((langResp) => { - this.defaultLanguage = langResp.language; - }); - this.service.getAllowedLanguages().then((supportedResp) => { - this.defaultLanguageOptions = supportedResp.languagesList; - }); - } - - private updateData(): Promise { - return (this.service as AdminService).setDefaultLanguage(this.defaultLanguage); - } - - public savePolicy(): void { - const prom = this.updateData(); - this.loading = true; - if (prom) { - prom - .then(() => { - this.toast.showInfo('POLICY.LOGIN_POLICY.SAVED', true); - this.loading = false; - setTimeout(() => { - this.fetchData(); - }, 2000); - }) - .catch((error) => { - this.loading = false; - this.toast.showError(error); - }); - } - } -} diff --git a/console/src/app/modules/policies/general-settings/general-settings.module.ts b/console/src/app/modules/policies/general-settings/general-settings.module.ts deleted file mode 100644 index 98f0e4cb53..0000000000 --- a/console/src/app/modules/policies/general-settings/general-settings.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSelectModule } from '@angular/material/select'; -import { TranslateModule } from '@ngx-translate/core'; -import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; - -import { CardModule } from '../../card/card.module'; -import { FormFieldModule } from '../../form-field/form-field.module'; -import { GeneralSettingsComponent } from './general-settings.component'; - -@NgModule({ - declarations: [GeneralSettingsComponent], - imports: [ - CommonModule, - CardModule, - FormsModule, - MatButtonModule, - FormFieldModule, - MatProgressSpinnerModule, - MatSelectModule, - HasRolePipeModule, - TranslateModule, - ], - exports: [GeneralSettingsComponent], -}) -export class GeneralSettingsModule {} diff --git a/console/src/app/modules/policies/language-settings/language-settings.component.html b/console/src/app/modules/policies/language-settings/language-settings.component.html new file mode 100644 index 0000000000..e03b005a53 --- /dev/null +++ b/console/src/app/modules/policies/language-settings/language-settings.component.html @@ -0,0 +1,135 @@ +

{{ 'SETTING.LANGUAGES.TITLE' | translate }}

+ +
+ +
+ +
+
+
+
+
+ {{ 'SETTING.LANGUAGES.ALLOWED' | translate }} + +
+
+
+ {{ i + 1 }} + {{ lang }} + {{ 'SETTING.LANGUAGES.OPTIONS.' + lang | translate }} + {{ + 'SETTING.LANGUAGES.DEFAULT' | translate + }} + + + + + + +
+
+
+
+
+
+
+ {{ 'SETTING.LANGUAGES.NOT_ALLOWED' | translate }} + +
+
+
+ {{ lang }} + {{ 'SETTING.LANGUAGES.OPTIONS.' + lang | translate }} +
+
+
+
+
+ + +
+
+
diff --git a/console/src/app/modules/policies/language-settings/language-settings.component.scss b/console/src/app/modules/policies/language-settings/language-settings.component.scss new file mode 100644 index 0000000000..90f0b42e91 --- /dev/null +++ b/console/src/app/modules/policies/language-settings/language-settings.component.scss @@ -0,0 +1,112 @@ +.languages-container-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 2rem; + + @media only screen and (max-width: 700px) { + grid-template-columns: 1fr; + } + + .languages-container { + display: inline-block; + max-width: 400px; + vertical-align: top; + width: 100%; + + .spinner-wr { + margin: 0.5rem 0; + } + + .default-language { + max-width: 400px; + display: block; + } + } + + .general-btn-container { + display: flex; + justify-content: flex-start; + margin-top: 1rem; + + .save-button { + display: block; + margin-left: 1rem; + } + } +} + +.languages-list { + overflow: hidden; + display: block; + height: 100%; + + .languages-top-row { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 1rem; + margin-top: 1.5rem; + + .label { + margin-right: 1rem; + flex: 1; + white-space: nowrap; + } + + .list-button { + white-space: nowrap; + } + } +} + +.languages-box { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.5rem 0.5rem 2rem; + height: 56px; + cursor: move; + margin: 2px 0; + + .index { + position: absolute; + top: 4px; + left: 4px; + opacity: 0.5; + font-size: 10px; + } + + .locale { + width: 35px; + margin-right: 1rem; + } + + .lang { + flex: 1; + } + + [hoveractions] { + display: none; + } + + .more-button { + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + } + + &:hover { + [hoveractions] { + display: flex; + } + } + + .defaultlanglabel { + margin-left: 0.5rem; + } +} diff --git a/console/src/app/modules/policies/general-settings/general-settings.component.spec.ts b/console/src/app/modules/policies/language-settings/language-settings.component.spec.ts similarity index 50% rename from console/src/app/modules/policies/general-settings/general-settings.component.spec.ts rename to console/src/app/modules/policies/language-settings/language-settings.component.spec.ts index 23eee2e9f4..8da2c7bb8e 100644 --- a/console/src/app/modules/policies/general-settings/general-settings.component.spec.ts +++ b/console/src/app/modules/policies/language-settings/language-settings.component.spec.ts @@ -1,19 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { GeneralSettingsComponent } from './general-settings.component'; +import { LanguageSettingsComponent } from './language-settings.component'; -describe('GeneralSettingsComponent', () => { - let component: GeneralSettingsComponent; - let fixture: ComponentFixture; +describe('LanguageSettingsComponent', () => { + let component: LanguageSettingsComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [GeneralSettingsComponent], + declarations: [LanguageSettingsComponent], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(GeneralSettingsComponent); + fixture = TestBed.createComponent(LanguageSettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/console/src/app/modules/policies/language-settings/language-settings.component.ts b/console/src/app/modules/policies/language-settings/language-settings.component.ts new file mode 100644 index 0000000000..f2af4641ff --- /dev/null +++ b/console/src/app/modules/policies/language-settings/language-settings.component.ts @@ -0,0 +1,116 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { AdminService } from 'src/app/services/admin.service'; +import { ToastService } from 'src/app/services/toast.service'; +import { UntypedFormBuilder } from '@angular/forms'; +import { LanguagesService } from '../../../services/languages.service'; +import { BehaviorSubject, concat, forkJoin, from, Observable, of, Subject, switchMap, take, takeUntil } from 'rxjs'; +import { GrpcAuthService } from '../../../services/grpc-auth.service'; +import { CdkDrag, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; +import { catchError, map } from 'rxjs/operators'; + +interface State { + allowed: string[]; + notAllowed: string[]; +} + +@Component({ + selector: 'cnsl-language-settings', + templateUrl: './language-settings.component.html', + styleUrls: ['./language-settings.component.scss'], +}) +export class LanguageSettingsComponent { + public canWriteRestrictions$: Observable = this.authService.isAllowed(['iam.restrictions.write']); + public canWriteDefaultLanguage$: Observable = this.authService.isAllowed(['iam.write']); + + public localState$ = new BehaviorSubject({ allowed: [], notAllowed: [] }); + public remoteState$ = new BehaviorSubject({ allowed: [], notAllowed: [] }); + public defaultLang$ = new BehaviorSubject(''); + + public loading: boolean = false; + constructor( + private service: AdminService, + private toast: ToastService, + private langSvc: LanguagesService, + private authService: GrpcAuthService, + ) { + const sub = forkJoin([ + langSvc.allowed$.pipe(take(1)), + langSvc.notAllowed$.pipe(take(1)), + from(this.service.getDefaultLanguage()).pipe(take(1)), + ]).subscribe({ + next: ([allowed, notAllowed, { language: defaultLang }]) => { + this.defaultLang$.next(defaultLang); + this.remoteState$.next({ notAllowed: [...notAllowed], ...{ allowed: [...allowed] } }); + this.localState$.next({ notAllowed: [...notAllowed], ...{ allowed: [...allowed] } }); + }, + error: this.toast.showError, + complete: () => { + sub.unsubscribe(); + }, + }); + } + + drop(event: CdkDragDrop) { + if (event.previousContainer === event.container) { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } else { + transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex); + } + } + + public defaultLangPredicate = (lang: CdkDrag) => { + return !!lang?.data && lang.data !== this.defaultLang$.value; + }; + + public isRemotelyAllowed$(lang: string): Observable { + return this.remoteState$.pipe(map(({ allowed }) => allowed.includes(lang))); + } + + public allowAll(): void { + this.localState$.next({ allowed: [...this.allLocalLangs()], notAllowed: [] }); + } + + public disallowAll(): void { + const disallowed = this.allLocalLangs().filter((lang) => lang !== this.defaultLang$.value); + this.localState$.next({ allowed: [this.defaultLang$.value], notAllowed: disallowed }); + } + + public submit(): void { + const { allowed, notAllowed } = this.localState$.value; + const sub = from(this.service.setRestrictions(undefined, allowed)).subscribe({ + next: () => { + this.remoteState$.next({ + allowed: [...allowed], + notAllowed: [...notAllowed], + }); + this.langSvc.newAllowed(allowed); + this.toast.showInfo('SETTING.LANGUAGES.ALLOWED_SAVED', true); + }, + error: this.toast.showError, + complete: () => { + sub.unsubscribe(); + }, + }); + } + + public discard(): void { + this.localState$.next(this.remoteState$.value); + } + + public setDefaultLang(lang: string): void { + const sub = from(this.service.setDefaultLanguage(lang)).subscribe({ + next: () => { + this.defaultLang$.next(lang); + this.toast.showInfo('SETTING.LANGUAGES.DEFAULT_SAVED', true); + }, + error: this.toast.showError, + complete: () => { + sub.unsubscribe(); + }, + }); + } + + private allLocalLangs(): string[] { + return [...this.localState$.value.allowed, ...this.localState$.value.notAllowed]; + } +} diff --git a/console/src/app/modules/policies/language-settings/language-settings.module.ts b/console/src/app/modules/policies/language-settings/language-settings.module.ts new file mode 100644 index 0000000000..f19a0d3d3e --- /dev/null +++ b/console/src/app/modules/policies/language-settings/language-settings.module.ts @@ -0,0 +1,46 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { TranslateModule } from '@ngx-translate/core'; +import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; + +import { CardModule } from '../../card/card.module'; +import { FormFieldModule } from '../../form-field/form-field.module'; +import { LanguageSettingsComponent } from './language-settings.component'; +import { MatListModule } from '@angular/material/list'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TableActionsModule } from '../../table-actions/table-actions.module'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIconModule } from '@angular/material/icon'; + +@NgModule({ + declarations: [LanguageSettingsComponent], + imports: [ + CommonModule, + CardModule, + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatButtonModule, + MatSelectModule, + FormFieldModule, + MatProgressSpinnerModule, + MatSelectModule, + HasRolePipeModule, + TranslateModule, + MatListModule, + DragDropModule, + MatRadioModule, + MatTooltipModule, + MatMenuModule, + MatIconModule, + ], + exports: [LanguageSettingsComponent], +}) +export class LanguageSettingsModule {} diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index 7fd7e2627f..ece4d1c405 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -242,14 +242,20 @@ | async) === false " > - {{ 'POLICY.DATA.ALLOWREGISTER' | translate }} + {{ 'POLICY.DATA.ALLOWREGISTERUSERS' | translate }} + + +