UI: New control widget Persistent table
This commit is contained in:
		
							parent
							
								
									77b9a8c1af
								
							
						
					
					
						commit
						5d4579ce28
					
				
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -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));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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 =
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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 { }
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								ui-ngx/src/app/shared/components/json-object-view.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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": {
 | 
			
		||||
 | 
			
		||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user