154 changed files with 6160 additions and 326 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,207 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.service.sql; |
|||
|
|||
import com.google.gson.JsonObject; |
|||
import com.google.gson.JsonParser; |
|||
import org.jetbrains.annotations.NotNull; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; |
|||
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.asset.Asset; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
|||
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgDataType; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.TimeoutException; |
|||
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
@DaoSqlTest |
|||
public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest { |
|||
|
|||
static final int TIMEOUT = 30; |
|||
|
|||
final String TOTALIZER = "Totalizer"; |
|||
final int TTL = 99999; |
|||
final String GENERIC_CUMULATIVE_OBJ = "genericCumulativeObj"; |
|||
final List<Long> ts = List.of(10L, 20L, 30L, 40L, 60L, 70L, 50L, 80L); |
|||
final List<Long> msgValue = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L); |
|||
|
|||
@Autowired |
|||
TimeseriesService timeseriesService; |
|||
|
|||
TbMsgTimeseriesNodeConfiguration configuration; |
|||
Tenant savedTenant; |
|||
User tenantAdmin; |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
configuration = new TbMsgTimeseriesNodeConfiguration(); |
|||
configuration.setUseServerTs(true); |
|||
|
|||
loginSysAdmin(); |
|||
|
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("My tenant"); |
|||
savedTenant = doPost("/api/tenant", tenant, Tenant.class); |
|||
Assert.assertNotNull(savedTenant); |
|||
|
|||
tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(savedTenant.getId()); |
|||
tenantAdmin.setEmail("tenant2@thingsboard.org"); |
|||
tenantAdmin.setFirstName("Joe"); |
|||
tenantAdmin.setLastName("Downs"); |
|||
|
|||
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest() throws Exception { |
|||
loginSysAdmin(); |
|||
doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testSequentialTimeseriesPersistence() throws Exception { |
|||
Asset asset = saveAsset("Asset"); |
|||
|
|||
Device deviceA = saveDevice("Device A"); |
|||
Device deviceB = saveDevice("Device B"); |
|||
Device deviceC = saveDevice("Device C"); |
|||
Device deviceD = saveDevice("Device D"); |
|||
List<Device> devices = List.of(deviceA, deviceB, deviceC, deviceD); |
|||
|
|||
for (int i = 0; i < 2; i++) { |
|||
int idx = i * devices.size(); |
|||
saveLatestTsForAssetAndDevice(devices, asset, idx); |
|||
checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset); |
|||
} |
|||
} |
|||
|
|||
Device saveDevice(String name) throws Exception { |
|||
Device device = new Device(); |
|||
device.setName(name); |
|||
device.setType("default"); |
|||
Device savedDevice = doPost("/api/device", device, Device.class); |
|||
Assert.assertNotNull(savedDevice); |
|||
return savedDevice; |
|||
} |
|||
|
|||
Asset saveAsset(String name) throws Exception { |
|||
Asset asset = new Asset(); |
|||
asset.setName(name); |
|||
asset.setType("default"); |
|||
Asset savedAsset = doPost("/api/asset", asset, Asset.class); |
|||
Assert.assertNotNull(savedAsset); |
|||
return savedAsset; |
|||
} |
|||
|
|||
void saveLatestTsForAssetAndDevice(List<Device> devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { |
|||
for (Device device : devices) { |
|||
TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), |
|||
device.getId(), |
|||
getTbMsgMetadata(device.getName(), ts.get(idx)), |
|||
TbMsgDataType.JSON, |
|||
getTbMsgData(msgValue.get(idx))); |
|||
saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx)); |
|||
saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs())); |
|||
idx++; |
|||
} |
|||
} |
|||
|
|||
void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { |
|||
TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); |
|||
Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); |
|||
JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); |
|||
for (Device device : devices) { |
|||
Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); |
|||
TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); |
|||
Assert.assertTrue(deviceLatest.getLongValue().isPresent()); |
|||
Long deviceValue = deviceLatest.getLongValue().get(); |
|||
Assert.assertEquals(assetValue, deviceValue); |
|||
} |
|||
} |
|||
|
|||
String getTbMsgData(long value) { |
|||
return "{\"Totalizer\": " + value + "}"; |
|||
} |
|||
|
|||
TbMsgMetaData getTbMsgMetadata(String name, long ts) { |
|||
Map<String, String> metadata = new HashMap<>(); |
|||
metadata.put("deviceName", name); |
|||
metadata.put("ts", String.valueOf(ts)); |
|||
return new TbMsgMetaData(metadata); |
|||
} |
|||
|
|||
void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException { |
|||
TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs()), new LongDataEntry(TOTALIZER, value)); |
|||
saveTimeseries(entityId, tsKvEntry); |
|||
} |
|||
|
|||
void saveAssetTsEntry(Asset asset, String key, long value, long ts) throws ExecutionException, InterruptedException, TimeoutException { |
|||
Optional<String> tsKvEntryOpt = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ).getJsonValue(); |
|||
TsKvEntry saveTsKvEntry = new BasicTsKvEntry(ts, new JsonDataEntry(GENERIC_CUMULATIVE_OBJ, getJsonObject(key, value, tsKvEntryOpt).toString())); |
|||
saveTimeseries(asset.getId(), saveTsKvEntry); |
|||
} |
|||
|
|||
@NotNull |
|||
JsonObject getJsonObject(String key, long value, Optional<String> tsKvEntryOpt) { |
|||
JsonObject jsonObject = new JsonObject(); |
|||
if (tsKvEntryOpt.isPresent()) { |
|||
jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); |
|||
} |
|||
jsonObject.addProperty(key, value); |
|||
return jsonObject; |
|||
} |
|||
|
|||
void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { |
|||
timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
TsKvEntry getTsKvLatest(EntityId entityId, String key) throws InterruptedException, ExecutionException, TimeoutException { |
|||
List<TsKvEntry> tsKvEntries = timeseriesService.findLatest( |
|||
savedTenant.getTenantId(), |
|||
entityId, |
|||
List.of(key)).get(TIMEOUT, TimeUnit.SECONDS); |
|||
Assert.assertEquals(1, tsKvEntries.size()); |
|||
return tsKvEntries.get(0); |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<mat-card class="settings-card"> |
|||
<mat-toolbar class="details-toolbar"> |
|||
<div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center"> |
|||
<div class="tb-details-title-header" fxLayout="column" fxLayoutAlign="center start"> |
|||
<div class="tb-details-title tb-ellipsis">{{ headerTitle }}</div> |
|||
<div class="tb-details-subtitle tb-ellipsis">{{ headerSubtitle }}</div> |
|||
</div> |
|||
<div class="tb-help" [tb-help]="helpLinkId()"></div> |
|||
<span fxFlex></span> |
|||
<section *ngIf="!isReadOnly" fxLayout="row" class="tb-header-button" fxLayoutGap="8px"> |
|||
<button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty" |
|||
mat-fab |
|||
matTooltip="{{ 'action.apply-changes' | translate }}" |
|||
matTooltipPosition="above" |
|||
color="accent" class="tb-btn-header" |
|||
[ngClass]="{'tb-hide': !isEdit}" |
|||
(click)="onApplyDetails()"> |
|||
<mat-icon class="material-icons">done</mat-icon> |
|||
</button> |
|||
<button [disabled]="(isLoading$ | async)" |
|||
mat-fab |
|||
matTooltip="{{ 'action.decline-changes' | translate }}" |
|||
matTooltipPosition="above" |
|||
color="accent" class="tb-btn-header" |
|||
(click)="onToggleDetailsEditMode()"> |
|||
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon> |
|||
</button> |
|||
</section> |
|||
</div> |
|||
</mat-toolbar> |
|||
<mat-card-content fxFlex="100"> |
|||
<mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': hideDetailsTabs()}" [(selectedIndex)]="selectedTab" fxFill> |
|||
<mat-tab label="{{ 'details.details' | translate }}"> |
|||
<tb-anchor #entityDetailsForm></tb-anchor> |
|||
</mat-tab> |
|||
<tb-anchor #entityTabs></tb-anchor> |
|||
</mat-tab-group> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
|
|||
@ -0,0 +1,119 @@ |
|||
/** |
|||
* 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 "../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
overflow: hidden; |
|||
|
|||
.settings-card { |
|||
margin: 8px; |
|||
padding: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.details-toolbar { |
|||
height: 84px; |
|||
min-height: 84px; |
|||
border-radius: 4px 4px 0 0; |
|||
background: #fff; |
|||
border-bottom: 1px solid rgba(0, 0, 0, 0.12); |
|||
|
|||
.mat-toolbar-tools { |
|||
padding: 0 8px; |
|||
} |
|||
|
|||
.tb-details-title-header { |
|||
min-width: 0; |
|||
width: auto; |
|||
} |
|||
|
|||
.tb-details-title { |
|||
font-size: 1rem; |
|||
font-weight: 500; |
|||
|
|||
@media #{$mat-gt-sm} { |
|||
font-size: 1.2rem; |
|||
} |
|||
} |
|||
|
|||
.tb-details-subtitle { |
|||
font-size: 0.9rem; |
|||
opacity: .8; |
|||
} |
|||
|
|||
.tb-ellipsis { |
|||
width: 100%; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
|
|||
@media #{$mat-md} { |
|||
width: 80%; |
|||
} |
|||
|
|||
@media #{$mat-gt-md} { |
|||
width: 60%; |
|||
} |
|||
|
|||
.tb-header-button { |
|||
.tb-btn-header { |
|||
position: relative !important; |
|||
display: inline-block !important; |
|||
animation: tbMoveFromTopFade .3s ease both; |
|||
|
|||
&.tb-hide { |
|||
animation: tbMoveToTopFade .3s ease both; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-help { |
|||
.mat-icon-button.mat-primary { |
|||
color: rgba(0, 0, 0, 0.52); |
|||
} |
|||
} |
|||
|
|||
.mat-card-content { |
|||
position: relative; |
|||
overflow: hidden; |
|||
|
|||
> .mat-tab-group { |
|||
> .mat-tab-body-wrapper { |
|||
position: absolute; |
|||
top: 49px; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
} |
|||
> .mat-tab-header { |
|||
.mat-tab-label { |
|||
min-width: 40px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,182 @@ |
|||
///
|
|||
/// 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 { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ComponentFactoryResolver, |
|||
HostBinding, |
|||
Injector, |
|||
OnDestroy, |
|||
OnInit |
|||
} from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
|||
import { BaseData, HasId } from '@shared/models/base-data'; |
|||
import { ActivatedRoute, Router } from '@angular/router'; |
|||
import { FormGroup } from '@angular/forms'; |
|||
import { AssetId } from '@shared/models/id/asset-id'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { deepClone, mergeDeep } from '@core/utils'; |
|||
import { BroadcastService } from '@core/services/broadcast.service'; |
|||
import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-details-page', |
|||
templateUrl: './entity-details-page.component.html', |
|||
styleUrls: ['./entity-details-page.component.scss'], |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements OnInit, OnDestroy { |
|||
|
|||
headerTitle: string; |
|||
headerSubtitle: string; |
|||
|
|||
isReadOnly = false; |
|||
|
|||
set entitiesTableConfig(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) { |
|||
if (this.entitiesTableConfigValue !== entitiesTableConfig) { |
|||
this.entitiesTableConfigValue = entitiesTableConfig; |
|||
if (this.entitiesTableConfigValue) { |
|||
this.isEdit = false; |
|||
this.entity = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
get entitiesTableConfig(): EntityTableConfig<BaseData<HasId>> { |
|||
return this.entitiesTableConfigValue; |
|||
} |
|||
|
|||
@HostBinding('class') 'tb-absolute-fill'; |
|||
|
|||
constructor(private route: ActivatedRoute, |
|||
private router: Router, |
|||
protected injector: Injector, |
|||
protected cd: ChangeDetectorRef, |
|||
protected componentFactoryResolver: ComponentFactoryResolver, |
|||
private broadcast: BroadcastService, |
|||
private translate: TranslateService, |
|||
private dialogService: DialogService, |
|||
protected store: Store<AppState>) { |
|||
super(store, injector, cd, componentFactoryResolver); |
|||
this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.headerSubtitle = ''; |
|||
this.route.paramMap.subscribe( paramMap => { |
|||
this.entityId = new AssetId(paramMap.get('entityId')); |
|||
}); |
|||
this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); |
|||
super.init(); |
|||
this.entityComponent.isDetailsPage = true; |
|||
this.subscriptions.push(this.entityAction.subscribe((action) => { |
|||
if (action.action === 'delete') { |
|||
this.deleteEntity(action.event, action.entity); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
reload(): void { |
|||
this.isEdit = false; |
|||
this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( |
|||
(entity) => { |
|||
this.entity = entity; |
|||
this.broadcast.broadcast('updateBreadcrumb'); |
|||
this.isReadOnly = this.entitiesTableConfig.detailsReadonly(entity); |
|||
this.headerTitle = this.entitiesTableConfig.entityTitle(entity); |
|||
this.entityComponent.entity = entity; |
|||
this.entityComponent.isEdit = false; |
|||
if (this.entityTabsComponent) { |
|||
this.entityTabsComponent.entity = entity; |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
onToggleDetailsEditMode() { |
|||
if (this.isEdit) { |
|||
this.entityComponent.entity = this.entity; |
|||
if (this.entityTabsComponent) { |
|||
this.entityTabsComponent.entity = this.entity; |
|||
} |
|||
this.isEdit = !this.isEdit; |
|||
} else { |
|||
this.isEdit = !this.isEdit; |
|||
this.editingEntity = deepClone(this.entity); |
|||
this.entityComponent.entity = this.editingEntity; |
|||
if (this.entityTabsComponent) { |
|||
this.entityTabsComponent.entity = this.editingEntity; |
|||
} |
|||
if (this.entitiesTableConfig.hideDetailsTabsOnEdit) { |
|||
this.selectedTab = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
onApplyDetails() { |
|||
if (this.detailsForm && this.detailsForm.valid) { |
|||
const editingEntity = {...this.editingEntity, ...this.detailsForm.getRawValue()}; |
|||
if (this.detailsForm.hasOwnProperty('additionalInfo')) { |
|||
editingEntity.additionalInfo = |
|||
mergeDeep((this.editingEntity as any).additionalInfo, this.detailsForm.getRawValue()?.additionalInfo); |
|||
} |
|||
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( |
|||
(entity) => { |
|||
this.entity = entity; |
|||
this.entityComponent.entity = entity; |
|||
if (this.entityTabsComponent) { |
|||
this.entityTabsComponent.entity = entity; |
|||
} |
|||
this.isEdit = false; |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
confirmForm(): FormGroup { |
|||
return this.detailsForm; |
|||
} |
|||
|
|||
private deleteEntity($event: Event, entity: BaseData<HasId>) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialogService.confirm( |
|||
this.entitiesTableConfig.deleteEntityTitle(entity), |
|||
this.entitiesTableConfig.deleteEntityContent(entity), |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes'), |
|||
true |
|||
).subscribe((result) => { |
|||
if (result) { |
|||
this.entitiesTableConfig.deleteEntity(entity.id).subscribe( |
|||
() => { |
|||
this.router.navigate(['../'], {relativeTo: this.route}); |
|||
} |
|||
); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* 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{ |
|||
.mat-icon-button a { |
|||
border-bottom: none; |
|||
color: inherit; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* 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{ |
|||
.mat-icon-button a { |
|||
border-bottom: none; |
|||
color: inherit; |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="persistentFormGroup" (ngSubmit)="save()" style="min-width: 480px; max-width: 600px;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ 'widgets.persistent-table.add-title' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="close()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content class="add-dialog"> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<div fxLayout="row" fxLayoutGap="6px"> |
|||
<mat-slide-toggle fxFlex class="mat-block" formControlName="oneWayElseTwoWay"> |
|||
{{ rpcMessageTypeText }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutGap="6px"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>widgets.persistent-table.method</mat-label> |
|||
<input matInput formControlName="method" required> |
|||
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('required')"> |
|||
{{'widgets.persistent-table.method-error' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('pattern')"> |
|||
{{'widgets.persistent-table.white-space-error' | translate}} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>widgets.persistent-table.retries</mat-label> |
|||
<input matInput type="number" formControlName="retries"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="params-json-editor"> |
|||
<tb-json-object-edit formControlName="params" |
|||
[editorStyle]="{minHeight: '130px'}" |
|||
label="{{ 'widgets.persistent-table.params' | translate }}"> |
|||
</tb-json-object-edit> |
|||
</div> |
|||
<mat-expansion-panel class="additional-json-editor"> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate> |
|||
widgets.persistent-table.additional-info |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<tb-json-object-edit formControlName="additionalInfo" |
|||
[editorStyle]="{minHeight: '130px'}"> |
|||
</tb-json-object-edit> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</fieldset> |
|||
</div> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="close()"> |
|||
{{ 'action.close' | translate }} |
|||
</button> |
|||
<span fxFlex></span> |
|||
<div fxLayout="row" fxLayoutGap="8px"> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || persistentFormGroup.invalid || !persistentFormGroup.dirty"> |
|||
{{ 'widgets.persistent-table.send-request' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host ::ng-deep { |
|||
.add-dialog { |
|||
|
|||
.params-json-editor, |
|||
.additional-json-editor { |
|||
.tb-json-object-panel { |
|||
margin: 0 0 16px; |
|||
} |
|||
|
|||
.mat-expansion-panel-body { |
|||
padding-bottom: 0 !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { RequestData } from '@shared/models/rpc.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-persistent-add-dialog', |
|||
templateUrl: './persistent-add-dialog.component.html', |
|||
styleUrls: ['./persistent-add-dialog.component.scss'] |
|||
}) |
|||
|
|||
export class PersistentAddDialogComponent extends DialogComponent<PersistentAddDialogComponent, RequestData> implements OnInit { |
|||
|
|||
public persistentFormGroup: FormGroup; |
|||
public rpcMessageTypeText: string; |
|||
|
|||
private requestData: RequestData = null; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
public dialogRef: MatDialogRef<PersistentAddDialogComponent, RequestData>, |
|||
private fb: FormBuilder, |
|||
private translate: TranslateService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.persistentFormGroup = this.fb.group( |
|||
{ |
|||
method: ['', [Validators.required, Validators.pattern(/^\S+$/)]], |
|||
oneWayElseTwoWay: [false], |
|||
retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]], |
|||
params: [null], |
|||
additionalInfo: [null] |
|||
} |
|||
); |
|||
} |
|||
|
|||
save() { |
|||
this.requestData = this.persistentFormGroup.value; |
|||
this.close(); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.rpcMessageTypeText = this.translate.instant('widgets.persistent-table.message-types.false'); |
|||
this.persistentFormGroup.get('oneWayElseTwoWay').valueChanges.subscribe( |
|||
() => { |
|||
this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`); |
|||
} |
|||
); |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(this.requestData); |
|||
} |
|||
} |
|||
@ -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,34 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host ::ng-deep { |
|||
.rpc-dialog { |
|||
.mat-expansion-panel-body { |
|||
padding-bottom: 0 !important; |
|||
} |
|||
|
|||
.tb-json-object-panel { |
|||
margin: 0 0 16px 0; |
|||
} |
|||
} |
|||
|
|||
.tb-audit-log-response-data { |
|||
width: 100%; |
|||
min-width: 400px; |
|||
height: 100%; |
|||
min-height: 100px; |
|||
border: 1px solid #c0c0c0; |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
|
|||
export interface PersistentDetailsDialogData { |
|||
persistentRequest: PersistentRpc; |
|||
allowDelete: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-persistent-details-dialog', |
|||
templateUrl: './persistent-details-dialog.component.html', |
|||
styleUrls: ['./persistent-details-dialog.component.scss'] |
|||
}) |
|||
|
|||
export class PersistentDetailsDialogComponent extends DialogComponent<PersistentDetailsDialogComponent, boolean> implements OnInit { |
|||
|
|||
@ViewChild('responseDataEditor', {static: true}) |
|||
responseDataEditorElmRef: ElementRef; |
|||
|
|||
public persistentFormGroup: FormGroup; |
|||
public rpcStatusColorsMap = rpcStatusColors; |
|||
public rpcStatus = RpcStatus; |
|||
public allowDelete: boolean; |
|||
|
|||
private persistentUpdated = false; |
|||
private responseData: string; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
private datePipe: DatePipe, |
|||
private translate: TranslateService, |
|||
@Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData, |
|||
public dialogRef: MatDialogRef<PersistentDetailsDialogComponent, boolean>, |
|||
private dialogService: DialogService, |
|||
private deviceService: DeviceService, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.allowDelete = data.allowDelete; |
|||
|
|||
this.persistentFormGroup = this.fb.group( |
|||
{ |
|||
rpcId: [''], |
|||
createdTime: [''], |
|||
expirationTime: [''], |
|||
messageType: [''], |
|||
status: [''], |
|||
method: [''], |
|||
params: [''], |
|||
retries: [''], |
|||
response: [''], |
|||
additionalInfo: [''] |
|||
} |
|||
); |
|||
this.loadPersistentFields(data.persistentRequest); |
|||
this.responseData = JSON.stringify(data.persistentRequest.response, null, 2); |
|||
} |
|||
|
|||
loadPersistentFields(request: PersistentRpc) { |
|||
this.persistentFormGroup.patchValue({ |
|||
rpcId: this.translate.instant('widgets.persistent-table.details-title') + request.id.id, |
|||
createdTime: this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss'), |
|||
expirationTime: this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss'), |
|||
messageType: this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway), |
|||
status: this.translate.instant(rpcStatusTranslation.get(request.status)), |
|||
method: request.request.body.method, |
|||
retries: request.request.retries || null, |
|||
response: request.response || null, |
|||
params: JSON.parse(request.request.body.params) || null, |
|||
additionalInfo: request.additionalInfo || null |
|||
}, {emitEvent: false}); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(this.persistentUpdated); |
|||
} |
|||
|
|||
deleteRpcRequest() { |
|||
const persistentRpc = this.data.persistentRequest; |
|||
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { |
|||
this.dialogService.confirm( |
|||
this.translate.instant('widgets.persistent-table.delete-request-title'), |
|||
this.translate.instant('widgets.persistent-table.delete-request-text'), |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes') |
|||
).subscribe((res) => { |
|||
if (res) { |
|||
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { |
|||
this.persistentUpdated = true; |
|||
this.close(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="persistentFilterFormGroup" (ngSubmit)="update()"> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|||
<mat-label translate>widgets.persistent-table.rpc-status-list</mat-label> |
|||
<mat-select formControlName="rpcStatus" |
|||
placeholder="{{ rpcSearchPlaceholder }}"> |
|||
<mat-option [value]="null"> |
|||
{{ 'widgets.persistent-table.rpc-search-status-all' | translate }} |
|||
</mat-option> |
|||
<mat-option *ngFor="let searchStatus of persistentSearchStatuses" [value]="searchStatus"> |
|||
{{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
|||
<button type="button" |
|||
mat-button |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button type="submit" |
|||
mat-raised-button |
|||
color="primary" |
|||
[disabled]="persistentFilterFormGroup.invalid || !persistentFilterFormGroup.dirty"> |
|||
{{ 'action.update' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
min-width: 300px; |
|||
overflow: hidden; |
|||
background: #fff; |
|||
border-radius: 4px; |
|||
box-shadow: |
|||
0 7px 8px -4px rgba(0, 0, 0, .2), |
|||
0 13px 19px 2px rgba(0, 0, 0, .14), |
|||
0 5px 24px 4px rgba(0, 0, 0, .12); |
|||
|
|||
.mat-content { |
|||
overflow: hidden; |
|||
background-color: #fff; |
|||
} |
|||
|
|||
.mat-padding { |
|||
padding: 16px; |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, Inject, InjectionToken } from '@angular/core'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { OverlayRef } from '@angular/cdk/overlay'; |
|||
import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmFilterPanelData'); |
|||
|
|||
export interface PersistentFilterPanelData { |
|||
rpcStatus: RpcStatus; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-persistent-filter-panel', |
|||
templateUrl: './persistent-filter-panel.component.html', |
|||
styleUrls: ['./persistent-filter-panel.component.scss'] |
|||
}) |
|||
export class PersistentFilterPanelComponent { |
|||
|
|||
public persistentFilterFormGroup: FormGroup; |
|||
public result: PersistentFilterPanelData; |
|||
public rpcSearchStatusTranslationMap = rpcStatusTranslation; |
|||
public rpcSearchPlaceholder: string; |
|||
|
|||
public persistentSearchStatuses = Object.keys(RpcStatus); |
|||
|
|||
constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) |
|||
public data: PersistentFilterPanelData, |
|||
public overlayRef: OverlayRef, |
|||
private fb: FormBuilder, |
|||
private translate: TranslateService) { |
|||
this.persistentFilterFormGroup = this.fb.group( |
|||
{ |
|||
rpcStatus: this.data.rpcStatus |
|||
} |
|||
); |
|||
this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status'); |
|||
} |
|||
|
|||
update() { |
|||
this.result = { |
|||
rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value |
|||
}; |
|||
this.overlayRef.dispose(); |
|||
} |
|||
|
|||
cancel() { |
|||
this.overlayRef.dispose(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,126 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-table-widget tb-absolute-fill"> |
|||
<div fxFlex fxLayout="column" class="tb-absolute-fill"> |
|||
<div fxFlex class="table-container"> |
|||
<table mat-table [dataSource]="persistentDatasource" |
|||
matSort [matSortActive]="pageLink.sortOrder.property" |
|||
[matSortDirection]="pageLink.sortDirection()" matSortDisableClear> |
|||
<ng-container matColumnDef="rpcId"> |
|||
<mat-header-cell *matHeaderCellDef class="column-id"> |
|||
{{ 'widgets.persistent-table.rpc-id' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{ column.id.id }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="createdTime"> |
|||
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header> |
|||
{{ 'widgets.persistent-table.created-time' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="expirationTime"> |
|||
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header> |
|||
{{ 'widgets.persistent-table.expiration-time' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="status"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> |
|||
{{ 'widgets.persistent-table.status' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column" |
|||
[ngStyle]="{fontWeight: 'bold', color: rpcStatusColor.get((column.status))}"> |
|||
{{ rpcStatusTranslation.get(column.status) | translate }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="messageType"> |
|||
<mat-header-cell *matHeaderCellDef> |
|||
{{ 'widgets.persistent-table.message-type' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="method"> |
|||
<mat-header-cell *matHeaderCellDef> |
|||
{{ 'widgets.persistent-table.method' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{ column.request.body.method }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> |
|||
<mat-header-cell *matHeaderCellDef> |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
<div fxHide fxShow.gt-md fxLayout="row" fxLayoutAlign="end"> |
|||
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction"> |
|||
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
|||
matTooltip="{{ actionDescriptor.displayName }}" |
|||
matTooltipPosition="above" |
|||
(click)="onActionButtonClick($event, column, actionDescriptor)"> |
|||
<mat-icon>{{ actionDescriptor.icon }}</mat-icon> |
|||
</button> |
|||
</ng-container> |
|||
</div> |
|||
<div fxHide fxShow.lt-lg *ngIf="actionCellButtonAction.length"> |
|||
<button mat-button mat-icon-button |
|||
(click)="$event.stopPropagation(); ctx.detectChanges();" |
|||
[matMenuTriggerFor]="cellActionsMenu"> |
|||
<mat-icon class="material-icons">more_vert</mat-icon> |
|||
</button> |
|||
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
|||
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction"> |
|||
<button mat-menu-item *ngIf="actionDescriptor.icon" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onActionButtonClick($event, column, actionDescriptor)"> |
|||
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
|||
<span>{{ actionDescriptor.displayName }}</span> |
|||
</button> |
|||
</ng-container> |
|||
</mat-menu> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row> |
|||
<mat-row *matRowDef="let column; columns: displayedColumns" |
|||
[fxShow]="!persistentDatasource.dataLoading"></mat-row> |
|||
</table> |
|||
<span [fxShow]="(persistentDatasource.isEmpty() | async) && !persistentDatasource.dataLoading" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found">{{ noDataDisplayMessageText }}</span> |
|||
<span [fxShow]="persistentDatasource.dataLoading" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found">{{ 'common.loading' | translate }}</span> |
|||
</div> |
|||
<mat-divider *ngIf="displayPagination"></mat-divider> |
|||
<mat-paginator *ngIf="displayPagination" |
|||
[length]="persistentDatasource.total() | async" |
|||
[pageIndex]="pageLink.page" |
|||
[pageSize]="pageLink.pageSize" |
|||
[pageSizeOptions]="pageSizeOptions" |
|||
[hidePageSize]="hidePageSize" |
|||
showFirstLastButtons></mat-paginator> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,53 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
.tb-table-widget { |
|||
.table-container { |
|||
position: relative; |
|||
} |
|||
|
|||
.mat-table { |
|||
.mat-row { |
|||
&.invisible { |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
} |
|||
|
|||
span.no-data-found { |
|||
position: absolute; |
|||
top: 60px; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
} |
|||
|
|||
.column-id { |
|||
min-width: 250px; |
|||
max-width: 250px; |
|||
width: 250px; |
|||
} |
|||
|
|||
.column-time { |
|||
min-width: 120px; |
|||
max-width: 120px; |
|||
width: 120px; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,580 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
Injector, |
|||
Input, |
|||
OnInit, |
|||
StaticProvider, |
|||
ViewChild, |
|||
ViewContainerRef |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { WidgetConfig } from '@shared/models/widget.models'; |
|||
import { IWidgetSubscription } from '@core/api/widget-api.models'; |
|||
import { BehaviorSubject, merge, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; |
|||
import { catchError, map, tap } from 'rxjs/operators'; |
|||
import { |
|||
constructTableCssString, |
|||
noDataMessage, |
|||
TableCellButtonActionDescriptor, |
|||
TableWidgetSettings |
|||
} from '@home/components/widget/lib/table-widget.models'; |
|||
import cssjs from '@core/css/css'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { hashCode, isDefined, isNumber } from '@core/utils'; |
|||
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
|||
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
|||
import { |
|||
PersistentRpc, |
|||
PersistentRpcData, |
|||
RequestData, |
|||
RpcStatus, |
|||
rpcStatusColors, |
|||
rpcStatusTranslation |
|||
} from '@shared/models/rpc.models'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; |
|||
import { MatPaginator } from '@angular/material/paginator'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
PersistentDetailsDialogComponent, |
|||
PersistentDetailsDialogData |
|||
} from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; |
|||
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
|||
import { ComponentPortal } from '@angular/cdk/portal'; |
|||
import { |
|||
PERSISTENT_FILTER_PANEL_DATA, |
|||
PersistentFilterPanelComponent, |
|||
PersistentFilterPanelData |
|||
} from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; |
|||
import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { hidePageSizePixelValue } from '@shared/models/constants'; |
|||
import { HttpErrorResponse } from '@angular/common/http'; |
|||
|
|||
interface PersistentTableWidgetSettings extends TableWidgetSettings { |
|||
defaultSortOrder: string; |
|||
defaultPageSize: number; |
|||
displayPagination: boolean; |
|||
enableStickyAction: boolean; |
|||
enableStickyHeader: boolean; |
|||
enableFilter: boolean; |
|||
displayColumns: string[]; |
|||
displayDetails: boolean; |
|||
allowDelete: boolean; |
|||
allowSendRequest: boolean; |
|||
} |
|||
|
|||
interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor { |
|||
details?: boolean; |
|||
delete?: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-persistent-table-widget', |
|||
templateUrl: './persistent-table.component.html', |
|||
styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] |
|||
}) |
|||
|
|||
export class PersistentTableComponent extends PageComponent implements OnInit { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@ViewChild(MatPaginator) paginator: MatPaginator; |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
private settings: PersistentTableWidgetSettings; |
|||
private widgetConfig: WidgetConfig; |
|||
private subscription: IWidgetSubscription; |
|||
private enableFilterAction = true; |
|||
private allowSendRequest = true; |
|||
private defaultPageSize = 10; |
|||
private defaultSortOrder = '-createdTime'; |
|||
private rpcStatusFilter: RpcStatus; |
|||
private displayDetails = true; |
|||
private allowDelete = true; |
|||
private displayTableColumns: string[]; |
|||
private widgetResize$: ResizeObserver; |
|||
|
|||
public persistentDatasource: PersistentDatasource; |
|||
public noDataDisplayMessageText: string; |
|||
public rpcStatusColor = rpcStatusColors; |
|||
public rpcStatusTranslation = rpcStatusTranslation; |
|||
public displayPagination = true; |
|||
public enableStickyHeader = true; |
|||
public enableStickyAction = true; |
|||
public pageLink: PageLink; |
|||
public pageSizeOptions; |
|||
public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; |
|||
public displayedColumns: string[]; |
|||
public hidePageSize = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private elementRef: ElementRef, |
|||
private overlay: Overlay, |
|||
private viewContainerRef: ViewContainerRef, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private dialogService: DialogService, |
|||
private deviceService: DeviceService, |
|||
private dialog: MatDialog, |
|||
private cd: ChangeDetectorRef) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.ctx.$scope.persistentTableWidget = this; |
|||
this.settings = this.ctx.settings; |
|||
this.widgetConfig = this.ctx.widgetConfig; |
|||
this.subscription = this.ctx.defaultSubscription; |
|||
this.initializeConfig(); |
|||
this.ctx.updateWidgetParams(); |
|||
if (this.displayPagination) { |
|||
this.widgetResize$ = new ResizeObserver(() => { |
|||
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; |
|||
if (showHidePageSize !== this.hidePageSize) { |
|||
this.hidePageSize = showHidePageSize; |
|||
this.cd.markForCheck(); |
|||
} |
|||
}); |
|||
this.widgetResize$.observe(this.elementRef.nativeElement); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
if (this.widgetResize$) { |
|||
this.widgetResize$.disconnect(); |
|||
} |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
if (this.displayPagination) { |
|||
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
|||
} |
|||
((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable<any>) |
|||
.pipe( |
|||
tap(() => this.updateData()) |
|||
) |
|||
.subscribe(); |
|||
this.updateData(); |
|||
} |
|||
|
|||
private initializeConfig() { |
|||
|
|||
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; |
|||
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; |
|||
this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : []; |
|||
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; |
|||
this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; |
|||
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; |
|||
this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true; |
|||
this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true; |
|||
|
|||
this.noDataDisplayMessageText = |
|||
noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate); |
|||
|
|||
this.displayedColumns = [...this.displayTableColumns]; |
|||
|
|||
const pageSize = this.settings.defaultPageSize; |
|||
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { |
|||
this.defaultPageSize = pageSize; |
|||
} |
|||
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; |
|||
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { |
|||
this.defaultSortOrder = this.settings.defaultSortOrder; |
|||
} |
|||
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); |
|||
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); |
|||
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; |
|||
|
|||
|
|||
this.ctx.widgetActions = [ |
|||
{ |
|||
name: 'widgets.persistent-table.add', |
|||
show: this.allowSendRequest, |
|||
icon: 'add', |
|||
onAction: $event => this.addPersistentRpcRequest($event) |
|||
}, |
|||
{ |
|||
name: 'widgets.persistent-table.refresh', |
|||
show: true, |
|||
icon: 'refresh', |
|||
onAction: () => this.reloadPersistentRequests() |
|||
}, |
|||
{ |
|||
name: 'widgets.persistent-table.filter', |
|||
show: this.enableFilterAction, |
|||
icon: 'filter_list', |
|||
onAction: $event => this.editFilter($event) |
|||
} |
|||
]; |
|||
|
|||
if (this.settings.displayDetails) { |
|||
this.actionCellButtonAction.push( |
|||
{ |
|||
displayName: this.translate.instant('widgets.persistent-table.details'), |
|||
icon: 'more_horiz', |
|||
details: true |
|||
} as PersistentTableWidgetActionDescriptor |
|||
); |
|||
} |
|||
if (this.settings.allowDelete) { |
|||
this.actionCellButtonAction.push( |
|||
{ |
|||
displayName: this.translate.instant('widgets.persistent-table.delete'), |
|||
icon: 'delete', |
|||
delete: true |
|||
} as PersistentTableWidgetActionDescriptor |
|||
); |
|||
} |
|||
if (this.actionCellButtonAction.length) { |
|||
this.displayedColumns.push('actions'); |
|||
} |
|||
|
|||
this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx); |
|||
|
|||
const cssString = constructTableCssString(this.widgetConfig); |
|||
const cssParser = new cssjs(); |
|||
cssParser.testMode = false; |
|||
const namespace = 'persistent-table-' + hashCode(cssString); |
|||
cssParser.cssPreviewNamespace = namespace; |
|||
cssParser.createStyleElement(namespace, cssString); |
|||
$(this.elementRef.nativeElement).addClass(namespace); |
|||
} |
|||
|
|||
private updateData() { |
|||
if (this.displayPagination) { |
|||
this.pageLink.page = this.paginator.pageIndex; |
|||
this.pageLink.pageSize = this.paginator.pageSize; |
|||
} else { |
|||
this.pageLink.page = 0; |
|||
} |
|||
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { |
|||
this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); |
|||
} |
|||
this.pageLink.sortOrder.property = this.sort.active; |
|||
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
|||
this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter); |
|||
this.ctx.detectChanges(); |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
this.ctx.detectChanges(); |
|||
} |
|||
|
|||
reloadPersistentRequests() { |
|||
if (this.displayPagination) { |
|||
this.paginator.pageIndex = 0; |
|||
} |
|||
this.updateData(); |
|||
} |
|||
|
|||
deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { |
|||
this.dialogService.confirm( |
|||
this.translate.instant('widgets.persistent-table.delete-request-title'), |
|||
this.translate.instant('widgets.persistent-table.delete-request-text'), |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes') |
|||
).subscribe((res) => { |
|||
if (res) { |
|||
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { |
|||
this.reloadPersistentRequests(); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
openRequestDetails($event: Event, persistentRpc: PersistentRpc) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { |
|||
this.dialog.open<PersistentDetailsDialogComponent, PersistentDetailsDialogData, boolean> |
|||
(PersistentDetailsDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
persistentRequest: persistentRpc, |
|||
allowDelete: this.allowDelete |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
this.reloadPersistentRequests(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
addPersistentRpcRequest($event: Event){ |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialog.open<PersistentAddDialogComponent, RequestData> |
|||
(PersistentAddDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe( |
|||
(requestData) => { |
|||
if (requestData) { |
|||
this.sendRequests(requestData); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private sendRequests(requestData: RequestData) { |
|||
let commandPromise; |
|||
if (requestData.oneWayElseTwoWay) { |
|||
commandPromise = this.ctx.controlApi.sendOneWayCommand( |
|||
requestData.method, |
|||
requestData.params, null, |
|||
true, null, |
|||
requestData.retries, |
|||
requestData.additionalInfo |
|||
); |
|||
} else { |
|||
commandPromise = this.ctx.controlApi.sendTwoWayCommand( |
|||
requestData.method, |
|||
requestData.params, |
|||
null, |
|||
true, null, |
|||
requestData.retries, |
|||
requestData.additionalInfo |
|||
); |
|||
} |
|||
commandPromise.subscribe( |
|||
() => { |
|||
this.reloadPersistentRequests(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) { |
|||
if (actionDescriptor.details) { |
|||
this.openRequestDetails($event, persistentRpc); |
|||
} |
|||
if (actionDescriptor.delete) { |
|||
this.deleteRpcRequest($event, persistentRpc); |
|||
} |
|||
} |
|||
|
|||
private editFilter($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const target = $event.target || $event.srcElement || $event.currentTarget; |
|||
const config = new OverlayConfig(); |
|||
config.backdropClass = 'cdk-overlay-transparent-backdrop'; |
|||
config.hasBackdrop = true; |
|||
const connectedPosition: ConnectedPosition = { |
|||
originX: 'end', |
|||
originY: 'bottom', |
|||
overlayX: 'end', |
|||
overlayY: 'top' |
|||
}; |
|||
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) |
|||
.withPositions([connectedPosition]); |
|||
|
|||
const overlayRef = this.overlay.create(config); |
|||
overlayRef.backdropClick().subscribe(() => { |
|||
overlayRef.dispose(); |
|||
}); |
|||
const providers: StaticProvider[] = [ |
|||
{ |
|||
provide: PERSISTENT_FILTER_PANEL_DATA, |
|||
useValue: { |
|||
rpcStatus: this.rpcStatusFilter |
|||
} as PersistentFilterPanelData |
|||
}, |
|||
{ |
|||
provide: OverlayRef, |
|||
useValue: overlayRef |
|||
} |
|||
]; |
|||
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); |
|||
const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent, |
|||
this.viewContainerRef, injector)); |
|||
componentRef.onDestroy(() => { |
|||
if (componentRef.instance.result) { |
|||
const result = componentRef.instance.result; |
|||
this.rpcStatusFilter = result.rpcStatus; |
|||
this.reloadPersistentRequests(); |
|||
} |
|||
}); |
|||
this.ctx.detectChanges(); |
|||
} |
|||
} |
|||
|
|||
class PersistentDatasource implements DataSource<PersistentRpcData> { |
|||
|
|||
private persistentSubject = new BehaviorSubject<PersistentRpcData[]>([]); |
|||
private pageDataSubject = new BehaviorSubject<PageData<PersistentRpcData>>(emptyPageData<PersistentRpcData>()); |
|||
|
|||
private rpcErrorText: string; |
|||
private executingSubjects: Array<Subject<any>>; |
|||
private executingRpcRequest = false; |
|||
|
|||
public dataLoading = true; |
|||
public pageData$ = this.pageDataSubject.asObservable(); |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private subscription: IWidgetSubscription, |
|||
private ctx: WidgetContext) { |
|||
} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<PersistentRpcData[] | ReadonlyArray<PersistentRpcData>> { |
|||
return this.persistentSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.persistentSubject.complete(); |
|||
this.pageDataSubject.complete(); |
|||
} |
|||
|
|||
reset() { |
|||
const pageData = emptyPageData<PersistentRpcData>(); |
|||
this.persistentSubject.next(pageData.data); |
|||
this.pageDataSubject.next(pageData); |
|||
} |
|||
|
|||
loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) { |
|||
this.dataLoading = true; |
|||
|
|||
const result = new ReplaySubject<PageData<PersistentRpcData>>(); |
|||
this.fetchEntities(pageLink, rpcStatusFilter).pipe( |
|||
catchError(() => of(emptyPageData<PersistentRpcData>())), |
|||
).subscribe( |
|||
(pageData) => { |
|||
this.persistentSubject.next(pageData.data); |
|||
this.pageDataSubject.next(pageData); |
|||
result.next(pageData); |
|||
this.dataLoading = false; |
|||
} |
|||
); |
|||
return result; |
|||
} |
|||
|
|||
fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable<PageData<PersistentRpcData>> { |
|||
if (!this.subscription.rpcEnabled) { |
|||
return throwError(new Error('Rpc disabled!')); |
|||
} else if (!this.subscription.targetDeviceId) { |
|||
return throwError(new Error('Target device is not set!')); |
|||
} |
|||
const rpcSubject: Subject<any> = new Subject<any>(); |
|||
|
|||
this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe( |
|||
(responseBody) => { |
|||
rpcSubject.next(responseBody); |
|||
rpcSubject.complete(); |
|||
}, |
|||
(rejection: HttpErrorResponse) => { |
|||
this.rpcErrorText = null; |
|||
this.executingSubjects = []; |
|||
|
|||
const index = this.executingSubjects.indexOf(rpcSubject); |
|||
if (index >= 0) { |
|||
this.executingSubjects.splice(index, 1); |
|||
} |
|||
this.executingRpcRequest = this.executingSubjects.length > 0; |
|||
this.subscription.options.callbacks.rpcStateChanged(this.subscription); |
|||
if (!this.executingRpcRequest || rejection.status === 504) { |
|||
this.subscription.rpcRejection = rejection; |
|||
if (rejection.status === 504) { |
|||
this.subscription.rpcErrorText = 'Request Timeout.'; |
|||
} else { |
|||
this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; |
|||
const error = this.extractRejectionErrorText(rejection); |
|||
if (error) { |
|||
this.subscription.rpcErrorText += '</br>'; |
|||
this.subscription.rpcErrorText += error.message || ''; |
|||
} |
|||
} |
|||
this.subscription.callbacks.onRpcFailed(this.subscription); |
|||
} |
|||
rpcSubject.error(rejection); |
|||
} |
|||
); |
|||
return rpcSubject.asObservable(); |
|||
} |
|||
|
|||
extractRejectionErrorText(rejection: HttpErrorResponse) { |
|||
let error = null; |
|||
if (rejection.error) { |
|||
error = rejection.error; |
|||
try { |
|||
error = rejection.error ? JSON.parse(rejection.error) : null; |
|||
} catch (e) {} |
|||
} |
|||
if (error && !error.message) { |
|||
error = this.prepareMessageFromData(error); |
|||
} else if (error && error.message) { |
|||
error = error.message; |
|||
} |
|||
return error; |
|||
} |
|||
|
|||
prepareMessageFromData(data) { |
|||
if (typeof data === 'object' && data.constructor === ArrayBuffer) { |
|||
const msg = String.fromCharCode.apply(null, new Uint8Array(data)); |
|||
try { |
|||
const msgObj = JSON.parse(msg); |
|||
if (msgObj.message) { |
|||
return msgObj.message; |
|||
} else { |
|||
return msg; |
|||
} |
|||
} catch (e) { |
|||
return msg; |
|||
} |
|||
} else { |
|||
return data; |
|||
} |
|||
} |
|||
|
|||
isEmpty(): Observable<boolean> { |
|||
return this.persistentSubject.pipe( |
|||
map((requests) => !requests.length) |
|||
); |
|||
} |
|||
|
|||
total(): Observable<number> { |
|||
return this.pageDataSubject.pipe( |
|||
map((pageData) => pageData.totalElements) |
|||
); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue