32 changed files with 1805 additions and 24 deletions
File diff suppressed because one or more lines are too long
@ -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) |
|||
); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue