diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index d008b8e5c7..69791908b3 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -129,10 +129,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor { this.showError(req.method + ': ' + req.url + '
' + errorResponse.status + ': ' + errorResponse.statusText); } - } else if (errorResponse.status === 504) { - if (!ignoreErrors) { - this.showError('Request timeout'); - } } else { unhandled = true; } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index bf57dd4441..7f13f08ba6 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -23,7 +23,7 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; -import { serverErrorCodesTranslations } from '@shared/models/constants'; +import { httpStatusMessageMap, serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, @@ -910,11 +910,13 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, } else { error = errorResponse.error; } - if (error && !error.message) { - errorMessage = prepareMessageFromData(error); - } else if (error && error.message) { + if (error && error.message) { errorMessage = error.message; timeout = error.timeout ? error.timeout : 0; + } else if (isProxyError(errorResponse)) { + errorMessage = httpStatusMessageMap.get(errorResponse.status); + } else if (error) { + errorMessage = prepareMessageFromData(error); } else { errorMessage = `Unhandled error code ${error ? error.status : '\'Unknown\''}`; } @@ -951,6 +953,14 @@ function prepareMessageFromData(data): string { } } +function isProxyError(errorResponse: HttpErrorResponse): boolean { + if (!httpStatusMessageMap.has(errorResponse.status)) { + return false; + } + const error = errorResponse.error; + return !error || typeof error === 'string' || (typeof error === 'object' && !error.message); +} + export const genNextLabel = (name: string, datasources: Datasource[]): string => { let label = name; let i = 1; diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 106b79ab9a..7a0489345f 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -31,7 +31,6 @@ close + [matAutocompleteDisabled]="!predefinedValues?.length && !fetchOptionsFn"> @for (value of filteredValues | async; track value.value) { - + } @empty { @if (!allowUserValue) { - {{ 'common.not-found' | translate }} + {{ 'common.not-found' | translate }} } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index c7f905f68b..6d0c57ab0a 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -29,7 +29,7 @@ import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angula import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { isDefined, isUndefined } from '@core/utils'; export interface StringItemsOption { @@ -37,18 +37,18 @@ export interface StringItemsOption { value: any; } @Component({ - selector: 'tb-string-items-list', - templateUrl: './string-items-list.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => StringItemsListComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None, - standalone: false + selector: 'tb-string-items-list', + templateUrl: './string-items-list.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StringItemsListComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class StringItemsListComponent implements ControlValueAccessor, OnInit { @@ -149,9 +149,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { - if (value && typeof value !== 'string') { - this.add(value); - } else if (value === null) { + if (value === null) { this.clear(); } }), @@ -209,16 +207,17 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addOnBlur(event: FocusEvent) { - const target: HTMLElement = event.relatedTarget as HTMLElement; - if ((target && target.tagName !== 'MAT-OPTION') || this.allowUserValue) { - this.addItem(this.stringItemInput.nativeElement.value ?? ''); + onOptionSelected(event: MatAutocompleteSelectedEvent): void { + if (event.option.value != null) { + this.add(event.option.value); + } else { + this.clear(); } - this.onTouched(); } addOnEnd(event: MatChipInputEvent): void { - this.addItem(event.value ?? '') + this.addItem(event.value ?? ''); + this.onTouched(); } removeItems(item: StringItemsOption) { @@ -295,6 +294,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.stringItemInput.nativeElement.value = value; this.itemControl.patchValue(value, {emitEvent: true}); setTimeout(() => { + this.stringItemInput.nativeElement.value = value; + this.itemControl.patchValue(null, {emitEvent: false}); this.stringItemInput.nativeElement.blur(); this.stringItemInput.nativeElement.focus(); }, 0); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 2890b4827c..afc68fdfcd 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -56,6 +56,12 @@ export const serverErrorCodesTranslations = new Map([ [Constants.serverErrorCode.entitiesLimitExceeded, 'server-error.entities-limit-exceeded'], ]); +export const httpStatusMessageMap = new Map([ + [502, 'Server is temporarily unavailable (Bad Gateway)'], + [503, 'Server is temporarily unavailable'], + [504, 'Server did not respond in time (Gateway Timeout)'], +]); + export const MediaBreakpoints = { xs: 'screen and (max-width: 599px)', sm: 'screen and (min-width: 600px) and (max-width: 959px)', diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 721b8b8006..8aafb1a243 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6033,9 +6033,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.9: - version "3.4.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.1.tgz#84ccd9579e76e9cc0d246c11d8be0beb019143e6" - integrity sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ== + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== "flot.curvedlines@https://github.com/MichaelZinsmaier/CurvedLines.git#master": version "1.1.1"