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