Browse Source

Merge pull request #5629 from ArtemDzhereleiko/control-widget/rpc-persistent-table

[3.3.3] UI: New control widget Persistent table
pull/5872/head
Igor Kulikov 5 years ago
committed by GitHub
parent
commit
6306a27909
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 13
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  3. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java
  4. 10
      dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java
  5. 4
      dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
  6. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
  7. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
  8. 11
      ui-ngx/src/app/core/api/widget-api.models.ts
  9. 13
      ui-ngx/src/app/core/api/widget-subscription.ts
  10. 15
      ui-ngx/src/app/core/http/device.service.ts
  11. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  12. 92
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html
  13. 31
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss
  14. 75
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts
  15. 118
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html
  16. 34
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss
  17. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts
  18. 44
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html
  19. 37
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss
  20. 67
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts
  21. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  22. 53
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss
  23. 580
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  24. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts
  25. 10
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  26. 22
      ui-ngx/src/app/shared/components/json-object-view.component.html
  27. 27
      ui-ngx/src/app/shared/components/json-object-view.component.scss
  28. 165
      ui-ngx/src/app/shared/components/json-object-view.component.ts
  29. 1
      ui-ngx/src/app/shared/models/constants.ts
  30. 50
      ui-ngx/src/app/shared/models/rpc.models.ts
  31. 5
      ui-ngx/src/app/shared/shared.module.ts
  32. 42
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  33. 41
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  34. 41
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json

18
application/src/main/data/json/system/widget_bundles/control_widgets.json

File diff suppressed because one or more lines are too long

13
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -178,8 +178,8 @@ public class RpcV2Controller extends AbstractRpcController {
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES)
@RequestParam RpcStatus rpcStatus,
@ApiParam(value = "Status of the RPC", allowableValues = RPC_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) RpcStatus rpcStatus,
@ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -188,7 +188,7 @@ public class RpcV2Controller extends AbstractRpcController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("DeviceId", strDeviceId);
try {
if (rpcStatus.equals(RpcStatus.DELETED)) {
if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED");
}
@ -200,7 +200,12 @@ public class RpcV2Controller extends AbstractRpcController {
accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
PageData<Rpc> rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
PageData<Rpc> rpcCalls;
if (rpcStatus != null) {
rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
} else {
rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink);
}
response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK));
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java

@ -35,5 +35,7 @@ public interface RpcService {
ListenableFuture<Rpc> findRpcByIdAsync(TenantId tenantId, RpcId id);
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
}

10
dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java

@ -82,7 +82,15 @@ public class BaseRpcService implements RpcService {
log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink);
return rpcDao.findAllByDeviceId(tenantId, deviceId, rpcStatus, pageLink);
return rpcDao.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
}
@Override
public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) {
log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], pageLink [{}]", tenantId, deviceId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink);
return rpcDao.findAllByDeviceId(tenantId, deviceId, pageLink);
}
private PaginatedRemover<TenantId, Rpc> tenantRpcRemover =

4
dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java

@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.Dao;
public interface RpcDao extends Dao<Rpc> {
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
PageData<Rpc> findAllRpcByTenantId(TenantId tenantId, PageLink pageLink);

7
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java

@ -50,7 +50,12 @@ public class JpaRpcDao extends JpaAbstractDao<RpcEntity, Rpc> implements RpcDao
}
@Override
public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) {
public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) {
return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceId(tenantId.getId(), deviceId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) {
return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceIdAndStatus(tenantId.getId(), deviceId.getId(), rpcStatus, DaoUtil.toPageable(pageLink)));
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java

@ -26,6 +26,8 @@ import org.thingsboard.server.dao.model.sql.RpcEntity;
import java.util.UUID;
public interface RpcRepository extends CrudRepository<RpcEntity, UUID> {
Page<RpcEntity> findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable);
Page<RpcEntity> findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable);
Page<RpcEntity> findAllByTenantId(UUID tenantId, Pageable pageable);

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

