Browse Source

UI: Improve RPC state widget error handling.

pull/10053/head
Igor Kulikov 2 years ago
parent
commit
ebb2bda32f
  1. 2
      application/src/main/data/json/system/widget_types/single_switch.json
  2. 5
      application/src/main/data/json/system/widget_types/slide_toggle_control.json
  3. 3
      ui-ngx/angular.json
  4. 16
      ui-ngx/src/app/core/api/widget-api.models.ts
  5. 237
      ui-ngx/src/app/core/api/widget-subscription.ts
  6. 1
      ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  8. 91
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  10. 6
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
  11. 9
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
  12. 4
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  13. 1
      ui-ngx/src/app/shared/components/popover.models.ts
  14. 8
      ui-ngx/src/app/shared/models/rpc-widget-settings.models.ts
  15. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/data/json/system/widget_types/single_switch.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/slide_toggle_control.json

File diff suppressed because one or more lines are too long

3
ui-ngx/angular.json

@ -158,7 +158,8 @@
"ace-builds",
"diff-match-patch",
"tv4",
"@messageformat/parser"
"@messageformat/parser",
"sorted-btree"
]
},
"configurations": {

16
ui-ngx/src/app/core/api/widget-api.models.ts

@ -37,7 +37,7 @@ import { RafService } from '@core/services/raf.service';
import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@app/shared/models/entity.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import * as moment_ from 'moment';
import moment_ from 'moment';
import {
AlarmData,
AlarmDataPageLink,
@ -130,12 +130,12 @@ export interface IAliasController {
getFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
updateUserFilter(filter: Filter);
updateEntityAliases(entityAliases: EntityAliases);
updateFilters(filters: Filters);
updateAliases(aliasIds?: Array<string>);
dashboardStateChanged();
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo): void;
updateUserFilter(filter: Filter): void;
updateEntityAliases(entityAliases: EntityAliases): void;
updateFilters(filters: Filters): void;
updateAliases(aliasIds?: Array<string>): void;
dashboardStateChanged(): void;
}
export interface StateObject {
@ -315,7 +315,7 @@ export interface IWidgetSubscription {
rpcEnabled?: boolean;
executingRpcRequest?: boolean;
rpcErrorText?: string;
rpcRejection?: HttpErrorResponse;
rpcRejection?: HttpErrorResponse | Error;
getFirstEntityInfo(): SubscriptionEntityInfo;

237
ui-ngx/src/app/core/api/widget-subscription.ts

@ -55,7 +55,7 @@ import {
} from '@app/shared/models/time/time.models';
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError, timer } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import {
createLabelFromPattern,
deepClone,
@ -67,7 +67,7 @@ import {
parseHttpErrorMessage
} from '@core/utils';
import { EntityId } from '@app/shared/models/id/entity-id';
import * as moment_ from 'moment';
import moment_ from 'moment';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityDataListener } from '@core/api/entity-data.service';
import {
@ -205,8 +205,9 @@ export class WidgetSubscription implements IWidgetSubscription {
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcDisabledReason: string;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
rpcRejection: HttpErrorResponse | Error;
init$: Observable<IWidgetSubscription>;
@ -292,13 +293,15 @@ export class WidgetSubscription implements IWidgetSubscription {
this.subscriptionTimewindow = null;
this.loadingData = false;
this.displayLegend = false;
this.initAlarmSubscription().subscribe(() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
},
() => {
subscriptionSubject.error(null);
});
this.initAlarmSubscription().subscribe({
next:() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
},
error: () => {
subscriptionSubject.error(null);
}}
);
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onLatestDataUpdated = this.callbacks.onLatestDataUpdated || (() => {});
@ -373,13 +376,15 @@ export class WidgetSubscription implements IWidgetSubscription {
this.legendConfig.showAvg === true ||
this.legendConfig.showTotal === true ||
this.legendConfig.showLatest === true);
this.initDataSubscription().subscribe(() => {
this.initDataSubscription().subscribe({
next:() => {
subscriptionSubject.next(this);
subscriptionSubject.complete();
},
(err) => {
subscriptionSubject.error(err);
});
error: () => {
subscriptionSubject.error(null);
}}
);
}
}
@ -401,6 +406,16 @@ export class WidgetSubscription implements IWidgetSubscription {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.utils.widgetEditMode;
if (!this.rpcEnabled) {
if (this.targetEntityId) {
const entityType =
this.ctx.translate.instant(entityTypeTranslations.get(this.targetEntityId.entityType).type);
this.rpcDisabledReason =
this.ctx.translate.instant('rpc.error.invalid-target-entity', {entityType});
} else {
this.rpcDisabledReason = this.ctx.translate.instant('rpc.error.target-device-is-not-set');
}
}
}
this.hasResolvedData = true;
this.callbacks.rpcStateChanged(this);
@ -409,6 +424,7 @@ export class WidgetSubscription implements IWidgetSubscription {
},
error: () => {
this.rpcEnabled = false;
this.rpcDisabledReason = this.ctx.translate.instant('rpc.error.failed-to-resolve-target-device');
this.callbacks.rpcStateChanged(this);
initRpcSubject.next();
initRpcSubject.complete();
@ -427,17 +443,19 @@ export class WidgetSubscription implements IWidgetSubscription {
initAlarmSubscriptionSubject.complete();
} else {
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe(
(alarmSource) => {
this.alarmSource = alarmSource;
if (alarmSource) {
this.hasResolvedData = true;
{
next: (alarmSource) => {
this.alarmSource = alarmSource;
if (alarmSource) {
this.hasResolvedData = true;
}
this.configureAlarmsData();
initAlarmSubscriptionSubject.next();
initAlarmSubscriptionSubject.complete();
},
error: (err) => {
initAlarmSubscriptionSubject.error(err);
}
this.configureAlarmsData();
initAlarmSubscriptionSubject.next();
initAlarmSubscriptionSubject.complete();
},
(err) => {
initAlarmSubscriptionSubject.error(err);
}
);
}
@ -464,18 +482,20 @@ export class WidgetSubscription implements IWidgetSubscription {
);
} else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe(
(datasources) => {
this.configuredDatasources = datasources;
this.prepareDataSubscriptions().subscribe(
() => {
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
}
);
},
(err) => {
this.notifyDataLoaded();
initDataSubscriptionSubject.error(err);
{
next: (datasources) => {
this.configuredDatasources = datasources;
this.prepareDataSubscriptions().subscribe(
() => {
initDataSubscriptionSubject.next();
initDataSubscriptionSubject.complete();
}
);
},
error: (err) => {
this.notifyDataLoaded();
initDataSubscriptionSubject.error(err);
}
}
);
}
@ -801,9 +821,11 @@ export class WidgetSubscription implements IWidgetSubscription {
persistent?: boolean, persistentPollingInterval?: number, retries?: number,
additionalInfo?: any, requestUUID?: string): Observable<any> {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
this.rpcErrorText = this.rpcDisabledReason;
this.rpcRejection = new Error(this.rpcErrorText);
return throwError(() => this.rpcRejection);
} else {
if (this.rpcRejection && this.rpcRejection.status !== 504) {
if (this.rpcRejection && (!(this.rpcRejection as any).status || (this.rpcRejection as HttpErrorResponse).status !== 504)) {
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
@ -848,9 +870,9 @@ export class WidgetSubscription implements IWidgetSubscription {
persistentRespons.status !== RpcStatus.DELIVERED && persistentRespons.status !== RpcStatus.QUEUED),
switchMap(persistentResponse => {
if ([RpcStatus.TIMEOUT, RpcStatus.EXPIRED].includes(persistentResponse.status)) {
return throwError({status: 504});
return throwError(() => ({status: 504}));
} else if (persistentResponse.status === RpcStatus.FAILED) {
return throwError({status: 502, statusText: persistentResponse.response.error});
return throwError(() => ({status: 502, statusText: persistentResponse.response.error}));
} else {
return of(persistentResponse.response);
}
@ -861,40 +883,43 @@ export class WidgetSubscription implements IWidgetSubscription {
return of(response);
})
)
.subscribe((responseBody) => {
this.rpcRejection = null;
this.rpcErrorText = null;
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.onRpcSuccess(this);
rpcSubject.next(responseBody);
rpcSubject.complete();
},
(rejection: HttpErrorResponse) => {
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.rpcStateChanged(this);
if (!this.executingRpcRequest || rejection.status === 504) {
this.rpcRejection = rejection;
if (rejection.status === 504) {
this.rpcErrorText = 'Request Timeout.';
} else {
this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
const error = parseHttpErrorMessage(rejection, this.ctx.translate);
if (error) {
this.rpcErrorText += '</br>';
this.rpcErrorText += error.message;
.subscribe({
next: (responseBody) => {
this.rpcRejection = null;
this.rpcErrorText = null;
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.onRpcSuccess(this);
rpcSubject.next(responseBody);
rpcSubject.complete();
},
error: (rejection: HttpErrorResponse) => {
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice( index, 1 );
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.callbacks.rpcStateChanged(this);
if (!this.executingRpcRequest || rejection.status === 504) {
this.rpcRejection = rejection;
if (rejection.status === 504) {
this.rpcErrorText = this.ctx.translate.instant('rpc.error.request-timeout');
} else {
this.rpcErrorText = this.ctx.translate.instant('rpc.error.rpc-http-error',
{status: rejection.status, statusText: rejection.statusText});
const error = parseHttpErrorMessage(rejection, this.ctx.translate);
if (error) {
this.rpcErrorText += '</br>';
this.rpcErrorText += error.message;
}
}
this.callbacks.onRpcFailed(this);
}
this.callbacks.onRpcFailed(this);
rpcSubject.error(rejection);
}
rpcSubject.error(rejection);
});
}
return rpcSubject.asObservable();
@ -937,7 +962,7 @@ export class WidgetSubscription implements IWidgetSubscription {
subscribeAllForPaginatedData(pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]): Observable<any> {
const observables: Observable<any>[] = [];
this.configuredDatasources.forEach((datasource, datasourceIndex) => {
this.configuredDatasources.forEach((_datasource, datasourceIndex) => {
observables.push(this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters));
});
if (observables.length) {
@ -1118,16 +1143,18 @@ export class WidgetSubscription implements IWidgetSubscription {
this.updateAlarmDataSubscription();
} else {
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe(
(alarmSource) => {
this.alarmSource = alarmSource;
if (alarmSource) {
this.hasResolvedData = true;
{
next: (alarmSource) => {
this.alarmSource = alarmSource;
if (alarmSource) {
this.hasResolvedData = true;
}
this.configureAlarmsData();
this.updateAlarmDataSubscription();
},
error: () => {
this.notifyDataLoaded();
}
this.configureAlarmsData();
this.updateAlarmDataSubscription();
},
() => {
this.notifyDataLoaded();
}
);
}
@ -1193,16 +1220,18 @@ export class WidgetSubscription implements IWidgetSubscription {
);
} else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe(
(datasources) => {
this.configuredDatasources = datasources;
this.prepareDataSubscriptions().subscribe(
() => {
this.updatePaginatedDataSubscriptions();
}
);
},
() => {
this.notifyDataLoaded();
{
next: (datasources) => {
this.configuredDatasources = datasources;
this.prepareDataSubscriptions().subscribe(
() => {
this.updatePaginatedDataSubscriptions();
}
);
},
error: () => {
this.notifyDataLoaded();
}
}
);
}
@ -1318,7 +1347,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataLoaded(pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>,
datasourceIndex: number, pageLink: EntityDataPageLink, isUpdate: boolean) {
datasourceIndex: number, _pageLink: EntityDataPageLink, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) =>
@ -1403,7 +1432,7 @@ export class WidgetSubscription implements IWidgetSubscription {
});
if (datasource.latestDataKeys && datasource.latestDataKeys.length) {
this.hasLatestData = true;
datasource.latestDataKeys.forEach((dataKey, currentLatestDataKeyIndex) => {
datasource.latestDataKeys.forEach((_dataKey, currentLatestDataKeyIndex) => {
const currentDataKeyIndex = datasource.dataKeys.length + currentLatestDataKeyIndex;
const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex];
this.latestData.push(datasourceData);
@ -1608,7 +1637,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated();
}
private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
this.alarmsLoaded(alarms, 0, 0);
}
@ -1639,15 +1668,17 @@ export class WidgetSubscription implements IWidgetSubscription {
const loadSubject = new ReplaySubject<void>(1);
if (this.ctx.getServerTimeDiff && this.timeWindow) {
this.ctx.getServerTimeDiff().subscribe(
(stDiff) => {
this.timeWindow.stDiff = stDiff;
loadSubject.next();
loadSubject.complete();
},
() => {
this.timeWindow.stDiff = 0;
loadSubject.next();
loadSubject.complete();
{
next: (stDiff) => {
this.timeWindow.stDiff = stDiff;
loadSubject.next();
loadSubject.complete();
},
error: () => {
this.timeWindow.stDiff = 0;
loadSubject.next();
loadSubject.complete();
}
}
);
} else {

1
ui-ngx/src/app/modules/home/components/widget/config/target-device.component.html

@ -32,7 +32,6 @@
*ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.entity"
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityType.DEVICE]"
[callbacks]="entityAliasSelectCallbacks"
formControlName="entityAliasId">
</tb-entity-alias-select>

2
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -57,7 +57,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
rpcRejection: HttpErrorResponse | Error;
[key: string]: any;

91
ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts

@ -14,9 +14,14 @@
/// limitations under the License.
///
import { AttributeData, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import {
AttributeData,
AttributeScope,
LatestTelemetry,
telemetryTypeTranslationsShort
} from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, delay, map, share } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service';
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnInit, TemplateRef } from '@angular/core';
@ -24,6 +29,7 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import {
RpcActionSettings,
RpcGetAttributeSettings,
RpcSetAttributeSettings,
RpcSettings,
@ -42,6 +48,7 @@ import {
RpcUpdateStateSettings
} from '@app/shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -77,7 +84,7 @@ export abstract class BasicRpcStateWidgetComponent<V, S extends RpcStateWidgetSe
this.overlayStyle = overlayStyle(this.settings.background.overlay);
const behaviourSettings: RpcStateBehaviourSettings<V> = {
initialState: this.settings.initialState,
initialState: this.initialState(),
updateStateByValue: val => this.getUpdateStateSettingsForValue(val)
};
@ -117,6 +124,8 @@ export abstract class BasicRpcStateWidgetComponent<V, S extends RpcStateWidgetSe
protected abstract defaultSettings(): S;
protected abstract initialState(): RpcInitialStateSettings<V>;
protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings;
protected stateValueType(): ValueType {
@ -161,14 +170,14 @@ export class RpcStateBehaviorApi<V> extends RpcHasLoading {
private ctx: WidgetContext,
private settings: RpcStateBehaviourSettings<V>,
private callbacks: RpcStateCallbacks<V>,
private stateValueType: ValueType) {
stateValueType: ValueType) {
super();
this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks);
this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>();
}
initState() {
if (this.ctx.defaultSubscription.rpcEnabled) {
if (this.ctx.defaultSubscription.targetEntityId || this.ctx.defaultSubscription.rpcEnabled) {
this.loadingSubject.next(true);
this.initialStateGetter.initState().subscribe(
{
@ -186,7 +195,7 @@ export class RpcStateBehaviorApi<V> extends RpcHasLoading {
}
);
} else {
this.callbacks.onError('Target device is not set!');
this.callbacks.onError(this.ctx.translate.instant('widgets.rpc-state.error.target-entity-is-not-set'));
}
}
@ -268,7 +277,23 @@ export class RpcDataToStateConverter<V> {
}
}
export abstract class RpcInitialStateGetter<V> {
export abstract class RpcAction {
protected constructor(protected ctx: WidgetContext,
protected settings: RpcActionSettings) {}
handleError(err: any): Error {
const reason = parseError(this.ctx, err);
let errorMessage = this.ctx.translate.instant('widgets.rpc-state.error.failed-to-perform-action',
{actionLabel: this.settings.actionLabel});
if (reason) {
errorMessage += '<br>' + reason;
}
return new Error(errorMessage);
}
}
export abstract class RpcInitialStateGetter<V> extends RpcAction {
static fromSettings<V>(ctx: WidgetContext,
settings: RpcInitialStateSettings<V>,
@ -293,6 +318,7 @@ export abstract class RpcInitialStateGetter<V> {
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) {
this.dataConverter = new RpcDataToStateConverter<V>(settings.dataToState, stateValueType, this.callbacks);
@ -308,6 +334,9 @@ export abstract class RpcInitialStateGetter<V> {
} else {
return data;
}
}),
catchError(err => {
throw this.handleError(err);
})
);
}
@ -355,7 +384,7 @@ export class RpcStateToParamsConverter<V> {
}
}
export abstract class RpcStateUpdater<V> {
export abstract class RpcStateUpdater<V> extends RpcAction {
static fromSettings<V>(ctx: WidgetContext,
settings: RpcUpdateStateSettings): RpcStateUpdater<V> {
@ -374,6 +403,7 @@ export abstract class RpcStateUpdater<V> {
protected constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
this.paramsConverter = new RpcStateToParamsConverter<V>(settings.stateToParams);
}
@ -382,7 +412,11 @@ export abstract class RpcStateUpdater<V> {
if (this.isSimulated) {
return of(null).pipe(delay(500));
} else {
return this.doUpdateState(this.paramsConverter.stateToParams(state));
return this.doUpdateState(this.paramsConverter.stateToParams(state)).pipe(
catchError(err => {
throw this.handleError(err);
})
);
}
}
@ -423,8 +457,8 @@ export class ExecuteRpcStateGetter<V> extends RpcInitialStateGetter<V> {
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError(() => {
throw new Error(this.ctx.defaultSubscription.rpcErrorText);
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
@ -444,6 +478,10 @@ export class RpcAttributeStateGetter<V> extends RpcInitialStateGetter<V> {
protected doGetState(): Observable<V> {
if (this.ctx.defaultSubscription.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.getAttributeSettings.scope);
if (err) {
return throwError(() => err);
}
return this.ctx.attributeService.getEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true})
.pipe(
@ -506,8 +544,8 @@ export class ExecuteRpcStateUpdater<V> extends RpcStateUpdater<V> {
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError(() => {
throw new Error(this.ctx.defaultSubscription.rpcErrorText);
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
@ -525,6 +563,10 @@ export class RpcAttributeStateUpdater<V> extends RpcStateUpdater<V> {
protected doUpdateState(params: any): Observable<any> {
if (this.ctx.defaultSubscription.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.setAttributeSettings.scope);
if (err) {
return throwError(() => err);
}
const attributes: Array<AttributeData> = [{key: this.setAttributeSettings.key, value: params}];
return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.setAttributeSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true});
@ -559,3 +601,26 @@ export class RpcTimeSeriesStateUpdater<V> extends RpcStateUpdater<V> {
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
const handleRpcError = (ctx: WidgetContext, err: any): Error => {
let reason: string;
if (ctx.defaultSubscription.rpcErrorText) {
reason = ctx.defaultSubscription.rpcErrorText;
} else {
reason = parseError(ctx, err);
}
return new Error(reason);
};
const validateAttributeScope = (ctx: WidgetContext, scope?: AttributeScope): Error | null => {
if (ctx.defaultSubscription.targetEntityId.entityType !== EntityType.DEVICE && scope && scope !== AttributeScope.SERVER_SCOPE) {
const scopeStr = ctx.translate.instant(telemetryTypeTranslationsShort.get(scope));
const entityType =
ctx.translate.instant(entityTypeTranslations.get(ctx.defaultSubscription.targetEntityId.entityType).type);
const errorMessage =
ctx.translate.instant('widgets.rpc-state.error.invalid-attribute-scope', {scope: scopeStr, entityType});
return new Error(errorMessage);
} else {
return null;
}
};

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html

@ -35,7 +35,7 @@
<mat-progress-bar class="tb-single-switch-progress" style="height: 2px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
<div *ngIf="error" class="tb-single-switch-error-container">
<div class="tb-single-switch-error-panel">
<div class="tb-single-switch-error-text">{{ error }}</div>
<div class="tb-single-switch-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-single-switch-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss

@ -61,14 +61,14 @@ $switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
align-items: center;
gap: 4px;
border-radius: 4px;
border: 1px solid rgba(209, 39, 48, 0.04);
background: rgba(209, 39, 48, 0.04);
background-color: #fff2f3;
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
.tb-single-switch-error-text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: #D12730;
color: rgba(209, 39, 48, 1);
}
.tb-single-switch-error-clear {
color: rgba(209, 39, 48, 1);

9
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts

@ -38,7 +38,7 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css';
import { hashCode } from '@core/utils';
import { RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models';
import { RpcInitialStateSettings, RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
const horizontalLayoutPadding = 48;
@ -170,8 +170,13 @@ export class SingleSwitchWidgetComponent extends
return {...singleSwitchDefaultSettings};
}
protected initialState(): RpcInitialStateSettings<boolean> {
return {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
}
protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings {
return value ? this.settings.onUpdateState : this.settings.offUpdateState;
const targetSettings = value ? this.settings.onUpdateState : this.settings.offUpdateState;
return {...targetSettings, actionLabel: this.ctx.translate.instant(value ? 'widgets.rpc-state.turn-on' : 'widgets.rpc-state.turn-off')};
}
protected validateValue(value: any): boolean {

4
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -122,7 +122,7 @@ export interface WidgetAction extends IWidgetAction {
}
export interface IDashboardWidget {
updateWidgetParams();
updateWidgetParams(): void;
}
export class WidgetContext {
@ -514,7 +514,7 @@ export interface IDynamicWidgetComponent {
executingRpcRequest: boolean;
rpcEnabled: boolean;
rpcErrorText: string;
rpcRejection: HttpErrorResponse;
rpcRejection: HttpErrorResponse | Error;
raf: RafService;
[key: string]: any;
}

1
ui-ngx/src/app/shared/components/popover.models.ts

@ -80,6 +80,7 @@ export const getPlacementName = (position: ConnectedOverlayPositionChange): Popo
};
export interface PropertyMapping {
// @ts-ignore
[key: string]: [string, () => unknown];
}

8
ui-ngx/src/app/shared/models/rpc-widget-settings.models.ts

@ -65,7 +65,11 @@ export interface RpcDataToStateSettings {
compareToValue?: any;
}
export interface RpcInitialStateSettings<V> {
export interface RpcActionSettings {
actionLabel?: string;
}
export interface RpcInitialStateSettings<V> extends RpcActionSettings {
action: RpcInitialStateAction;
defaultValue: V;
executeRpc: RpcSettings;
@ -102,7 +106,7 @@ export interface RpcStateToParamsSettings {
stateToParamsFunction: string;
}
export interface RpcUpdateStateSettings {
export interface RpcUpdateStateSettings extends RpcActionSettings {
action: RpcUpdateStateAction;
executeRpc: RpcSettings;
setAttribute: RpcSetAttributeSettings;

16
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3723,6 +3723,15 @@
"pkcs-12": "PKCS #12"
}
},
"rpc": {
"error": {
"target-device-is-not-set": "Target device is not set!",
"invalid-target-entity": "RPC commands are not supported by <b>{{entityType}}</b> entity.",
"failed-to-resolve-target-device": "Failed to resolve target device!",
"request-timeout": "Request Timeout.",
"rpc-http-error": "Error: {{status}} - {{statusText}}"
}
},
"rulechain": {
"rulechain": "Rule chain",
"rulechain-events": "Rule chain events",
@ -6038,7 +6047,12 @@
"parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is",
"parameters": "Parameters",
"convert-value-function": "Convert value function"
"convert-value-function": "Convert value function",
"error": {
"target-entity-is-not-set": "Target entity is not set!",
"failed-to-perform-action": "Failed to perform the <b>{{ actionLabel }}</b> action.",
"invalid-attribute-scope": "{{scope}} attribute scope is not supported by <b>{{entityType}}</b> entity."
}
},
"maps": {
"select-entity": "Select entity",

Loading…
Cancel
Save