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", "ace-builds",
"diff-match-patch", "diff-match-patch",
"tv4", "tv4",
"@messageformat/parser" "@messageformat/parser",
"sorted-btree"
] ]
}, },
"configurations": { "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 { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@app/shared/models/entity.models'; import { EntityInfo } from '@app/shared/models/entity.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models'; import { IDashboardComponent } from '@home/models/dashboard-component.models';
import * as moment_ from 'moment'; import moment_ from 'moment';
import { import {
AlarmData, AlarmData,
AlarmDataPageLink, AlarmDataPageLink,
@ -130,12 +130,12 @@ export interface IAliasController {
getFilters(): Filters; getFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo; getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>; getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo): void;
updateUserFilter(filter: Filter); updateUserFilter(filter: Filter): void;
updateEntityAliases(entityAliases: EntityAliases); updateEntityAliases(entityAliases: EntityAliases): void;
updateFilters(filters: Filters); updateFilters(filters: Filters): void;
updateAliases(aliasIds?: Array<string>); updateAliases(aliasIds?: Array<string>): void;
dashboardStateChanged(); dashboardStateChanged(): void;
} }
export interface StateObject { export interface StateObject {
@ -315,7 +315,7 @@ export interface IWidgetSubscription {
rpcEnabled?: boolean; rpcEnabled?: boolean;
executingRpcRequest?: boolean; executingRpcRequest?: boolean;
rpcErrorText?: string; rpcErrorText?: string;
rpcRejection?: HttpErrorResponse; rpcRejection?: HttpErrorResponse | Error;
getFirstEntityInfo(): SubscriptionEntityInfo; getFirstEntityInfo(): SubscriptionEntityInfo;

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

@ -55,7 +55,7 @@ import {
} from '@app/shared/models/time/time.models'; } from '@app/shared/models/time/time.models';
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError, timer } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, Subject, throwError, timer } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service'; 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 { import {
createLabelFromPattern, createLabelFromPattern,
deepClone, deepClone,
@ -67,7 +67,7 @@ import {
parseHttpErrorMessage parseHttpErrorMessage
} from '@core/utils'; } from '@core/utils';
import { EntityId } from '@app/shared/models/id/entity-id'; 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 { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityDataListener } from '@core/api/entity-data.service'; import { EntityDataListener } from '@core/api/entity-data.service';
import { import {
@ -205,8 +205,9 @@ export class WidgetSubscription implements IWidgetSubscription {
executingRpcRequest: boolean; executingRpcRequest: boolean;
rpcEnabled: boolean; rpcEnabled: boolean;
rpcDisabledReason: string;
rpcErrorText: string; rpcErrorText: string;
rpcRejection: HttpErrorResponse; rpcRejection: HttpErrorResponse | Error;
init$: Observable<IWidgetSubscription>; init$: Observable<IWidgetSubscription>;
@ -292,13 +293,15 @@ export class WidgetSubscription implements IWidgetSubscription {
this.subscriptionTimewindow = null; this.subscriptionTimewindow = null;
this.loadingData = false; this.loadingData = false;
this.displayLegend = false; this.displayLegend = false;
this.initAlarmSubscription().subscribe(() => { this.initAlarmSubscription().subscribe({
subscriptionSubject.next(this); next:() => {
subscriptionSubject.complete(); subscriptionSubject.next(this);
}, subscriptionSubject.complete();
() => { },
subscriptionSubject.error(null); error: () => {
}); subscriptionSubject.error(null);
}}
);
} else { } else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {}); this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onLatestDataUpdated = this.callbacks.onLatestDataUpdated || (() => {}); this.callbacks.onLatestDataUpdated = this.callbacks.onLatestDataUpdated || (() => {});
@ -373,13 +376,15 @@ export class WidgetSubscription implements IWidgetSubscription {
this.legendConfig.showAvg === true || this.legendConfig.showAvg === true ||
this.legendConfig.showTotal === true || this.legendConfig.showTotal === true ||
this.legendConfig.showLatest === true); this.legendConfig.showLatest === true);
this.initDataSubscription().subscribe(() => { this.initDataSubscription().subscribe({
next:() => {
subscriptionSubject.next(this); subscriptionSubject.next(this);
subscriptionSubject.complete(); subscriptionSubject.complete();
}, },
(err) => { error: () => {
subscriptionSubject.error(err); subscriptionSubject.error(null);
}); }}
);
} }
} }
@ -401,6 +406,16 @@ export class WidgetSubscription implements IWidgetSubscription {
this.rpcEnabled = true; this.rpcEnabled = true;
} else { } else {
this.rpcEnabled = this.ctx.utils.widgetEditMode; 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.hasResolvedData = true;
this.callbacks.rpcStateChanged(this); this.callbacks.rpcStateChanged(this);
@ -409,6 +424,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}, },
error: () => { error: () => {
this.rpcEnabled = false; this.rpcEnabled = false;
this.rpcDisabledReason = this.ctx.translate.instant('rpc.error.failed-to-resolve-target-device');
this.callbacks.rpcStateChanged(this); this.callbacks.rpcStateChanged(this);
initRpcSubject.next(); initRpcSubject.next();
initRpcSubject.complete(); initRpcSubject.complete();
@ -427,17 +443,19 @@ export class WidgetSubscription implements IWidgetSubscription {
initAlarmSubscriptionSubject.complete(); initAlarmSubscriptionSubject.complete();
} else { } else {
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe( this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe(
(alarmSource) => { {
this.alarmSource = alarmSource; next: (alarmSource) => {
if (alarmSource) { this.alarmSource = alarmSource;
this.hasResolvedData = true; 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 { } else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe( this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe(
(datasources) => { {
this.configuredDatasources = datasources; next: (datasources) => {
this.prepareDataSubscriptions().subscribe( this.configuredDatasources = datasources;
() => { this.prepareDataSubscriptions().subscribe(
initDataSubscriptionSubject.next(); () => {
initDataSubscriptionSubject.complete(); initDataSubscriptionSubject.next();
} initDataSubscriptionSubject.complete();
); }
}, );
(err) => { },
this.notifyDataLoaded(); error: (err) => {
initDataSubscriptionSubject.error(err); this.notifyDataLoaded();
initDataSubscriptionSubject.error(err);
}
} }
); );
} }
@ -801,9 +821,11 @@ export class WidgetSubscription implements IWidgetSubscription {
persistent?: boolean, persistentPollingInterval?: number, retries?: number, persistent?: boolean, persistentPollingInterval?: number, retries?: number,
additionalInfo?: any, requestUUID?: string): Observable<any> { additionalInfo?: any, requestUUID?: string): Observable<any> {
if (!this.rpcEnabled) { if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!')); this.rpcErrorText = this.rpcDisabledReason;
this.rpcRejection = new Error(this.rpcErrorText);
return throwError(() => this.rpcRejection);
} else { } 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.rpcRejection = null;
this.rpcErrorText = null; this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this); this.callbacks.onRpcErrorCleared(this);
@ -848,9 +870,9 @@ export class WidgetSubscription implements IWidgetSubscription {
persistentRespons.status !== RpcStatus.DELIVERED && persistentRespons.status !== RpcStatus.QUEUED), persistentRespons.status !== RpcStatus.DELIVERED && persistentRespons.status !== RpcStatus.QUEUED),
switchMap(persistentResponse => { switchMap(persistentResponse => {
if ([RpcStatus.TIMEOUT, RpcStatus.EXPIRED].includes(persistentResponse.status)) { if ([RpcStatus.TIMEOUT, RpcStatus.EXPIRED].includes(persistentResponse.status)) {
return throwError({status: 504}); return throwError(() => ({status: 504}));
} else if (persistentResponse.status === RpcStatus.FAILED) { } else if (persistentResponse.status === RpcStatus.FAILED) {
return throwError({status: 502, statusText: persistentResponse.response.error}); return throwError(() => ({status: 502, statusText: persistentResponse.response.error}));
} else { } else {
return of(persistentResponse.response); return of(persistentResponse.response);
} }
@ -861,40 +883,43 @@ export class WidgetSubscription implements IWidgetSubscription {
return of(response); return of(response);
}) })
) )
.subscribe((responseBody) => { .subscribe({
this.rpcRejection = null; next: (responseBody) => {
this.rpcErrorText = null; this.rpcRejection = null;
const index = this.executingSubjects.indexOf(rpcSubject); this.rpcErrorText = null;
if (index >= 0) { const index = this.executingSubjects.indexOf(rpcSubject);
this.executingSubjects.splice( index, 1 ); if (index >= 0) {
} this.executingSubjects.splice( index, 1 );
this.executingRpcRequest = this.executingSubjects.length > 0; }
this.callbacks.onRpcSuccess(this); this.executingRpcRequest = this.executingSubjects.length > 0;
rpcSubject.next(responseBody); this.callbacks.onRpcSuccess(this);
rpcSubject.complete(); rpcSubject.next(responseBody);
}, rpcSubject.complete();
(rejection: HttpErrorResponse) => { },
const index = this.executingSubjects.indexOf(rpcSubject); error: (rejection: HttpErrorResponse) => {
if (index >= 0) { const index = this.executingSubjects.indexOf(rpcSubject);
this.executingSubjects.splice( index, 1 ); if (index >= 0) {
} this.executingSubjects.splice( index, 1 );
this.executingRpcRequest = this.executingSubjects.length > 0; }
this.callbacks.rpcStateChanged(this); this.executingRpcRequest = this.executingSubjects.length > 0;
if (!this.executingRpcRequest || rejection.status === 504) { this.callbacks.rpcStateChanged(this);
this.rpcRejection = rejection; if (!this.executingRpcRequest || rejection.status === 504) {
if (rejection.status === 504) { this.rpcRejection = rejection;
this.rpcErrorText = 'Request Timeout.'; if (rejection.status === 504) {
} else { this.rpcErrorText = this.ctx.translate.instant('rpc.error.request-timeout');
this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; } else {
const error = parseHttpErrorMessage(rejection, this.ctx.translate); this.rpcErrorText = this.ctx.translate.instant('rpc.error.rpc-http-error',
if (error) { {status: rejection.status, statusText: rejection.statusText});
this.rpcErrorText += '</br>'; const error = parseHttpErrorMessage(rejection, this.ctx.translate);
this.rpcErrorText += error.message; 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(); return rpcSubject.asObservable();
@ -937,7 +962,7 @@ export class WidgetSubscription implements IWidgetSubscription {
subscribeAllForPaginatedData(pageLink: EntityDataPageLink, subscribeAllForPaginatedData(pageLink: EntityDataPageLink,
keyFilters: KeyFilter[]): Observable<any> { keyFilters: KeyFilter[]): Observable<any> {
const observables: Observable<any>[] = []; const observables: Observable<any>[] = [];
this.configuredDatasources.forEach((datasource, datasourceIndex) => { this.configuredDatasources.forEach((_datasource, datasourceIndex) => {
observables.push(this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters)); observables.push(this.subscribeForPaginatedData(datasourceIndex, pageLink, keyFilters));
}); });
if (observables.length) { if (observables.length) {
@ -1118,16 +1143,18 @@ export class WidgetSubscription implements IWidgetSubscription {
this.updateAlarmDataSubscription(); this.updateAlarmDataSubscription();
} else { } else {
this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe( this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe(
(alarmSource) => { {
this.alarmSource = alarmSource; next: (alarmSource) => {
if (alarmSource) { this.alarmSource = alarmSource;
this.hasResolvedData = true; 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 { } else {
this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe( this.ctx.aliasController.resolveDatasources(this.configuredDatasources, this.singleEntity, this.pageSize).subscribe(
(datasources) => { {
this.configuredDatasources = datasources; next: (datasources) => {
this.prepareDataSubscriptions().subscribe( this.configuredDatasources = datasources;
() => { this.prepareDataSubscriptions().subscribe(
this.updatePaginatedDataSubscriptions(); () => {
} this.updatePaginatedDataSubscriptions();
); }
}, );
() => { },
this.notifyDataLoaded(); error: () => {
this.notifyDataLoaded();
}
} }
); );
} }
@ -1318,7 +1347,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataLoaded(pageData: PageData<EntityData>, private dataLoaded(pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>, data: Array<Array<DataSetHolder>>,
datasourceIndex: number, pageLink: EntityDataPageLink, isUpdate: boolean) { datasourceIndex: number, _pageLink: EntityDataPageLink, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex]; const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true; datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) => const datasources = pageData.data.map((entityData, index) =>
@ -1403,7 +1432,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}); });
if (datasource.latestDataKeys && datasource.latestDataKeys.length) { if (datasource.latestDataKeys && datasource.latestDataKeys.length) {
this.hasLatestData = true; this.hasLatestData = true;
datasource.latestDataKeys.forEach((dataKey, currentLatestDataKeyIndex) => { datasource.latestDataKeys.forEach((_dataKey, currentLatestDataKeyIndex) => {
const currentDataKeyIndex = datasource.dataKeys.length + currentLatestDataKeyIndex; const currentDataKeyIndex = datasource.dataKeys.length + currentLatestDataKeyIndex;
const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex]; const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex];
this.latestData.push(datasourceData); this.latestData.push(datasourceData);
@ -1608,7 +1637,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated(); this.onDataUpdated();
} }
private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) { private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
this.alarmsLoaded(alarms, 0, 0); this.alarmsLoaded(alarms, 0, 0);
} }
@ -1639,15 +1668,17 @@ export class WidgetSubscription implements IWidgetSubscription {
const loadSubject = new ReplaySubject<void>(1); const loadSubject = new ReplaySubject<void>(1);
if (this.ctx.getServerTimeDiff && this.timeWindow) { if (this.ctx.getServerTimeDiff && this.timeWindow) {
this.ctx.getServerTimeDiff().subscribe( this.ctx.getServerTimeDiff().subscribe(
(stDiff) => { {
this.timeWindow.stDiff = stDiff; next: (stDiff) => {
loadSubject.next(); this.timeWindow.stDiff = stDiff;
loadSubject.complete(); loadSubject.next();
}, loadSubject.complete();
() => { },
this.timeWindow.stDiff = 0; error: () => {
loadSubject.next(); this.timeWindow.stDiff = 0;
loadSubject.complete(); loadSubject.next();
loadSubject.complete();
}
} }
); );
} else { } 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" *ngIf="targetDeviceFormGroup.get('type').value === targetDeviceType.entity"
[tbRequired]="!widgetEditMode" [tbRequired]="!widgetEditMode"
[aliasController]="aliasController" [aliasController]="aliasController"
[allowedEntityTypes]="[entityType.DEVICE]"
[callbacks]="entityAliasSelectCallbacks" [callbacks]="entityAliasSelectCallbacks"
formControlName="entityAliasId"> formControlName="entityAliasId">
</tb-entity-alias-select> </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; executingRpcRequest: boolean;
rpcEnabled: boolean; rpcEnabled: boolean;
rpcErrorText: string; rpcErrorText: string;
rpcRejection: HttpErrorResponse; rpcRejection: HttpErrorResponse | Error;
[key: string]: any; [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. /// 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 { 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 { catchError, delay, map, share } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnInit, TemplateRef } from '@angular/core'; 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 { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { import {
RpcActionSettings,
RpcGetAttributeSettings, RpcGetAttributeSettings,
RpcSetAttributeSettings, RpcSetAttributeSettings,
RpcSettings, RpcSettings,
@ -42,6 +48,7 @@ import {
RpcUpdateStateSettings RpcUpdateStateSettings
} from '@app/shared/models/rpc-widget-settings.models'; } from '@app/shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // 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); this.overlayStyle = overlayStyle(this.settings.background.overlay);
const behaviourSettings: RpcStateBehaviourSettings<V> = { const behaviourSettings: RpcStateBehaviourSettings<V> = {
initialState: this.settings.initialState, initialState: this.initialState(),
updateStateByValue: val => this.getUpdateStateSettingsForValue(val) updateStateByValue: val => this.getUpdateStateSettingsForValue(val)
}; };
@ -117,6 +124,8 @@ export abstract class BasicRpcStateWidgetComponent<V, S extends RpcStateWidgetSe
protected abstract defaultSettings(): S; protected abstract defaultSettings(): S;
protected abstract initialState(): RpcInitialStateSettings<V>;
protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings; protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings;
protected stateValueType(): ValueType { protected stateValueType(): ValueType {
@ -161,14 +170,14 @@ export class RpcStateBehaviorApi<V> extends RpcHasLoading {
private ctx: WidgetContext, private ctx: WidgetContext,
private settings: RpcStateBehaviourSettings<V>, private settings: RpcStateBehaviourSettings<V>,
private callbacks: RpcStateCallbacks<V>, private callbacks: RpcStateCallbacks<V>,
private stateValueType: ValueType) { stateValueType: ValueType) {
super(); super();
this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks); this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks);
this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>(); this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>();
} }
initState() { initState() {
if (this.ctx.defaultSubscription.rpcEnabled) { if (this.ctx.defaultSubscription.targetEntityId || this.ctx.defaultSubscription.rpcEnabled) {
this.loadingSubject.next(true); this.loadingSubject.next(true);
this.initialStateGetter.initState().subscribe( this.initialStateGetter.initState().subscribe(
{ {
@ -186,7 +195,7 @@ export class RpcStateBehaviorApi<V> extends RpcHasLoading {
} }
); );
} else { } 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, static fromSettings<V>(ctx: WidgetContext,
settings: RpcInitialStateSettings<V>, settings: RpcInitialStateSettings<V>,
@ -293,6 +318,7 @@ export abstract class RpcInitialStateGetter<V> {
protected settings: RpcInitialStateSettings<V>, protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType, protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) { protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) { if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) {
this.dataConverter = new RpcDataToStateConverter<V>(settings.dataToState, stateValueType, this.callbacks); this.dataConverter = new RpcDataToStateConverter<V>(settings.dataToState, stateValueType, this.callbacks);
@ -308,6 +334,9 @@ export abstract class RpcInitialStateGetter<V> {
} else { } else {
return data; 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, static fromSettings<V>(ctx: WidgetContext,
settings: RpcUpdateStateSettings): RpcStateUpdater<V> { settings: RpcUpdateStateSettings): RpcStateUpdater<V> {
@ -374,6 +403,7 @@ export abstract class RpcStateUpdater<V> {
protected constructor(protected ctx: WidgetContext, protected constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) { protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode; this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
this.paramsConverter = new RpcStateToParamsConverter<V>(settings.stateToParams); this.paramsConverter = new RpcStateToParamsConverter<V>(settings.stateToParams);
} }
@ -382,7 +412,11 @@ export abstract class RpcStateUpdater<V> {
if (this.isSimulated) { if (this.isSimulated) {
return of(null).pipe(delay(500)); return of(null).pipe(delay(500));
} else { } 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.requestTimeout,
this.executeRpcSettings.requestPersistent, this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe( this.executeRpcSettings.persistentPollingInterval).pipe(
catchError(() => { catchError((err) => {
throw new Error(this.ctx.defaultSubscription.rpcErrorText); throw handleRpcError(this.ctx, err);
}) })
); );
} }
@ -444,6 +478,10 @@ export class RpcAttributeStateGetter<V> extends RpcInitialStateGetter<V> {
protected doGetState(): Observable<V> { protected doGetState(): Observable<V> {
if (this.ctx.defaultSubscription.targetEntityId) { 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, return this.ctx.attributeService.getEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true}) this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true})
.pipe( .pipe(
@ -506,8 +544,8 @@ export class ExecuteRpcStateUpdater<V> extends RpcStateUpdater<V> {
this.executeRpcSettings.requestTimeout, this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent, this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe( this.executeRpcSettings.persistentPollingInterval).pipe(
catchError(() => { catchError((err) => {
throw new Error(this.ctx.defaultSubscription.rpcErrorText); throw handleRpcError(this.ctx, err);
}) })
); );
} }
@ -525,6 +563,10 @@ export class RpcAttributeStateUpdater<V> extends RpcStateUpdater<V> {
protected doUpdateState(params: any): Observable<any> { protected doUpdateState(params: any): Observable<any> {
if (this.ctx.defaultSubscription.targetEntityId) { 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}]; const attributes: Array<AttributeData> = [{key: this.setAttributeSettings.key, value: params}];
return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId, return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.setAttributeSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true}); 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 => const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error'; 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> <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 *ngIf="error" class="tb-single-switch-error-container">
<div class="tb-single-switch-error-panel"> <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> <button class="tb-single-switch-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div> </div>
</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; align-items: center;
gap: 4px; gap: 4px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(209, 39, 48, 0.04); background-color: #fff2f3;
background: rgba(209, 39, 48, 0.04); box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
.tb-single-switch-error-text { .tb-single-switch-error-text {
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 16px; line-height: 16px;
color: #D12730; color: rgba(209, 39, 48, 1);
} }
.tb-single-switch-error-clear { .tb-single-switch-error-clear {
color: rgba(209, 39, 48, 1); 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 { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { hashCode } from '@core/utils'; 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'; import { ValueType } from '@shared/models/constants';
const horizontalLayoutPadding = 48; const horizontalLayoutPadding = 48;
@ -170,8 +170,13 @@ export class SingleSwitchWidgetComponent extends
return {...singleSwitchDefaultSettings}; 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 { 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 { 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 { export interface IDashboardWidget {
updateWidgetParams(); updateWidgetParams(): void;
} }
export class WidgetContext { export class WidgetContext {
@ -514,7 +514,7 @@ export interface IDynamicWidgetComponent {
executingRpcRequest: boolean; executingRpcRequest: boolean;
rpcEnabled: boolean; rpcEnabled: boolean;
rpcErrorText: string; rpcErrorText: string;
rpcRejection: HttpErrorResponse; rpcRejection: HttpErrorResponse | Error;
raf: RafService; raf: RafService;
[key: string]: any; [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 { export interface PropertyMapping {
// @ts-ignore
[key: string]: [string, () => unknown]; [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; compareToValue?: any;
} }
export interface RpcInitialStateSettings<V> { export interface RpcActionSettings {
actionLabel?: string;
}
export interface RpcInitialStateSettings<V> extends RpcActionSettings {
action: RpcInitialStateAction; action: RpcInitialStateAction;
defaultValue: V; defaultValue: V;
executeRpc: RpcSettings; executeRpc: RpcSettings;
@ -102,7 +106,7 @@ export interface RpcStateToParamsSettings {
stateToParamsFunction: string; stateToParamsFunction: string;
} }
export interface RpcUpdateStateSettings { export interface RpcUpdateStateSettings extends RpcActionSettings {
action: RpcUpdateStateAction; action: RpcUpdateStateAction;
executeRpc: RpcSettings; executeRpc: RpcSettings;
setAttribute: RpcSetAttributeSettings; setAttribute: RpcSetAttributeSettings;

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

@ -3723,6 +3723,15 @@
"pkcs-12": "PKCS #12" "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": {
"rulechain": "Rule chain", "rulechain": "Rule chain",
"rulechain-events": "Rule chain events", "rulechain-events": "Rule chain events",
@ -6038,7 +6047,12 @@
"parse-value-function": "Parse value function", "parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is", "on-when-result-is": "'On' when result is",
"parameters": "Parameters", "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": { "maps": {
"select-entity": "Select entity", "select-entity": "Select entity",

Loading…
Cancel
Save