@ -55,6 +55,7 @@ import { TranslateService } from '@ngx-translate/core';
import { AlarmDataService } from '@core/api/alarm-data.service';
import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models';
import { PopoverPlacement } from '@shared/components/popover.models';
import { PersistentRpc } from '@shared/models/rpc.models';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -71,9 +72,9 @@ export interface WidgetSubscriptionApi {
export interface RpcApi {
sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string) => Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable<any>;
sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string) => Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable<any>;
completedCommand: () => void;
}
@ -288,6 +289,8 @@ export interface IWidgetSubscription {
comparisonEnabled?: boolean;
comparisonTimeWindow?: WidgetTimewindow;
persistentRequests?: PageData<PersistentRpc>;
alarms?: PageData<AlarmData>;
alarmSource?: Datasource;
@ -314,9 +317,9 @@ export interface IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void;
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any>;
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any>;
clearRpcError(): void;
subscribe(): void;

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

@ -656,13 +656,13 @@ export class WidgetSubscription implements IWidgetSubscription {
}
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, requestUUID);
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
}
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, requestUUID);
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
}
clearRpcError(): void {
@ -679,7 +679,8 @@ export class WidgetSubscription implements IWidgetSubscription {
}
sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number,
persistent?: boolean, persistentPollingInterval?: number, requestUUID?: string): Observable<any> {
persistent?: boolean, persistentPollingInterval?: number, retries?: number,
additionalInfo?: any, requestUUID?: string): Observable<any> {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else {
@ -692,6 +693,8 @@ export class WidgetSubscription implements IWidgetSubscription {
method,
params,
persistent,
retries,
additionalInfo,
requestUUID
};
if (timeout && timeout > 0) {

15
ui-ngx/src/app/core/http/device.service.ts

@ -31,7 +31,7 @@ import {
import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
import { PersistentRpc } from '@shared/models/rpc.models';
import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models';
@Injectable({
providedIn: 'root'
@ -143,6 +143,19 @@ export class DeviceService {
return this.http.get<PersistentRpc>(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config));
}
public deletePersistedRpc(rpcId: string, config?: RequestConfig) {
return this.http.delete<PersistentRpc>(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config));
}
public getPersistedRpcRequests(deviceId: string, pageLink: PageLink,
rpcStatus?: RpcStatus, config?: RequestConfig): Observable<PageData<PersistentRpc>> {
let url = `/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}`;
if (rpcStatus && rpcStatus.length) {
url += `&rpcStatus=${rpcStatus}`;
}
return this.http.get<PageData<PersistentRpc>>(url, defaultHttpOptionsFromConfig(config));
}
public findByQuery(query: DeviceSearchQuery,
config?: RequestConfig): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config));

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -139,6 +139,7 @@ import * as QueueTypeListComponent from '@shared/components/queue/queue-type-lis
import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component';
import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component';
import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component';
import * as JsonObjectViewComponent from '@shared/components/json-object-view.component';
import * as JsonContentComponent from '@shared/components/json-content.component';
import * as JsFuncComponent from '@shared/components/js-func.component';
import * as FabToolbarComponent from '@shared/components/fab-toolbar.component';
@ -420,6 +421,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent,
'@shared/components/socialshare-panel.component': SocialSharePanelComponent,
'@shared/components/json-object-edit.component': JsonObjectEditComponent,
'@shared/components/json-object-view.component': JsonObjectViewComponent,
'@shared/components/json-content.component': JsonContentComponent,
'@shared/components/js-func.component': JsFuncComponent,
'@shared/components/fab-toolbar.component': FabToolbarComponent,

92
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html

@ -0,0 +1,92 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="persistentFormGroup" (ngSubmit)="save()" style="min-width: 480px; max-width: 600px;">
<mat-toolbar color="primary">
<h2>{{ 'widgets.persistent-table.add-title' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content class="add-dialog">
<fieldset [disabled]="isLoading$ | async">
<div fxLayout="row" fxLayoutGap="6px">
<mat-slide-toggle fxFlex class="mat-block" formControlName="oneWayElseTwoWay">
{{ rpcMessageTypeText }}
</mat-slide-toggle>
</div>
<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.method</mat-label>
<input matInput formControlName="method" required>
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('required')">
{{'widgets.persistent-table.method-error' | translate}}
</mat-error>
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('pattern')">
{{'widgets.persistent-table.white-space-error' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.retries</mat-label>
<input matInput type="number" formControlName="retries">
</mat-form-field>
</div>
<div class="params-json-editor">
<tb-json-object-edit formControlName="params"
[editorStyle]="{minHeight: '130px'}"
label="{{ 'widgets.persistent-table.params' | translate }}">
</tb-json-object-edit>
</div>
<mat-expansion-panel class="additional-json-editor">
<mat-expansion-panel-header>
<mat-panel-title translate>
widgets.persistent-table.additional-info
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit formControlName="additionalInfo"
[editorStyle]="{minHeight: '130px'}">
</tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-actions fxLayout="row">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.close' | translate }}
</button>
<span fxFlex></span>
<div fxLayout="row" fxLayoutGap="8px">
<button mat-raised-button
color="primary"
type="submit"
[disabled]="(isLoading$ | async) || persistentFormGroup.invalid || !persistentFormGroup.dirty">
{{ 'widgets.persistent-table.send-request' | translate }}
</button>
</div>
</div>
</form>

31
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.add-dialog {
.params-json-editor,
.additional-json-editor {
.tb-json-object-panel {
margin: 0 0 16px;
}
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
}
}
}

75
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts

@ -0,0 +1,75 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RequestData } from '@shared/models/rpc.models';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-persistent-add-dialog',
templateUrl: './persistent-add-dialog.component.html',
styleUrls: ['./persistent-add-dialog.component.scss']
})
export class PersistentAddDialogComponent extends DialogComponent<PersistentAddDialogComponent, RequestData> implements OnInit {
public persistentFormGroup: FormGroup;
public rpcMessageTypeText: string;
private requestData: RequestData = null;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<PersistentAddDialogComponent, RequestData>,
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.persistentFormGroup = this.fb.group(
{
method: ['', [Validators.required, Validators.pattern(/^\S+$/)]],
oneWayElseTwoWay: [false],
retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]],
params: [null],
additionalInfo: [null]
}
);
}
save() {
this.requestData = this.persistentFormGroup.value;
this.close();
}
ngOnInit(): void {
this.rpcMessageTypeText = this.translate.instant('widgets.persistent-table.message-types.false');
this.persistentFormGroup.get('oneWayElseTwoWay').valueChanges.subscribe(
() => {
this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`);
}
);
}
close(): void {
this.dialogRef.close(this.requestData);
}
}

118
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html

@ -0,0 +1,118 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="persistentFormGroup" style="min-width: 480px;">
<mat-toolbar color="primary">
<h2>{{ persistentFormGroup.get('rpcId').value }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.created-time</mat-label>
<input matInput formControlName="createdTime" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.expiration-time</mat-label>
<input matInput formControlName="expirationTime" readonly>
</mat-form-field>
</div>
<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.message-type</mat-label>
<input matInput formControlName="messageType" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.status</mat-label>
<input matInput formControlName="status" readonly
[ngStyle]="{ fontWeight: 'bold', color: rpcStatusColorsMap.get(data.persistentRequest.status) }">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.method</mat-label>
<input matInput formControlName="method" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block"
*ngIf="persistentFormGroup.get('retries').value">
<mat-label translate>widgets.persistent-table.retries</mat-label>
<input matInput formControlName="retries" readonly>
</mat-form-field>
</div>
<mat-accordion class="rpc-dialog" multi>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.response' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<tb-json-object-view formControlName="response" autoHeight></tb-json-object-view>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.params' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-view formControlName="params" autoHeight></tb-json-object-view>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.additional-info' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-view
formControlName="additionalInfo"
autoHeight>
</tb-json-object-view>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-raised-button
*ngIf="allowDelete"
color="primary"
type="button"
(click)="deleteRpcRequest()"
[disabled]="(isLoading$ | async)">
{{ 'widgets.persistent-table.delete' | translate }}
</button>
<span fxFlex></span>
<div fxLayout="row" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</div>
</form>

34
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.rpc-dialog {
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
.tb-json-object-panel {
margin: 0 0 16px 0;
}
}
.tb-audit-log-response-data {
width: 100%;
min-width: 400px;
height: 100%;
min-height: 100px;
border: 1px solid #c0c0c0;
}
}

126
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts

@ -0,0 +1,126 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DeviceService } from '@core/http/device.service';
import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DialogService } from '@core/services/dialog.service';
export interface PersistentDetailsDialogData {
persistentRequest: PersistentRpc;
allowDelete: boolean;
}
@Component({
selector: 'tb-persistent-details-dialog',
templateUrl: './persistent-details-dialog.component.html',
styleUrls: ['./persistent-details-dialog.component.scss']
})
export class PersistentDetailsDialogComponent extends DialogComponent<PersistentDetailsDialogComponent, boolean> implements OnInit {
@ViewChild('responseDataEditor', {static: true})
responseDataEditorElmRef: ElementRef;
public persistentFormGroup: FormGroup;
public rpcStatusColorsMap = rpcStatusColors;
public rpcStatus = RpcStatus;
public allowDelete: boolean;
private persistentUpdated = false;
private responseData: string;
constructor(protected store: Store<AppState>,
protected router: Router,
private datePipe: DatePipe,
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData,
public dialogRef: MatDialogRef<PersistentDetailsDialogComponent, boolean>,
private dialogService: DialogService,
private deviceService: DeviceService,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.allowDelete = data.allowDelete;
this.persistentFormGroup = this.fb.group(
{
rpcId: [''],
createdTime: [''],
expirationTime: [''],
messageType: [''],
status: [''],
method: [''],
params: [''],
retries: [''],
response: [''],
additionalInfo: ['']
}
);
this.loadPersistentFields(data.persistentRequest);
this.responseData = JSON.stringify(data.persistentRequest.response, null, 2);
}
loadPersistentFields(request: PersistentRpc) {
this.persistentFormGroup.patchValue({
rpcId: this.translate.instant('widgets.persistent-table.details-title') + request.id.id,
createdTime: this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss'),
expirationTime: this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss'),
messageType: this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway),
status: this.translate.instant(rpcStatusTranslation.get(request.status)),
method: request.request.body.method,
retries: request.request.retries || null,
response: request.response || null,
params: JSON.parse(request.request.body.params) || null,
additionalInfo: request.additionalInfo || null
}, {emitEvent: false});
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close(this.persistentUpdated);
}
deleteRpcRequest() {
const persistentRpc = this.data.persistentRequest;
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('widgets.persistent-table.delete-request-title'),
this.translate.instant('widgets.persistent-table.delete-request-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => {
this.persistentUpdated = true;
this.close();
});
}
});
}
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="persistentFilterFormGroup" (ngSubmit)="update()">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>widgets.persistent-table.rpc-status-list</mat-label>
<mat-select formControlName="rpcStatus"
placeholder="{{ rpcSearchPlaceholder }}">
<mat-option [value]="null">
{{ 'widgets.persistent-table.rpc-search-status-all' | translate }}
</mat-option>
<mat-option *ngFor="let searchStatus of persistentSearchStatuses" [value]="searchStatus">
{{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="persistentFilterFormGroup.invalid || !persistentFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

37
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
min-width: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
}
}

67
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { OverlayRef } from '@angular/cdk/overlay';
import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models';
import { TranslateService } from '@ngx-translate/core';
export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmFilterPanelData');
export interface PersistentFilterPanelData {
rpcStatus: RpcStatus;
}
@Component({
selector: 'tb-persistent-filter-panel',
templateUrl: './persistent-filter-panel.component.html',
styleUrls: ['./persistent-filter-panel.component.scss']
})
export class PersistentFilterPanelComponent {
public persistentFilterFormGroup: FormGroup;
public result: PersistentFilterPanelData;
public rpcSearchStatusTranslationMap = rpcStatusTranslation;
public rpcSearchPlaceholder: string;
public persistentSearchStatuses = Object.keys(RpcStatus);
constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA)
public data: PersistentFilterPanelData,
public overlayRef: OverlayRef,
private fb: FormBuilder,
private translate: TranslateService) {
this.persistentFilterFormGroup = this.fb.group(
{
rpcStatus: this.data.rpcStatus
}
);
this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status');
}
update() {
this.result = {
rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value
};
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
}

126
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html

@ -0,0 +1,126 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-table-widget tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-absolute-fill">
<div fxFlex class="table-container">
<table mat-table [dataSource]="persistentDatasource"
matSort [matSortActive]="pageLink.sortOrder.property"
[matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="rpcId">
<mat-header-cell *matHeaderCellDef class="column-id">
{{ 'widgets.persistent-table.rpc-id' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.id.id }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="createdTime">
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header>
{{ 'widgets.persistent-table.created-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="expirationTime">
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header>
{{ 'widgets.persistent-table.expiration-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'widgets.persistent-table.status' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column"
[ngStyle]="{fontWeight: 'bold', color: rpcStatusColor.get((column.status))}">
{{ rpcStatusTranslation.get(column.status) | translate }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="messageType">
<mat-header-cell *matHeaderCellDef>
{{ 'widgets.persistent-table.message-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="method">
<mat-header-cell *matHeaderCellDef>
{{ 'widgets.persistent-table.method' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.request.body.method }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
<mat-header-cell *matHeaderCellDef>
</mat-header-cell>
<mat-cell *matCellDef="let column">
<div fxHide fxShow.gt-md fxLayout="row" fxLayoutAlign="end">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{ actionDescriptor.icon }}</mat-icon>
</button>
</ng-container>
</div>
<div fxHide fxShow.lt-lg *ngIf="actionCellButtonAction.length">
<button mat-button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="(isLoading$ | async)"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row *matRowDef="let column; columns: displayedColumns"
[fxShow]="!persistentDatasource.dataLoading"></mat-row>
</table>
<span [fxShow]="(persistentDatasource.isEmpty() | async) && !persistentDatasource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ noDataDisplayMessageText }}</span>
<span [fxShow]="persistentDatasource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ 'common.loading' | translate }}</span>
</div>
<mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator *ngIf="displayPagination"
[length]="persistentDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
</div>
</div>

53
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
.tb-table-widget {
.table-container {
position: relative;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
.column-id {
min-width: 250px;
max-width: 250px;
width: 250px;
}
.column-time {
min-width: 120px;
max-width: 120px;
width: 120px;
}
}
}

580
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts

@ -0,0 +1,580 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ElementRef,
Injector,
Input,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetContext } from '@home/models/widget-component.models';
import { WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { BehaviorSubject, merge, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import {
constructTableCssString,
noDataMessage,
TableCellButtonActionDescriptor,
TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models';
import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { hashCode, isDefined, isNumber } from '@core/utils';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import {
PersistentRpc,
PersistentRpcData,
RequestData,
RpcStatus,
rpcStatusColors,
rpcStatusTranslation
} from '@shared/models/rpc.models';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DialogService } from '@core/services/dialog.service';
import { DeviceService } from '@core/http/device.service';
import { MatDialog } from '@angular/material/dialog';
import {
PersistentDetailsDialogComponent,
PersistentDetailsDialogData
} from '@home/components/widget/lib/rpc/persistent-details-dialog.component';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
PERSISTENT_FILTER_PANEL_DATA,
PersistentFilterPanelComponent,
PersistentFilterPanelData
} from '@home/components/widget/lib/rpc/persistent-filter-panel.component';
import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { HttpErrorResponse } from '@angular/common/http';
interface PersistentTableWidgetSettings extends TableWidgetSettings {
defaultSortOrder: string;
defaultPageSize: number;
displayPagination: boolean;
enableStickyAction: boolean;
enableStickyHeader: boolean;
enableFilter: boolean;
displayColumns: string[];
displayDetails: boolean;
allowDelete: boolean;
allowSendRequest: boolean;
}
interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor {
details?: boolean;
delete?: boolean;
}
@Component({
selector: 'tb-persistent-table-widget',
templateUrl: './persistent-table.component.html',
styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss']
})
export class PersistentTableComponent extends PageComponent implements OnInit {
@Input()
ctx: WidgetContext;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
private settings: PersistentTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private enableFilterAction = true;
private allowSendRequest = true;
private defaultPageSize = 10;
private defaultSortOrder = '-createdTime';
private rpcStatusFilter: RpcStatus;
private displayDetails = true;
private allowDelete = true;
private displayTableColumns: string[];
private widgetResize$: ResizeObserver;
public persistentDatasource: PersistentDatasource;
public noDataDisplayMessageText: string;
public rpcStatusColor = rpcStatusColors;
public rpcStatusTranslation = rpcStatusTranslation;
public displayPagination = true;
public enableStickyHeader = true;
public enableStickyAction = true;
public pageLink: PageLink;
public pageSizeOptions;
public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = [];
public displayedColumns: string[];
public hidePageSize = false;
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private translate: TranslateService,
private dialogService: DialogService,
private deviceService: DeviceService,
private dialog: MatDialog,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit() {
this.ctx.$scope.persistentTableWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.initializeConfig();
this.ctx.updateWidgetParams();
if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
}
ngOnDestroy() {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
}
ngAfterViewInit(): void {
if (this.displayPagination) {
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
}
((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable<any>)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.updateData();
}
private initializeConfig() {
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : [];
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true;
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true;
this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true;
this.noDataDisplayMessageText =
noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate);
this.displayedColumns = [...this.displayTableColumns];
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
this.defaultPageSize = pageSize;
}
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024;
this.ctx.widgetActions = [
{
name: 'widgets.persistent-table.add',
show: this.allowSendRequest,
icon: 'add',
onAction: $event => this.addPersistentRpcRequest($event)
},
{
name: 'widgets.persistent-table.refresh',
show: true,
icon: 'refresh',
onAction: () => this.reloadPersistentRequests()
},
{
name: 'widgets.persistent-table.filter',
show: this.enableFilterAction,
icon: 'filter_list',
onAction: $event => this.editFilter($event)
}
];
if (this.settings.displayDetails) {
this.actionCellButtonAction.push(
{
displayName: this.translate.instant('widgets.persistent-table.details'),
icon: 'more_horiz',
details: true
} as PersistentTableWidgetActionDescriptor
);
}
if (this.settings.allowDelete) {
this.actionCellButtonAction.push(
{
displayName: this.translate.instant('widgets.persistent-table.delete'),
icon: 'delete',
delete: true
} as PersistentTableWidgetActionDescriptor
);
}
if (this.actionCellButtonAction.length) {
this.displayedColumns.push('actions');
}
this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx);
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'persistent-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateData() {
if (this.displayPagination) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
} else {
this.pageLink.page = 0;
}
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder);
}
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter);
this.ctx.detectChanges();
}
public onDataUpdated() {
this.ctx.detectChanges();
}
reloadPersistentRequests() {
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
}
deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) {
if ($event) {
$event.stopPropagation();
}
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('widgets.persistent-table.delete-request-title'),
this.translate.instant('widgets.persistent-table.delete-request-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => {
this.reloadPersistentRequests();
});
}
});
}
}
openRequestDetails($event: Event, persistentRpc: PersistentRpc) {
if ($event) {
$event.stopPropagation();
}
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialog.open<PersistentDetailsDialogComponent, PersistentDetailsDialogData, boolean>
(PersistentDetailsDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
persistentRequest: persistentRpc,
allowDelete: this.allowDelete
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.reloadPersistentRequests();
}
}
);
}
}
addPersistentRpcRequest($event: Event){
if ($event) {
$event.stopPropagation();
}
this.dialog.open<PersistentAddDialogComponent, RequestData>
(PersistentAddDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(requestData) => {
if (requestData) {
this.sendRequests(requestData);
}
}
);
}
private sendRequests(requestData: RequestData) {
let commandPromise;
if (requestData.oneWayElseTwoWay) {
commandPromise = this.ctx.controlApi.sendOneWayCommand(
requestData.method,
requestData.params, null,
true, null,
requestData.retries,
requestData.additionalInfo
);
} else {
commandPromise = this.ctx.controlApi.sendTwoWayCommand(
requestData.method,
requestData.params,
null,
true, null,
requestData.retries,
requestData.additionalInfo
);
}
commandPromise.subscribe(
() => {
this.reloadPersistentRequests();
}
);
}
public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) {
if (actionDescriptor.details) {
this.openRequestDetails($event, persistentRpc);
}
if (actionDescriptor.delete) {
this.deleteRpcRequest($event, persistentRpc);
}
}
private editFilter($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: PERSISTENT_FILTER_PANEL_DATA,
useValue: {
rpcStatus: this.rpcStatusFilter
} as PersistentFilterPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result) {
const result = componentRef.instance.result;
this.rpcStatusFilter = result.rpcStatus;
this.reloadPersistentRequests();
}
});
this.ctx.detectChanges();
}
}
class PersistentDatasource implements DataSource<PersistentRpcData> {
private persistentSubject = new BehaviorSubject<PersistentRpcData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<PersistentRpcData>>(emptyPageData<PersistentRpcData>());
private rpcErrorText: string;
private executingSubjects: Array<Subject<any>>;
private executingRpcRequest = false;
public dataLoading = true;
public pageData$ = this.pageDataSubject.asObservable();
constructor(private translate: TranslateService,
private subscription: IWidgetSubscription,
private ctx: WidgetContext) {
}
connect(collectionViewer: CollectionViewer): Observable<PersistentRpcData[] | ReadonlyArray<PersistentRpcData>> {
return this.persistentSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.persistentSubject.complete();
this.pageDataSubject.complete();
}
reset() {
const pageData = emptyPageData<PersistentRpcData>();
this.persistentSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) {
this.dataLoading = true;
const result = new ReplaySubject<PageData<PersistentRpcData>>();
this.fetchEntities(pageLink, rpcStatusFilter).pipe(
catchError(() => of(emptyPageData<PersistentRpcData>())),
).subscribe(
(pageData) => {
this.persistentSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;
}
fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable<PageData<PersistentRpcData>> {
if (!this.subscription.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else if (!this.subscription.targetDeviceId) {
return throwError(new Error('Target device is not set!'));
}
const rpcSubject: Subject<any> = new Subject<any>();
this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe(
(responseBody) => {
rpcSubject.next(responseBody);
rpcSubject.complete();
},
(rejection: HttpErrorResponse) => {
this.rpcErrorText = null;
this.executingSubjects = [];
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice(index, 1);
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.subscription.options.callbacks.rpcStateChanged(this.subscription);
if (!this.executingRpcRequest || rejection.status === 504) {
this.subscription.rpcRejection = rejection;
if (rejection.status === 504) {
this.subscription.rpcErrorText = 'Request Timeout.';
} else {
this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
const error = this.extractRejectionErrorText(rejection);
if (error) {
this.subscription.rpcErrorText += '</br>';
this.subscription.rpcErrorText += error.message || '';
}
}
this.subscription.callbacks.onRpcFailed(this.subscription);
}
rpcSubject.error(rejection);
}
);
return rpcSubject.asObservable();
}
extractRejectionErrorText(rejection: HttpErrorResponse) {
let error = null;
if (rejection.error) {
error = rejection.error;
try {
error = rejection.error ? JSON.parse(rejection.error) : null;
} catch (e) {}
}
if (error && !error.message) {
error = this.prepareMessageFromData(error);
} else if (error && error.message) {
error = error.message;
}
return error;
}
prepareMessageFromData(data) {
if (typeof data === 'object' && data.constructor === ArrayBuffer) {
const msg = String.fromCharCode.apply(null, new Uint8Array(data));
try {
const msgObj = JSON.parse(msg);
if (msgObj.message) {
return msgObj.message;
} else {
return msg;
}
} catch (e) {
return msg;
}
} else {
return data;
}
}
isEmpty(): Observable<boolean> {
return this.persistentSubject.pipe(
map((requests) => !requests.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
}

15
ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts

@ -21,6 +21,10 @@ import { LedIndicatorComponent } from '@home/components/widget/lib/rpc/led-indic
import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component';
import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component';
import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
import { PersistentTableComponent } from '@home/components/widget/lib/rpc/persistent-table.component';
import { PersistentDetailsDialogComponent } from '@home/components/widget/lib/rpc/persistent-details-dialog.component';
import { PersistentFilterPanelComponent } from '@home/components/widget/lib/rpc/persistent-filter-panel.component';
import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component';
@NgModule({
declarations:
@ -28,7 +32,11 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent,
RoundSwitchComponent,
SwitchComponent,
KnobComponent
KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent,
PersistentFilterPanelComponent
],
imports: [
CommonModule,
@ -38,7 +46,10 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent,
RoundSwitchComponent,
SwitchComponent,
KnobComponent
KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent
]
})
export class RpcWidgetsModule { }

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

@ -197,16 +197,18 @@ export class WidgetContext {
};
controlApi: RpcApi = {
sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => {
sendOneWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID);
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else {
return of(null);
}
},
sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => {
sendTwoWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID);
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else {
return of(null);
}

22
ui-ngx/src/app/shared/components/json-object-view.component.html

@ -0,0 +1,22 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}">
<label class="tb-title no-padding" *ngIf="label">{{ label }}</label>
<span fxFlex></span>
<div #jsonViewer id="tb-json-view" [ngClass]="{'fill-height': fillHeight}"></div>
</div>

27
ui-ngx/src/app/shared/components/json-object-view.component.scss

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
#tb-json-view {
width: 100%;
height: 100%;
margin-bottom: 16px;
border: 1px solid #c0c0c0;
&:not(.fill-height) {
min-height: 100px;
}
}
}

165
ui-ngx/src/app/shared/components/json-object-view.component.ts

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { RafService } from '@core/services/raf.service';
import { isDefinedAndNotNull, isUndefined } from '@core/utils';
import { getAce } from '@shared/models/ace/ace.models';
@Component({
selector: 'tb-json-object-view',
templateUrl: './json-object-view.component.html',
styleUrls: ['./json-object-view.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonObjectViewComponent),
multi: true
}
]
})
export class JsonObjectViewComponent implements OnInit {
@ViewChild('jsonViewer', {static: true})
jsonViewerElmRef: ElementRef;
private jsonViewer: Ace.Editor;
private viewerElement: Ace.Editor;
private propagateChange = null;
private modelValue: any;
private contentValue: string;
@Input() label: string;
@Input() fillHeight: boolean;
@Input() editorStyle: { [klass: string]: any };
@Input() sort: (key: string, value: any) => any;
private widthValue: boolean;
get autoWidth(): boolean {
return this.widthValue;
}
@Input()
set autoWidth(value: boolean) {
this.widthValue = coerceBooleanProperty(value);
}
private heigthValue: boolean;
get autoHeight(): boolean {
return this.heigthValue;
}
@Input()
set autoHeight(value: boolean) {
this.heigthValue = coerceBooleanProperty(value);
}
constructor(public elementRef: ElementRef,
protected store: Store<AppState>,
private raf: RafService,
private renderer: Renderer2) {
}
ngOnInit(): void {
this.viewerElement = this.jsonViewerElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/java',
theme: 'ace/theme/github',
showGutter: false,
showPrintMargin: false,
readOnly: true
};
const advancedOptions = {
enableSnippets: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.jsonViewer = ace.edit(this.viewerElement, editorOptions);
this.jsonViewer.session.setUseWrapMode(false);
this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1);
if (this.contentValue && (this.widthValue || this.heigthValue)) {
this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer);
}
}
);
}
updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) {
let newHeight = 200;
let newWidth = 600;
if (content && content.length > 0) {
const lines = content.split('\n');
newHeight = 17 * lines.length + 17;
let maxLineLength = 0;
lines.forEach((row) => {
const line = row.replace(/\t/g, ' ').replace(/\n/g, '');
const lineLength = line.length;
maxLineLength = Math.max(maxLineLength, lineLength);
});
newWidth = 8 * maxLineLength + 16;
}
if (this.heigthValue) {
this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
}
if (this.widthValue) {
this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px');
}
editor.resize();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
writeValue(value: any): void {
this.modelValue = value;
this.contentValue = '';
try {
if (isDefinedAndNotNull(this.modelValue)) {
this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
(key, objectValue) => {
return this.sort(key, objectValue);
}, 2);
}
} catch (e) {
console.error(e);
}
if (this.jsonViewer) {
this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1);
if (this.contentValue && (this.widthValue || this.heigthValue)) {
this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer);
}
}
}
}

1
ui-ngx/src/app/shared/models/constants.ts

@ -243,6 +243,7 @@ export const contentTypesMap = new Map<ContentType, ContentTypeData>(
]
);
export const hidePageSizePixelValue = 550;
export const customTranslationsPrefix = 'custom.';
export const i18nPrefix = 'i18n';

50
ui-ngx/src/app/shared/models/rpc.models.ts

@ -17,15 +17,42 @@
import { TenantId } from '@shared/models/id/tenant-id';
import { RpcId } from '@shared/models/id/rpc-id';
import { DeviceId } from '@shared/models/id/device-id';
import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models';
export enum RpcStatus {
QUEUED = 'QUEUED',
DELIVERED = 'DELIVERED',
SUCCESSFUL = 'SUCCESSFUL',
TIMEOUT = 'TIMEOUT',
FAILED = 'FAILED'
FAILED = 'FAILED',
SENT = 'SENT',
EXPIRED = 'EXPIRED'
}
export const rpcStatusColors = new Map<RpcStatus, string>(
[
[RpcStatus.QUEUED, 'black'],
[RpcStatus.DELIVERED, 'green'],
[RpcStatus.SUCCESSFUL, 'green'],
[RpcStatus.TIMEOUT, 'orange'],
[RpcStatus.FAILED, 'red'],
[RpcStatus.SENT, 'green'],
[RpcStatus.EXPIRED, 'red']
]
);
export const rpcStatusTranslation = new Map<RpcStatus, string>(
[
[RpcStatus.QUEUED, 'widgets.persistent-table.rpc-status.QUEUED'],
[RpcStatus.DELIVERED, 'widgets.persistent-table.rpc-status.DELIVERED'],
[RpcStatus.SUCCESSFUL, 'widgets.persistent-table.rpc-status.SUCCESSFUL'],
[RpcStatus.TIMEOUT, 'widgets.persistent-table.rpc-status.TIMEOUT'],
[RpcStatus.FAILED, 'widgets.persistent-table.rpc-status.FAILED'],
[RpcStatus.SENT, 'widgets.persistent-table.rpc-status.SENT'],
[RpcStatus.EXPIRED, 'widgets.persistent-table.rpc-status.EXPIRED']
]
);
export interface PersistentRpc {
id: RpcId;
createdTime: number;
@ -34,7 +61,28 @@ export interface PersistentRpc {
response: any;
request: {
id: string;
oneway: boolean;
body: {
method: string;
params: string;
};
retries: null | number;
};
deviceId: DeviceId;
tenantId: TenantId;
additionalInfo?: string;
}
export interface PersistentRpcData extends PersistentRpc {
actionCellButtons?: TableCellButtonActionDescriptor[];
hasActions?: boolean;
}
export interface RequestData {
method?: string;
oneWayElseTwoWay?: boolean;
persistentPollingInterval?: number;
retries?: number;
params?: object;
additionalInfo?: object;
}

5
ui-ngx/src/app/shared/shared.module.ts

@ -94,6 +94,7 @@ import { SocialSharePanelComponent } from '@shared/components/socialshare-panel.
import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
import { EntityListSelectComponent } from '@shared/components/entity/entity-list-select.component';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { JsonObjectViewComponent, } from '@shared/components/json-object-view.component';
import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
import { CircularProgressDirective } from '@shared/components/circular-progress.directive';
import {
@ -149,7 +150,7 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo
import { HelpPopupComponent } from '@shared/components/help-popup.component';
import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component';
import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive';
import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive';
import { TbComponentOutletDirective } from '@shared/components/directives/component-outlet.directive';
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
import { MarkedOptionsService } from '@shared/components/marked-options.service';
import { TbPopoverService } from '@shared/components/popover.service';
@ -232,6 +233,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonObjectViewComponent,
JsonContentComponent,
JsFuncComponent,
CssComponent,
@ -378,6 +380,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonObjectViewComponent,
JsonContentComponent,
JsFuncComponent,
CssComponent,

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

@ -3269,6 +3269,48 @@
"value": "Value"
},
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type",
"persistent-table": {
"rpc-id": "RPC ID",
"message-type": "Message type",
"method": "Method",
"params": "Params",
"created-time": "Created time",
"expiration-time": "Expiration time",
"retries": "Retries",
"status": "Status",
"filter": "Filter",
"refresh": "Refresh",
"add": "Add RPC request",
"details": "Details",
"delete": "Delete",
"delete-request-title": "Delete Persistent RPC request",
"delete-request-text": "Are you sure you want to delete request?",
"details-title": "Details RPC ID: ",
"additional-info": "Additional info",
"response": "Response",
"any-status": "Any status",
"rpc-status-list": "RPC status list",
"no-request-prompt": "No request to display",
"send-request": "Send request",
"add-title": "Create Persistent RPC request",
"method-error": "Method is required.",
"timeout-error": "Min timeout value is 5000 (5 seconds).",
"white-space-error": "White space is not allowed.",
"rpc-status": {
"QUEUED": "QUEUED",
"SENT": "SENT",
"DELIVERED": "DELIVERED",
"SUCCESSFUL": "SUCCESSFUL",
"TIMEOUT": "TIMEOUT",
"EXPIRED": "EXPIRED",
"FAILED": "FAILED"
},
"rpc-search-status-all": "ALL",
"message-types": {
"false": "Two-way",
"true": "One-way"
}
},
"maps": {
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position",

41
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -1785,6 +1785,47 @@
"update-attribute": "Обновить атрибут",
"update-timeseries": "Обновить телеметрию",
"value": "Значение"
},
"persistent-table": {
"rpc-id": "RPC ID",
"message-type": "Тип сообщения",
"method": "Метод",
"params": "Параметры",
"created-time": "Время создания",
"expiration-time": "Время жизни",
"retries": "Повторные попытки",
"status": "Статус",
"filter": "Фильтр",
"refresh": "Обновить",
"add": "Добавить RPC запрос",
"details": "Детали",
"delete": "Удалить",
"delete-request-title": "Удалить RPC запрос",
"delete-request-text": "Вы точно хотите удалить RPC запрос?",
"details-title": "Детали RPC ID: ",
"additional-info": "Дополнительная информация",
"response": "Ответ",
"any-status": "Любой статус",
"rpc-status-list": "Список RPC статусов",
"no-request-prompt": "Запросы не найдены",
"send-request": "Отправить запрос",
"add-title": "Добавить новый RPC запрос",
"method-error": "Метод обязателен.",
"white-space-error": "Пробелы не допускаются.",
"rpc-status": {
"QUEUED": "В ОЧЕРЕДИ",
"SENT": "ОТПРАВЛЕННО",
"DELIVERED": "ДОСТАВЛЕННО",
"SUCCESSFUL": "УСПЕШНО",
"TIMEOUT": "ВРЕМЯ ИСТЕКЛО",
"EXPIRED": "ПРОСРОЧЕНО",
"FAILED": "НЕУДАЧНО"
},
"rpc-search-status-all": "ВСЕ",
"message-types": {
"false": "Двусторонний",
"true": "Односторонний"
}
}
},
"icon": {

41
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -2357,6 +2357,47 @@
"update-attribute": "Оновити атрибут",
"update-timeseries": "Оновити телеметрію",
"value": "Значення"
},
"persistent-table": {
"rpc-id": "RPC ID",
"message-type": "Тип повідомлення",
"method": "Метод",
"params": "Параметри",
"created-time": "Час створення",
"expiration-time": "Час життя",
"retries": "Повторні спроби",
"status": "Статус",
"filter": "Фільтр",
"refresh": "Оновити",
"add": "Додати RPC запит",
"details": "Деталі",
"delete": "Видалити",
"delete-request-title": "Видалити RPC запит",
"delete-request-text": "Ви впевнені, що хочете видалити RPC запит?",
"details-title": "Деталі RPC ID: ",
"additional-info": "Додаткова інформація",
"response": "Відповідь",
"any-status": "Будь-який статус",
"rpc-status-list": "Список RPC статусів",
"no-request-prompt": "Запитів не знайдено",
"send-request": "Відправити запит",
"add-title": "Додати новий RPC запит",
"method-error": "Необхідно вказати метод.",
"white-space-error": "Пробіли не допускаються.",
"rpc-status": {
"QUEUED": "В ЧЕРЗІ",
"SENT": "ВІДПРАВЛЕНО",
"DELIVERED": "ДОСТАВЛЕННО",
"SUCCESSFUL": "УСПІШНО",
"TIMEOUT": "ЧАС МИНУВ",
"EXPIRED": "ПРОСРОЧЕНО",
"FAILED": "НЕ ВДАЛО"
},
"rpc-search-status-all": "ВСІ",
"message-types": {
"false": "Двосторонній",
"true": "Односторонній"
}
}
},
"white-labeling": {

Loading…
Cancel
Save