Browse Source

UI: New control widget Persistent table

pull/5629/head
ArtemDzhereleiko 5 years ago
parent
commit
5d4579ce28
  1. 18
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 11
      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. 14
      ui-ngx/src/app/core/api/widget-api.models.ts
  9. 23
      ui-ngx/src/app/core/api/widget-subscription.ts
  10. 13
      ui-ngx/src/app/core/http/device.service.ts
  11. 92
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html
  12. 30
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss
  13. 77
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts
  14. 118
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html
  15. 33
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss
  16. 151
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts
  17. 44
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html
  18. 36
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss
  19. 71
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts
  20. 125
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  21. 52
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss
  22. 476
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  23. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts
  24. 10
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  25. 22
      ui-ngx/src/app/shared/components/json-object-view.component.html
  26. 27
      ui-ngx/src/app/shared/components/json-object-view.component.scss
  27. 166
      ui-ngx/src/app/shared/components/json-object-view.component.ts
  28. 51
      ui-ngx/src/app/shared/models/rpc.models.ts
  29. 3
      ui-ngx/src/app/shared/shared.module.ts
  30. 44
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  31. 41
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  32. 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

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

@ -177,8 +177,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)
@ -194,7 +194,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);

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

@ -55,6 +55,8 @@ 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 { PageLink } from '@shared/models/page/page-link';
import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -71,9 +73,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;
}
@ -287,6 +289,8 @@ export interface IWidgetSubscription {
comparisonEnabled?: boolean;
comparisonTimeWindow?: WidgetTimewindow;
persistentRequests?: PageData<PersistentRpc>;
alarms?: PageData<AlarmData>;
alarmSource?: Datasource;
@ -313,11 +317,13 @@ 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;
subscribeForPersistentRequests(pageLink: PageLink, keyFileter: RpcStatus): Observable<any>;
subscribe(): void;
subscribeAllForPaginatedData(pageLink: EntityDataPageLink,

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

@ -70,6 +70,7 @@ import {
import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators';
import { AlarmDataListener } from '@core/api/alarm-data.service';
import { RpcStatus } from '@shared/models/rpc.models';
import { PageLink } from '@shared/models/page/page-link';
const moment = moment_;
@ -656,13 +657,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 +680,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 +694,8 @@ export class WidgetSubscription implements IWidgetSubscription {
method,
params,
persistent,
retries,
additionalInfo,
requestUUID
};
if (timeout && timeout > 0) {
@ -777,6 +781,15 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
subscribeForPersistentRequests(pageLink: PageLink, keyFilter: RpcStatus): Observable<any> {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else if (!this.targetDeviceId) {
return throwError(new Error('Target device is not set!'));
}
return this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter);
}
private extractRejectionErrorText(rejection: HttpErrorResponse) {
let error = null;
if (rejection.error) {

13
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,17 @@ 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,
keyFilter: RpcStatus, config?: RequestConfig): Observable<PageData<PersistentRpc>> {
const rpcStatus = keyFilter ? '&rpcStatus=' + keyFilter : '';
return this.http.get<PageData<PersistentRpc>>(`/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}${rpcStatus}`,
defaultHttpOptionsFromConfig(config));
}
public findByQuery(query: DeviceSearchQuery,
config?: RequestConfig): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config));

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">
{{ 'widgets.persistent-table.message-types.' + persistentFormGroup.get('oneWayElseTwoWay').value | translate }}
</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>

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

@ -0,0 +1,30 @@
/**
* 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 {
.add-dialog ::ng-deep {
.params-json-editor,
.additional-json-editor {
.tb-json-object-panel {
margin: 0 0 16px;
}
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
}
}
}

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

@ -0,0 +1,77 @@
///
/// 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';
@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;
private requestData: RequestData = {
persistentUpdated: false
};
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<PersistentAddDialogComponent, RequestData>,
private fb: FormBuilder) {
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: [{}],
additionalInfo: [{}]
}
);
}
save() {
if (this.persistentFormGroup.valid) {
this.requestData = {
persistentUpdated: true,
method: this.persistentFormGroup.get('method').value,
oneWayElseTwoWay: this.persistentFormGroup.get('oneWayElseTwoWay').value,
params: this.persistentFormGroup.get('params').value,
additionalInfo: this.persistentFormGroup.get('additionalInfo').value,
retries: this.persistentFormGroup.get('retries').value
};
this.close();
}
}
ngOnInit(): void {
}
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>

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

@ -0,0 +1,33 @@
/**
* 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 {
.rpc-dialog ::ng-deep {
.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;
}
}

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

@ -0,0 +1,151 @@
///
/// 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,
rpcStatusColors,
RpcStatus,
rpcStatusTranslation
} from '@shared/models/rpc.models';
import { isDefinedAndNotNull } from '@core/utils';
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: [null]
}
);
this.loadPersistentFields(data.persistentRequest);
this.responseData = JSON.stringify(data.persistentRequest.response, null, 2);
}
loadPersistentFields(request: PersistentRpc) {
this.persistentFormGroup.get('rpcId')
.patchValue(this.translate.instant('widgets.persistent-table.details-title') + request.id.id);
this.persistentFormGroup.get('createdTime')
.patchValue(this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss'));
this.persistentFormGroup.get('expirationTime')
.patchValue(this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss'));
this.persistentFormGroup.get('messageType')
.patchValue(this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway)
);
this.persistentFormGroup.get('status')
.patchValue(this.translate.instant(rpcStatusTranslation.get(request.status)));
this.persistentFormGroup.get('method')
.patchValue(request.request.body.method);
if (isDefinedAndNotNull(request.request.retries)) {
this.persistentFormGroup.get('retries')
.patchValue(request.request.retries);
}
if (isDefinedAndNotNull(request.response)) {
this.persistentFormGroup.get('response')
.patchValue(request.response);
}
if (isDefinedAndNotNull(request.request.body.params)) {
this.persistentFormGroup.get('params')
.patchValue(JSON.parse(request.request.body.params));
}
if (isDefinedAndNotNull(request.additionalInfo)) {
this.persistentFormGroup.get('additionalInfo')
.patchValue(request.additionalInfo);
}
}
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) {
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="{{ !persistentFilterFormGroup.get('rpcStatus').value?.length ? ('widgets.persistent-table.any-status' | translate) : '' }}">
<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>

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

@ -0,0 +1,36 @@
/**
* 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;
}
}

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

@ -0,0 +1,71 @@
///
/// 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';
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 persistentSearchStatuses = [
RpcStatus.QUEUED,
RpcStatus.SENT,
RpcStatus.DELIVERED,
RpcStatus.SUCCESSFUL,
RpcStatus.TIMEOUT,
RpcStatus.EXPIRED,
RpcStatus.FAILED
];
constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA)
public data: PersistentFilterPanelData,
public overlayRef: OverlayRef,
private fb: FormBuilder) {
this.persistentFilterFormGroup = this.fb.group(
{
rpcStatus: this.data.rpcStatus
}
);
}
update() {
this.result = {
rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value
};
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
}

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

@ -0,0 +1,125 @@
<!--
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"
showFirstLastButtons></mat-paginator>
</div>
</div>

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

@ -0,0 +1,52 @@
/**
* 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%;
.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;
}
}
}

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

@ -0,0 +1,476 @@
///
/// 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,
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 } 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';
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 | null = null;
private displayDetails = true;
private allowDelete = true;
private displayTableColumns: string[];
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[];
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) {
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();
}
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);
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) {
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.persistentUpdated) {
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>());
public dataLoading = true;
public pageData$ = this.pageDataSubject.asObservable();
constructor(private translate: TranslateService,
private subscription: IWidgetSubscription) {
}
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, keyFilter: RpcStatus) {
this.dataLoading = true;
const result = new ReplaySubject<PageData<PersistentRpcData>>();
this.fetchEntities(pageLink, keyFilter).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, keyFilter: RpcStatus): Observable<PageData<PersistentRpcData>> {
return this.subscription.subscribeForPersistentRequests(pageLink, keyFilter);
}
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

@ -196,16 +196,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;
}
}
}

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

@ -0,0 +1,166 @@
///
/// 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, 'minHeight', newHeight.toString() + 'px');
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) {
//
}
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);
}
}
}
}

51
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,29 @@ 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 {
persistentUpdated: boolean;
method?: string;
oneWayElseTwoWay?: boolean;
persistentPollingInterval?: number;
retries?: number;
params?: object;
additionalInfo?: object;
}

3
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 {
@ -231,6 +232,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonObjectViewComponent,
JsonContentComponent,
JsFuncComponent,
FabTriggerDirective,
@ -376,6 +378,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonObjectViewComponent,
JsonContentComponent,
JsFuncComponent,
FabTriggerDirective,

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

@ -3238,7 +3238,49 @@
"update-timeseries": "Update timeseries",
"value": "Value"
},
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type"
"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"
}
}
},
"icon": {
"icon": "Icon",

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

@ -1787,6 +1787,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

@ -2359,6 +2359,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