109 changed files with 4929 additions and 235 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.asset; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.data.util.Pair; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.AssetProfileId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; |
||||
|
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class BaseAssetProcessor extends BaseEdgeProcessor { |
||||
|
|
||||
|
protected Pair<Boolean, Boolean> saveOrUpdateAsset(TenantId tenantId, AssetId assetId, AssetUpdateMsg assetUpdateMsg, CustomerId customerId) { |
||||
|
boolean created = false; |
||||
|
boolean assetNameUpdated = false; |
||||
|
assetCreationLock.lock(); |
||||
|
try { |
||||
|
Asset asset = assetService.findAssetById(tenantId, assetId); |
||||
|
String assetName = assetUpdateMsg.getName(); |
||||
|
if (asset == null) { |
||||
|
created = true; |
||||
|
asset = new Asset(); |
||||
|
asset.setTenantId(tenantId); |
||||
|
asset.setCreatedTime(Uuids.unixTimestamp(assetId.getId())); |
||||
|
} |
||||
|
Asset assetByName = assetService.findAssetByTenantIdAndName(tenantId, assetName); |
||||
|
if (assetByName != null && !assetByName.getId().equals(assetId)) { |
||||
|
assetName = assetName + "_" + StringUtils.randomAlphanumeric(15); |
||||
|
log.warn("Asset with name {} already exists. Renaming asset name to {}", |
||||
|
assetUpdateMsg.getName(), assetName); |
||||
|
assetNameUpdated = true; |
||||
|
} |
||||
|
asset.setName(assetName); |
||||
|
asset.setType(assetUpdateMsg.getType()); |
||||
|
asset.setLabel(assetUpdateMsg.hasLabel() ? assetUpdateMsg.getLabel() : null); |
||||
|
asset.setAdditionalInfo(assetUpdateMsg.hasAdditionalInfo() |
||||
|
? JacksonUtil.toJsonNode(assetUpdateMsg.getAdditionalInfo()) : null); |
||||
|
|
||||
|
UUID assetProfileUUID = safeGetUUID(assetUpdateMsg.getAssetProfileIdMSB(), assetUpdateMsg.getAssetProfileIdLSB()); |
||||
|
asset.setAssetProfileId(assetProfileUUID != null ? new AssetProfileId(assetProfileUUID) : null); |
||||
|
|
||||
|
asset.setCustomerId(customerId); |
||||
|
|
||||
|
assetValidator.validate(asset, Asset::getTenantId); |
||||
|
if (created) { |
||||
|
asset.setId(assetId); |
||||
|
} |
||||
|
assetService.saveAsset(asset, false); |
||||
|
} finally { |
||||
|
assetCreationLock.unlock(); |
||||
|
} |
||||
|
return Pair.of(created, assetNameUpdated); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.asset; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.server.common.data.asset.AssetProfile; |
||||
|
import org.thingsboard.server.common.data.id.AssetProfileId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; |
||||
|
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; |
||||
|
|
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class BaseAssetProfileProcessor extends BaseEdgeProcessor { |
||||
|
|
||||
|
protected boolean saveOrUpdateAssetProfile(TenantId tenantId, AssetProfileId assetProfileId, AssetProfileUpdateMsg assetProfileUpdateMsg) { |
||||
|
boolean created = false; |
||||
|
assetCreationLock.lock(); |
||||
|
try { |
||||
|
AssetProfile assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); |
||||
|
String assetProfileName = assetProfileUpdateMsg.getName(); |
||||
|
if (assetProfile == null) { |
||||
|
created = true; |
||||
|
assetProfile = new AssetProfile(); |
||||
|
assetProfile.setTenantId(tenantId); |
||||
|
assetProfile.setCreatedTime(Uuids.unixTimestamp(assetProfileId.getId())); |
||||
|
} |
||||
|
assetProfile.setName(assetProfileName); |
||||
|
assetProfile.setDefault(assetProfileUpdateMsg.getDefault()); |
||||
|
assetProfile.setDefaultQueueName(assetProfileUpdateMsg.hasDefaultQueueName() ? assetProfileUpdateMsg.getDefaultQueueName() : null); |
||||
|
assetProfile.setDescription(assetProfileUpdateMsg.hasDescription() ? assetProfileUpdateMsg.getDescription() : null); |
||||
|
assetProfile.setImage(assetProfileUpdateMsg.hasImage() |
||||
|
? new String(assetProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); |
||||
|
|
||||
|
UUID defaultRuleChainUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultRuleChainIdMSB(), assetProfileUpdateMsg.getDefaultRuleChainIdLSB()); |
||||
|
assetProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null); |
||||
|
|
||||
|
UUID defaultDashboardUUID = safeGetUUID(assetProfileUpdateMsg.getDefaultDashboardIdMSB(), assetProfileUpdateMsg.getDefaultDashboardIdLSB()); |
||||
|
assetProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); |
||||
|
|
||||
|
assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId); |
||||
|
if (created) { |
||||
|
assetProfile.setId(assetProfileId); |
||||
|
} |
||||
|
assetProfileService.saveAssetProfile(assetProfile, false); |
||||
|
} finally { |
||||
|
assetCreationLock.unlock(); |
||||
|
} |
||||
|
return created; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.dashboard; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.Dashboard; |
||||
|
import org.thingsboard.server.common.data.ShortCustomerInfo; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; |
||||
|
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { |
||||
|
|
||||
|
protected boolean saveOrUpdateDashboard(TenantId tenantId, DashboardId dashboardId, DashboardUpdateMsg dashboardUpdateMsg, CustomerId customerId) { |
||||
|
boolean created = false; |
||||
|
Dashboard dashboard = dashboardService.findDashboardById(tenantId, dashboardId); |
||||
|
if (dashboard == null) { |
||||
|
created = true; |
||||
|
dashboard = new Dashboard(); |
||||
|
dashboard.setTenantId(tenantId); |
||||
|
dashboard.setCreatedTime(Uuids.unixTimestamp(dashboardId.getId())); |
||||
|
} |
||||
|
dashboard.setTitle(dashboardUpdateMsg.getTitle()); |
||||
|
dashboard.setConfiguration(JacksonUtil.toJsonNode(dashboardUpdateMsg.getConfiguration())); |
||||
|
Set<ShortCustomerInfo> assignedCustomers = null; |
||||
|
if (dashboardUpdateMsg.hasAssignedCustomers()) { |
||||
|
assignedCustomers = JacksonUtil.fromString(dashboardUpdateMsg.getAssignedCustomers(), new TypeReference<>() { |
||||
|
}); |
||||
|
dashboard.setAssignedCustomers(assignedCustomers); |
||||
|
} |
||||
|
|
||||
|
dashboardValidator.validate(dashboard, Dashboard::getTenantId); |
||||
|
if (created) { |
||||
|
dashboard.setId(dashboardId); |
||||
|
} |
||||
|
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard, false); |
||||
|
if (assignedCustomers != null && !assignedCustomers.isEmpty()) { |
||||
|
for (ShortCustomerInfo assignedCustomer : assignedCustomers) { |
||||
|
if (assignedCustomer.getCustomerId().equals(customerId)) { |
||||
|
dashboardService.assignDashboardToCustomer(tenantId, dashboardId, assignedCustomer.getCustomerId()); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
unassignCustomersFromDashboard(tenantId, savedDashboard); |
||||
|
} |
||||
|
return created; |
||||
|
} |
||||
|
|
||||
|
private void unassignCustomersFromDashboard(TenantId tenantId, Dashboard dashboard) { |
||||
|
if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) { |
||||
|
for (ShortCustomerInfo assignedCustomer : dashboard.getAssignedCustomers()) { |
||||
|
dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), assignedCustomer.getCustomerId()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.device; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.DeviceProfileProvisionType; |
||||
|
import org.thingsboard.server.common.data.DeviceProfileType; |
||||
|
import org.thingsboard.server.common.data.DeviceTransportType; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceProfileId; |
||||
|
import org.thingsboard.server.common.data.id.OtaPackageId; |
||||
|
import org.thingsboard.server.common.data.id.RuleChainId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; |
||||
|
import org.thingsboard.server.queue.util.DataDecodingEncodingService; |
||||
|
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; |
||||
|
|
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.Optional; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class BaseDeviceProfileProcessor extends BaseEdgeProcessor { |
||||
|
|
||||
|
@Autowired |
||||
|
private DataDecodingEncodingService dataDecodingEncodingService; |
||||
|
|
||||
|
protected boolean saveOrUpdateDeviceProfile(TenantId tenantId, DeviceProfileId deviceProfileId, DeviceProfileUpdateMsg deviceProfileUpdateMsg) { |
||||
|
boolean created = false; |
||||
|
deviceCreationLock.lock(); |
||||
|
try { |
||||
|
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); |
||||
|
if (deviceProfile == null) { |
||||
|
created = true; |
||||
|
deviceProfile = new DeviceProfile(); |
||||
|
deviceProfile.setTenantId(tenantId); |
||||
|
deviceProfile.setCreatedTime(Uuids.unixTimestamp(deviceProfileId.getId())); |
||||
|
} |
||||
|
deviceProfile.setName(deviceProfileUpdateMsg.getName()); |
||||
|
deviceProfile.setDescription(deviceProfileUpdateMsg.hasDescription() ? deviceProfileUpdateMsg.getDescription() : null); |
||||
|
deviceProfile.setDefault(deviceProfileUpdateMsg.getDefault()); |
||||
|
deviceProfile.setType(DeviceProfileType.valueOf(deviceProfileUpdateMsg.getType())); |
||||
|
deviceProfile.setTransportType(deviceProfileUpdateMsg.hasTransportType() |
||||
|
? DeviceTransportType.valueOf(deviceProfileUpdateMsg.getTransportType()) : DeviceTransportType.DEFAULT); |
||||
|
deviceProfile.setImage(deviceProfileUpdateMsg.hasImage() |
||||
|
? new String(deviceProfileUpdateMsg.getImage().toByteArray(), StandardCharsets.UTF_8) : null); |
||||
|
deviceProfile.setProvisionType(deviceProfileUpdateMsg.hasProvisionType() |
||||
|
? DeviceProfileProvisionType.valueOf(deviceProfileUpdateMsg.getProvisionType()) : DeviceProfileProvisionType.DISABLED); |
||||
|
deviceProfile.setProvisionDeviceKey(deviceProfileUpdateMsg.hasProvisionDeviceKey() |
||||
|
? deviceProfileUpdateMsg.getProvisionDeviceKey() : null); |
||||
|
deviceProfile.setDefaultQueueName(deviceProfileUpdateMsg.getDefaultQueueName()); |
||||
|
|
||||
|
Optional<DeviceProfileData> profileDataOpt = |
||||
|
dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); |
||||
|
deviceProfile.setProfileData(profileDataOpt.orElse(null)); |
||||
|
|
||||
|
UUID defaultRuleChainUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB(), deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()); |
||||
|
deviceProfile.setDefaultRuleChainId(defaultRuleChainUUID != null ? new RuleChainId(defaultRuleChainUUID) : null); |
||||
|
|
||||
|
UUID defaultDashboardUUID = safeGetUUID(deviceProfileUpdateMsg.getDefaultDashboardIdMSB(), deviceProfileUpdateMsg.getDefaultDashboardIdLSB()); |
||||
|
deviceProfile.setDefaultDashboardId(defaultDashboardUUID != null ? new DashboardId(defaultDashboardUUID) : null); |
||||
|
|
||||
|
String defaultQueueName = StringUtils.isNotBlank(deviceProfileUpdateMsg.getDefaultQueueName()) |
||||
|
? deviceProfileUpdateMsg.getDefaultQueueName() : null; |
||||
|
deviceProfile.setDefaultQueueName(defaultQueueName); |
||||
|
|
||||
|
UUID firmwareUUID = safeGetUUID(deviceProfileUpdateMsg.getFirmwareIdMSB(), deviceProfileUpdateMsg.getFirmwareIdLSB()); |
||||
|
deviceProfile.setFirmwareId(firmwareUUID != null ? new OtaPackageId(firmwareUUID) : null); |
||||
|
|
||||
|
UUID softwareUUID = safeGetUUID(deviceProfileUpdateMsg.getSoftwareIdMSB(), deviceProfileUpdateMsg.getSoftwareIdLSB()); |
||||
|
deviceProfile.setSoftwareId(softwareUUID != null ? new OtaPackageId(softwareUUID) : null); |
||||
|
|
||||
|
|
||||
|
deviceProfileValidator.validate(deviceProfile, DeviceProfile::getTenantId); |
||||
|
if (created) { |
||||
|
deviceProfile.setId(deviceProfileId); |
||||
|
} |
||||
|
deviceProfileService.saveDeviceProfile(deviceProfile, false); |
||||
|
} finally { |
||||
|
deviceCreationLock.unlock(); |
||||
|
} |
||||
|
return created; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.entityview; |
||||
|
|
||||
|
import com.datastax.oss.driver.api.core.uuid.Uuids; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.data.util.Pair; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityViewId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.gen.edge.v1.EdgeEntityType; |
||||
|
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; |
||||
|
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class BaseEntityViewProcessor extends BaseEdgeProcessor { |
||||
|
|
||||
|
protected Pair<Boolean, Boolean> saveOrUpdateEntityView(TenantId tenantId, EntityViewId entityViewId, EntityViewUpdateMsg entityViewUpdateMsg, CustomerId customerId) { |
||||
|
boolean created = false; |
||||
|
boolean entityViewNameUpdated = false; |
||||
|
EntityView entityView = entityViewService.findEntityViewById(tenantId, entityViewId); |
||||
|
String entityViewName = entityViewUpdateMsg.getName(); |
||||
|
if (entityView == null) { |
||||
|
created = true; |
||||
|
entityView = new EntityView(); |
||||
|
entityView.setTenantId(tenantId); |
||||
|
entityView.setCreatedTime(Uuids.unixTimestamp(entityViewId.getId())); |
||||
|
} |
||||
|
EntityView entityViewByName = entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName); |
||||
|
if (entityViewByName != null && !entityViewByName.getId().equals(entityViewId)) { |
||||
|
entityViewName = entityViewName + "_" + StringUtils.randomAlphanumeric(15); |
||||
|
log.warn("Entity view with name {} already exists. Renaming entity view name to {}", |
||||
|
entityViewUpdateMsg.getName(), entityViewName); |
||||
|
entityViewNameUpdated = true; |
||||
|
} |
||||
|
entityView.setName(entityViewName); |
||||
|
entityView.setType(entityViewUpdateMsg.getType()); |
||||
|
entityView.setCustomerId(customerId); |
||||
|
entityView.setAdditionalInfo(entityViewUpdateMsg.hasAdditionalInfo() ? |
||||
|
JacksonUtil.toJsonNode(entityViewUpdateMsg.getAdditionalInfo()) : null); |
||||
|
|
||||
|
UUID entityIdUUID = safeGetUUID(entityViewUpdateMsg.getEntityIdMSB(), entityViewUpdateMsg.getEntityIdLSB()); |
||||
|
if (EdgeEntityType.DEVICE.equals(entityViewUpdateMsg.getEntityType())) { |
||||
|
entityView.setEntityId(entityIdUUID != null ? new DeviceId(entityIdUUID) : null); |
||||
|
} else if (EdgeEntityType.ASSET.equals(entityViewUpdateMsg.getEntityType())) { |
||||
|
entityView.setEntityId(entityIdUUID != null ? new AssetId(entityIdUUID) : null); |
||||
|
} |
||||
|
|
||||
|
entityViewValidator.validate(entityView, EntityView::getTenantId); |
||||
|
if (created) { |
||||
|
entityView.setId(entityViewId); |
||||
|
} |
||||
|
entityViewService.saveEntityView(entityView, false); |
||||
|
return Pair.of(created, entityViewNameUpdated); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.common.data.notification.settings; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
||||
|
import org.thingsboard.server.common.data.notification.NotificationType; |
||||
|
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; |
||||
|
|
||||
|
import javax.validation.Valid; |
||||
|
import javax.validation.constraints.AssertTrue; |
||||
|
import javax.validation.constraints.NotNull; |
||||
|
import java.util.Collections; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Data |
||||
|
public class UserNotificationSettings { |
||||
|
|
||||
|
@NotNull |
||||
|
@Valid |
||||
|
private final Map<NotificationType, NotificationPref> prefs; |
||||
|
|
||||
|
public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); |
||||
|
|
||||
|
public static final Set<NotificationDeliveryMethod> deliveryMethods = NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods(); |
||||
|
|
||||
|
@JsonCreator |
||||
|
public UserNotificationSettings(@JsonProperty("prefs") Map<NotificationType, NotificationPref> prefs) { |
||||
|
this.prefs = prefs; |
||||
|
} |
||||
|
|
||||
|
public boolean isEnabled(NotificationType notificationType, NotificationDeliveryMethod deliveryMethod) { |
||||
|
NotificationPref pref = prefs.get(notificationType); |
||||
|
if (pref == null) { |
||||
|
return true; |
||||
|
} |
||||
|
if (!pref.isEnabled()) { |
||||
|
return false; |
||||
|
} |
||||
|
return pref.getEnabledDeliveryMethods().getOrDefault(deliveryMethod, true); |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
public static class NotificationPref { |
||||
|
private boolean enabled; |
||||
|
@NotNull |
||||
|
private Map<NotificationDeliveryMethod, Boolean> enabledDeliveryMethods; |
||||
|
|
||||
|
public static NotificationPref createDefault() { |
||||
|
NotificationPref pref = new NotificationPref(); |
||||
|
pref.setEnabled(true); |
||||
|
pref.setEnabledDeliveryMethods(deliveryMethods.stream().collect(Collectors.toMap(v -> v, v -> true))); |
||||
|
return pref; |
||||
|
} |
||||
|
|
||||
|
@JsonIgnore |
||||
|
@AssertTrue(message = "Only email, Web and SMS delivery methods are allowed") |
||||
|
public boolean isValid() { |
||||
|
return enabledDeliveryMethods.entrySet().stream() |
||||
|
.allMatch(entry -> deliveryMethods.contains(entry.getKey()) && entry.getValue() != null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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 [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-aggregated-data-key-row"> |
||||
|
<mat-form-field class="tb-inline-field tb-position-field" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="position"> |
||||
|
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position"> |
||||
|
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="tb-inline-field tb-aggregation-field" subscriptSizing="dynamic"> |
||||
|
<mat-chip-grid #chipList> |
||||
|
<mat-chip-row class="tb-datakey-chip"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"> |
||||
|
<div class="tb-chip-labels"> |
||||
|
<div class="tb-chip-label tb-chip-text"> |
||||
|
<ng-container *ngTemplateOutlet="keyName"></ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
(click)="editKey()" mat-icon-button class="tb-mat-24"> |
||||
|
<mat-icon class="tb-mat-18">edit</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-chip-row> |
||||
|
</mat-chip-grid> |
||||
|
<input matInput |
||||
|
type="text" |
||||
|
fxHide |
||||
|
readonly |
||||
|
[matChipInputFor]="chipList" |
||||
|
/> |
||||
|
</mat-form-field> |
||||
|
<div class="tb-units-field"> |
||||
|
<tb-unit-input formControlName="units"> |
||||
|
</tb-unit-input> |
||||
|
</div> |
||||
|
<div class="tb-decimals-field"> |
||||
|
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-font-field"> |
||||
|
<tb-font-settings formControlName="font" |
||||
|
[previewText]="valuePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
</div> |
||||
|
<div class="tb-color-field"> |
||||
|
<tb-color-settings formControlName="color"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-table-row-cell-buttons"> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
(click)="keyRemoved.emit()" |
||||
|
matTooltip="{{ 'widgets.aggregated-value-card.remove-value' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #keyName> |
||||
|
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName"> |
||||
|
<strong><span>f(</span></strong><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><strong><span>)</span></strong> |
||||
|
</ng-container> |
||||
|
<ng-template #keyName> |
||||
|
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
<ng-template #keyNameTemplate> |
||||
|
<ng-container> |
||||
|
<strong><span class="tb-agg-func">{{ (modelValue?.aggregationType || aggregationTypes.NONE) }}</span></strong> |
||||
|
<span *ngIf="!modelValue?.aggregationType || modelValue?.aggregationType === aggregationTypes.NONE; else aggValue">({{ 'datakey.latest-value' | translate }})</span> |
||||
|
</ng-container> |
||||
|
<ng-template #aggValue> |
||||
|
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType !== comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta' | translate }}:{{ (modelValue?.comparisonResultType === comparisonResultTypes.DELTA_PERCENT ? 'datakey.percent' : 'datakey.absolute') | translate }})</span> |
||||
|
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType === comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta-calculation-result-previous-value' | translate }})</span> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
@ -0,0 +1,88 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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'; |
||||
|
|
||||
|
.tb-aggregated-data-key-row { |
||||
|
.mat-mdc-form-field.tb-inline-field.tb-aggregation-field { |
||||
|
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { |
||||
|
padding-left: 8px; |
||||
|
padding-right: 0; |
||||
|
.mat-mdc-form-field-infix { |
||||
|
padding-top: 0; |
||||
|
padding-bottom: 6px; |
||||
|
.mdc-evolution-chip-set .mdc-evolution-chip { |
||||
|
margin: 0; |
||||
|
} |
||||
|
input.mat-mdc-chip-input { |
||||
|
height: 32px; |
||||
|
margin-left: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { |
||||
|
.tb-attribute-chip { |
||||
|
.tb-chip-labels { |
||||
|
background: transparent; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-position-field { |
||||
|
width: 132px; |
||||
|
min-width: 132px; |
||||
|
} |
||||
|
|
||||
|
.tb-aggregation-field { |
||||
|
flex: 1; |
||||
|
min-width: 150px; |
||||
|
} |
||||
|
|
||||
|
.tb-units-field, .tb-decimals-field, .tb-font-field, .tb-color-field { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.tb-units-field { |
||||
|
width: 80px; |
||||
|
min-width: 80px; |
||||
|
} |
||||
|
|
||||
|
.tb-decimals-field { |
||||
|
width: 60px; |
||||
|
min-width: 60px; |
||||
|
} |
||||
|
|
||||
|
.tb-font-field { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
|
||||
|
.tb-color-field { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
|
||||
|
.tb-units-field, .tb-decimals-field { |
||||
|
display: none; |
||||
|
@media #{$mat-gt-sm} { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,233 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, |
||||
|
EventEmitter, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
SimpleChanges, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { |
||||
|
ComparisonResultType, |
||||
|
DataKey, |
||||
|
DataKeyConfigMode, |
||||
|
DatasourceType, Widget, |
||||
|
widgetType |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { AggregationType } from '@shared/models/time/time.models'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
||||
|
import { |
||||
|
DataKeyConfigDialogComponent, |
||||
|
DataKeyConfigDialogData |
||||
|
} from '@home/components/widget/config/data-key-config-dialog.component'; |
||||
|
import { deepClone, formatValue } from '@core/utils'; |
||||
|
import { |
||||
|
AggregatedValueCardKeyPosition, |
||||
|
aggregatedValueCardKeyPositionTranslations, |
||||
|
AggregatedValueCardKeySettings |
||||
|
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-aggregated-data-key-row', |
||||
|
templateUrl: './aggregated-data-key-row.component.html', |
||||
|
styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AggregatedDataKeyRowComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { |
||||
|
|
||||
|
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = |
||||
|
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); |
||||
|
|
||||
|
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; |
||||
|
|
||||
|
dataKeyTypes = DataKeyType; |
||||
|
|
||||
|
aggregationTypes = AggregationType; |
||||
|
|
||||
|
comparisonResultTypes = ComparisonResultType; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
datasourceType: DatasourceType; |
||||
|
|
||||
|
@Input() |
||||
|
keyName: string; |
||||
|
|
||||
|
@Output() |
||||
|
keyRemoved = new EventEmitter(); |
||||
|
|
||||
|
keyRowFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
modelValue: DataKey; |
||||
|
|
||||
|
valuePreviewFn = this._valuePreviewFn.bind(this); |
||||
|
|
||||
|
get callbacks(): DataKeysCallbacks { |
||||
|
return this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
} |
||||
|
|
||||
|
get widget(): Widget { |
||||
|
return this.widgetConfigComponent.widget; |
||||
|
} |
||||
|
|
||||
|
get isEntityDatasource(): boolean { |
||||
|
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); |
||||
|
} |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private dialog: MatDialog, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
public translate: TranslateService, |
||||
|
public truncate: TruncatePipe, |
||||
|
private widgetConfigComponent: WidgetConfigComponent) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.keyRowFormGroup = this.fb.group({ |
||||
|
position: [null, []], |
||||
|
units: [null, []], |
||||
|
decimals: [null, []], |
||||
|
font: [null, []], |
||||
|
color: [null, []] |
||||
|
}); |
||||
|
this.keyRowFormGroup.valueChanges.subscribe( |
||||
|
() => this.updateModel() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['keyName'].includes(propName)) { |
||||
|
if (change.currentValue) { |
||||
|
this.modelValue.name = change.currentValue; |
||||
|
setTimeout(() => { |
||||
|
this.updateModel(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.keyRowFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.keyRowFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: DataKey): void { |
||||
|
this.modelValue = value || {} as DataKey; |
||||
|
const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {}); |
||||
|
this.keyRowFormGroup.patchValue( |
||||
|
{ |
||||
|
position: settings.position || AggregatedValueCardKeyPosition.center, |
||||
|
units: value?.units, |
||||
|
decimals: value?.decimals, |
||||
|
font: settings.font, |
||||
|
color: settings.color |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
dataKeyHasPostprocessing(): boolean { |
||||
|
return !!this.modelValue?.postFuncBody; |
||||
|
} |
||||
|
|
||||
|
editKey() { |
||||
|
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
||||
|
{ |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
dataKey: deepClone(this.modelValue), |
||||
|
dataKeyConfigMode: DataKeyConfigMode.general, |
||||
|
dataKeySettingsSchema: null, |
||||
|
dataKeySettingsDirective: null, |
||||
|
dashboard: null, |
||||
|
aliasController: null, |
||||
|
widget: this.widget, |
||||
|
widgetType: widgetType.latest, |
||||
|
deviceId: null, |
||||
|
entityAliasId: null, |
||||
|
showPostProcessing: true, |
||||
|
callbacks: this.callbacks, |
||||
|
hideDataKeyName: true, |
||||
|
hideDataKeyLabel: true, |
||||
|
hideDataKeyColor: true |
||||
|
} |
||||
|
}).afterClosed().subscribe((updatedDataKey) => { |
||||
|
if (updatedDataKey) { |
||||
|
this.modelValue = updatedDataKey; |
||||
|
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); |
||||
|
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value = this.keyRowFormGroup.value; |
||||
|
this.modelValue.settings = this.modelValue.settings || {}; |
||||
|
this.modelValue.settings.position = value.position; |
||||
|
this.modelValue.settings.font = value.font; |
||||
|
this.modelValue.settings.color = value.color; |
||||
|
this.modelValue.units = value.units; |
||||
|
this.modelValue.decimals = value.decimals; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
private _valuePreviewFn(): string { |
||||
|
const units: string = this.keyRowFormGroup.get('units').value; |
||||
|
const decimals: number = this.keyRowFormGroup.get('decimals').value; |
||||
|
return formatValue(22, decimals, units, true); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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-form-panel tb-aggregated-data-keys-panel"> |
||||
|
<div class="tb-form-panel-title">{{ 'widgets.aggregated-value-card.values' | translate }}</div> |
||||
|
<div class="tb-form-table"> |
||||
|
<div class="tb-form-table-header"> |
||||
|
<div class="tb-form-table-header-cell tb-position-header" translate>widgets.aggregated-value-card.position</div> |
||||
|
<div class="tb-form-table-header-cell tb-aggregation-header" translate>widgets.aggregated-value-card.aggregation</div> |
||||
|
<div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div> |
||||
|
<div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> |
||||
|
<div class="tb-form-table-header-cell tb-font-header" translate>widgets.aggregated-value-card.font</div> |
||||
|
<div class="tb-form-table-header-cell tb-color-header" translate>widgets.aggregated-value-card.color</div> |
||||
|
<div class="tb-form-table-header-cell tb-actions-header"></div> |
||||
|
</div> |
||||
|
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body"> |
||||
|
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;"> |
||||
|
<tb-aggregated-data-key-row fxFlex |
||||
|
[formControl]="keyControl" |
||||
|
[datasourceType]="datasourceType" |
||||
|
[keyName]="keyName" |
||||
|
(keyRemoved)="removeKey($index)"> |
||||
|
</tb-aggregated-data-key-row> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
||||
|
{{ 'widgets.aggregated-value-card.add-value' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #noKeys> |
||||
|
<span fxLayoutAlign="center center" |
||||
|
class="tb-prompt">{{ 'widgets.aggregated-value-card.no-values' | translate }}</span> |
||||
|
</ng-template> |
||||
@ -0,0 +1,69 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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'; |
||||
|
|
||||
|
.tb-aggregated-data-keys-panel { |
||||
|
.tb-form-table-header-cell { |
||||
|
&.tb-position-header { |
||||
|
width: 132px; |
||||
|
min-width: 132px; |
||||
|
} |
||||
|
|
||||
|
&.tb-aggregation-header { |
||||
|
flex: 1; |
||||
|
min-width: 150px; |
||||
|
} |
||||
|
|
||||
|
&.tb-units-header { |
||||
|
width: 80px; |
||||
|
min-width: 80px; |
||||
|
} |
||||
|
|
||||
|
&.tb-decimals-header { |
||||
|
width: 60px; |
||||
|
min-width: 60px; |
||||
|
} |
||||
|
|
||||
|
&.tb-font-header { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
|
||||
|
&.tb-color-header { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
|
||||
|
&.tb-actions-header { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
} |
||||
|
|
||||
|
&.tb-units-header, &.tb-decimals-header { |
||||
|
display: none; |
||||
|
@media #{$mat-gt-sm} { |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-form-table-body { |
||||
|
tb-aggregated-data-key-row { |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,173 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
SimpleChanges, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup |
||||
|
} from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
||||
|
import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-aggregated-data-keys-panel', |
||||
|
templateUrl: './aggregated-data-keys-panel.component.html', |
||||
|
styleUrls: ['./aggregated-data-keys-panel.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
datasourceType: DatasourceType; |
||||
|
|
||||
|
@Input() |
||||
|
keyName: string; |
||||
|
|
||||
|
dataKeyType: DataKeyType; |
||||
|
|
||||
|
keysListFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
get widgetType(): widgetType { |
||||
|
return this.widgetConfigComponent.widgetType; |
||||
|
} |
||||
|
|
||||
|
get callbacks(): DataKeysCallbacks { |
||||
|
return this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
} |
||||
|
|
||||
|
get noKeys(): boolean { |
||||
|
const keys: DataKey[] = this.keysListFormGroup.get('keys').value; |
||||
|
return keys.length === 0; |
||||
|
} |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private dialog: MatDialog, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private utils: UtilsService, |
||||
|
private widgetConfigComponent: WidgetConfigComponent) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.keysListFormGroup = this.fb.group({ |
||||
|
keys: [this.fb.array([]), []] |
||||
|
}); |
||||
|
this.keysListFormGroup.valueChanges.subscribe( |
||||
|
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value) |
||||
|
); |
||||
|
this.updateParams(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['datasourceType'].includes(propName)) { |
||||
|
this.updateParams(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateParams() { |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
this.dataKeyType = DataKeyType.function; |
||||
|
} else { |
||||
|
this.dataKeyType = DataKeyType.timeseries; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.keysListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.keysListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: DataKey[] | undefined): void { |
||||
|
this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
keysFormArray(): UntypedFormArray { |
||||
|
return this.keysListFormGroup.get('keys') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackByKey(index: number, keyControl: AbstractControl): any { |
||||
|
return keyControl; |
||||
|
} |
||||
|
|
||||
|
removeKey(index: number) { |
||||
|
(this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
addKey() { |
||||
|
const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null); |
||||
|
dataKey.decimals = 0; |
||||
|
dataKey.settings = {...aggregatedValueCardDefaultKeySettings}; |
||||
|
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; |
||||
|
const keyControl = this.fb.control(dataKey, []); |
||||
|
keysArray.push(keyControl); |
||||
|
} |
||||
|
|
||||
|
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { |
||||
|
const keysControls: Array<AbstractControl> = []; |
||||
|
if (keys) { |
||||
|
keys.forEach((key) => { |
||||
|
keysControls.push(this.fb.control(key, [])); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(keysControls); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="aggregatedValueCardWidgetConfigForm"> |
||||
|
<tb-timewindow-config-panel formControlName="timewindowConfig"> |
||||
|
</tb-timewindow-config-panel> |
||||
|
<tb-datasources |
||||
|
[configMode]="basicMode" |
||||
|
hideDataKeyLabel |
||||
|
hideDataKeyColor |
||||
|
hideDataKeyUnits |
||||
|
hideDataKeyDecimals |
||||
|
hideLatestDataKeys |
||||
|
formControlName="datasources"> |
||||
|
</tb-datasources> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
||||
|
{{ 'widget-config.title' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="titleFont" |
||||
|
clearButton |
||||
|
[previewText]="aggregatedValueCardWidgetConfigForm.get('title').value" |
||||
|
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="titleColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
||||
|
{{ 'widgets.value-card.icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
||||
|
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
||||
|
<tb-material-icon-select asBoxInput |
||||
|
iconClearButton |
||||
|
[color]="aggregatedValueCardWidgetConfigForm.get('iconColor').value" |
||||
|
formControlName="icon"> |
||||
|
</tb-material-icon-select> |
||||
|
<tb-color-input asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showSubtitle"> |
||||
|
{{ 'widgets.aggregated-value-card.subtitle' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="subtitle" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings formControlName="subtitleFont" |
||||
|
clearButton |
||||
|
[previewText]="aggregatedValueCardWidgetConfigForm.get('subtitle').value"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="subtitleColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row column-xs"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate"> |
||||
|
{{ 'widgets.value-card.date' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
||||
|
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select> |
||||
|
<tb-font-settings formControlName="dateFont" |
||||
|
[previewText]="datePreviewFn"> |
||||
|
</tb-font-settings> |
||||
|
<tb-color-settings formControlName="dateColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showChart"> |
||||
|
{{ 'widgets.aggregated-value-card.chart' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-color-settings formControlName="chartColor"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-aggregated-data-keys-panel |
||||
|
[datasourceType]="datasource?.type" |
||||
|
[keyName]="keyName" |
||||
|
formControlName="values"> |
||||
|
</tb-aggregated-data-keys-panel> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.background.background' | translate }}</div> |
||||
|
<tb-background-settings formControlName="background"> |
||||
|
</tb-background-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between column-lt-md"> |
||||
|
<div translate>widget-config.show-card-buttons</div> |
||||
|
<mat-chip-listbox multiple formControlName="cardButtons"> |
||||
|
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-widget-actions-panel |
||||
|
formControlName="actions"> |
||||
|
</tb-widget-actions-panel> |
||||
|
</ng-container> |
||||
@ -0,0 +1,293 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, Injector } from '@angular/core'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
||||
|
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
||||
|
import { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { |
||||
|
getTimewindowConfig, |
||||
|
setTimewindowConfig |
||||
|
} from '@home/components/widget/config/timewindow-config-panel.component'; |
||||
|
import { isUndefined } from '@core/utils'; |
||||
|
import { |
||||
|
cssSizeToStrSize, |
||||
|
DateFormatProcessor, |
||||
|
DateFormatSettings, getDataKey, |
||||
|
resolveCssSize |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { |
||||
|
aggregatedValueCardDefaultSettings, |
||||
|
AggregatedValueCardWidgetSettings, |
||||
|
createDefaultAggregatedValueLatestDataKeys |
||||
|
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
||||
|
import { |
||||
|
AggregationType, |
||||
|
HistoryWindowType, |
||||
|
HOUR, |
||||
|
QuickTimeInterval, |
||||
|
TimewindowType |
||||
|
} from '@shared/models/time/time.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-aggregated-value-card-basic-config', |
||||
|
templateUrl: './aggregated-value-card-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
public get datasource(): Datasource { |
||||
|
const datasources: Datasource[] = this.aggregatedValueCardWidgetConfigForm.get('datasources').value; |
||||
|
if (datasources && datasources.length) { |
||||
|
return datasources[0]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public get keyName(): string { |
||||
|
const dataKey = getDataKey(this.aggregatedValueCardWidgetConfigForm.get('datasources').value); |
||||
|
if (dataKey) { |
||||
|
return dataKey.name; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
aggregatedValueCardWidgetConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
datePreviewFn = this._datePreviewFn.bind(this); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private $injector: Injector, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.aggregatedValueCardWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected setupDefaults(configData: WidgetConfigComponentData) { |
||||
|
this.setupDefaultDatasource(configData, [ |
||||
|
{ name: 'watermeter', label: 'Watermeter', type: DataKeyType.timeseries } |
||||
|
], |
||||
|
createDefaultAggregatedValueLatestDataKeys('watermeter', 'm³') |
||||
|
); |
||||
|
configData.config.useDashboardTimewindow = false; |
||||
|
configData.config.displayTimewindow = true; |
||||
|
configData.config.timewindow = { |
||||
|
selectedTab: TimewindowType.HISTORY, |
||||
|
history: { |
||||
|
historyType: HistoryWindowType.INTERVAL, |
||||
|
quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR, |
||||
|
}, |
||||
|
aggregation: { |
||||
|
type: AggregationType.AVG, |
||||
|
interval: 12 * HOUR, |
||||
|
limit: 5000 |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
const settings: AggregatedValueCardWidgetSettings = {...aggregatedValueCardDefaultSettings, ...(configData.config.settings || {})}; |
||||
|
const dataKey = getDataKey(configData.config.datasources); |
||||
|
const keyName = dataKey?.name; |
||||
|
const iconSize = resolveCssSize(configData.config.iconSize); |
||||
|
this.aggregatedValueCardWidgetConfigForm = this.fb.group({ |
||||
|
timewindowConfig: [getTimewindowConfig(configData.config), []], |
||||
|
datasources: [configData.config.datasources, []], |
||||
|
|
||||
|
showTitle: [configData.config.showTitle, []], |
||||
|
title: [configData.config.title, []], |
||||
|
titleFont: [configData.config.titleFont, []], |
||||
|
titleColor: [configData.config.titleColor, []], |
||||
|
|
||||
|
showIcon: [configData.config.showTitleIcon, []], |
||||
|
iconSize: [iconSize[0], [Validators.min(0)]], |
||||
|
iconSizeUnit: [iconSize[1], []], |
||||
|
icon: [configData.config.titleIcon, []], |
||||
|
iconColor: [configData.config.iconColor, []], |
||||
|
|
||||
|
showSubtitle: [settings.showSubtitle, []], |
||||
|
subtitle: [settings.subtitle, []], |
||||
|
subtitleFont: [settings.subtitleFont, []], |
||||
|
subtitleColor: [settings.subtitleColor, []], |
||||
|
|
||||
|
showDate: [settings.showDate, []], |
||||
|
dateFormat: [settings.dateFormat, []], |
||||
|
dateFont: [settings.dateFont, []], |
||||
|
dateColor: [settings.dateColor, []], |
||||
|
|
||||
|
showChart: [settings.showChart, []], |
||||
|
chartColor: [settings.chartColor, []], |
||||
|
|
||||
|
values: [this.getValues(configData.config.datasources, keyName), []], |
||||
|
|
||||
|
background: [settings.background, []], |
||||
|
|
||||
|
cardButtons: [this.getCardButtons(configData.config), []], |
||||
|
borderRadius: [configData.config.borderRadius, []], |
||||
|
|
||||
|
actions: [configData.config.actions || {}, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
||||
|
this.widgetConfig.config.datasources = config.datasources; |
||||
|
|
||||
|
this.widgetConfig.config.showTitle = config.showTitle; |
||||
|
this.widgetConfig.config.title = config.title; |
||||
|
this.widgetConfig.config.titleFont = config.titleFont; |
||||
|
this.widgetConfig.config.titleColor = config.titleColor; |
||||
|
|
||||
|
this.widgetConfig.config.showTitleIcon = config.showIcon; |
||||
|
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); |
||||
|
this.widgetConfig.config.titleIcon = config.icon; |
||||
|
this.widgetConfig.config.iconColor = config.iconColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showSubtitle = config.showSubtitle; |
||||
|
this.widgetConfig.config.settings.subtitle = config.subtitle; |
||||
|
this.widgetConfig.config.settings.subtitleFont = config.subtitleFont; |
||||
|
this.widgetConfig.config.settings.subtitleColor = config.subtitleColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showDate = config.showDate; |
||||
|
this.widgetConfig.config.settings.dateFormat = config.dateFormat; |
||||
|
this.widgetConfig.config.settings.dateFont = config.dateFont; |
||||
|
this.widgetConfig.config.settings.dateColor = config.dateColor; |
||||
|
|
||||
|
this.widgetConfig.config.settings.showChart = config.showChart; |
||||
|
this.widgetConfig.config.settings.chartColor = config.chartColor; |
||||
|
|
||||
|
this.setValues(config.values, this.widgetConfig.config.datasources); |
||||
|
|
||||
|
this.widgetConfig.config.settings.background = config.background; |
||||
|
|
||||
|
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
||||
|
this.widgetConfig.config.borderRadius = config.borderRadius; |
||||
|
|
||||
|
this.widgetConfig.config.actions = config.actions; |
||||
|
return this.widgetConfig; |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['showTitle', 'showIcon', 'showSubtitle', 'showDate', 'showChart']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean, trigger?: string) { |
||||
|
const showTitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showTitle').value; |
||||
|
const showIcon: boolean = this.aggregatedValueCardWidgetConfigForm.get('showIcon').value; |
||||
|
const showSubtitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showSubtitle').value; |
||||
|
const showDate: boolean = this.aggregatedValueCardWidgetConfigForm.get('showDate').value; |
||||
|
const showChart: boolean = this.aggregatedValueCardWidgetConfigForm.get('showChart').value; |
||||
|
|
||||
|
if (showTitle) { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('title').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('titleFont').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('titleColor').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); |
||||
|
if (showIcon) { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSize').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('icon').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconColor').enable(); |
||||
|
} else { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
} else { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('title').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('titleFont').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('titleColor').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showSubtitle) { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitle').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').enable(); |
||||
|
} else { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitle').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showDate) { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateFont').enable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateColor').enable(); |
||||
|
} else { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateFont').disable(); |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('dateColor').disable(); |
||||
|
} |
||||
|
|
||||
|
if (showChart) { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('chartColor').enable(); |
||||
|
} else { |
||||
|
this.aggregatedValueCardWidgetConfigForm.get('chartColor').disable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getValues(datasources: Datasource[], keyName: string): DataKey[] { |
||||
|
if (datasources && datasources.length) { |
||||
|
return (datasources[0].latestDataKeys || []).filter(k => k.name === keyName); |
||||
|
} |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
private setValues(values: DataKey[], datasources?: Datasource[]) { |
||||
|
if (datasources && datasources.length) { |
||||
|
datasources[0].latestDataKeys = values; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getCardButtons(config: WidgetConfig): string[] { |
||||
|
const buttons: string[] = []; |
||||
|
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
||||
|
buttons.push('fullscreen'); |
||||
|
} |
||||
|
return buttons; |
||||
|
} |
||||
|
|
||||
|
private setCardButtons(buttons: string[], config: WidgetConfig) { |
||||
|
config.enableFullscreen = buttons.includes('fullscreen'); |
||||
|
} |
||||
|
|
||||
|
private _datePreviewFn(): string { |
||||
|
const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetConfigForm.get('dateFormat').value; |
||||
|
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); |
||||
|
processor.update(Date.now()); |
||||
|
return processor.formatted; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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-aggregated-value-card-panel" [style]="backgroundStyle"> |
||||
|
<div class="tb-aggregated-value-card-overlay" [style]="overlayStyle"></div> |
||||
|
<div class="tb-aggregated-value-card-title-panel"> |
||||
|
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
||||
|
<ng-container *ngTemplateOutlet="subtitleTpl"></ng-container> |
||||
|
</div> |
||||
|
<ng-container *ngTemplateOutlet="valuesTpl"></ng-container> |
||||
|
<ng-container *ngTemplateOutlet="chartTpl"></ng-container> |
||||
|
<ng-container *ngTemplateOutlet="dateTpl"></ng-container> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #subtitleTpl> |
||||
|
<div *ngIf="showSubtitle" class="tb-aggregated-value-card-subtitle" |
||||
|
[style]="subtitleStyle" [style.color]="subtitleColor.color">{{ subtitle$ | async }}</div> |
||||
|
</ng-template> |
||||
|
<ng-template #valuesTpl> |
||||
|
<div *ngIf="showValues" class="tb-aggregated-value-card-values"> |
||||
|
<div class="tb-aggregated-value-card-values-container"> |
||||
|
<div class="tb-aggregated-value-card-values-section left"> |
||||
|
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftTop]"> |
||||
|
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftTop]}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftBottom]"> |
||||
|
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftBottom]}"></ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<div class="tb-aggregated-value-card-values-section center"> |
||||
|
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.center]"> |
||||
|
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.center]}"></ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<div class="tb-aggregated-value-card-values-section right"> |
||||
|
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightTop]"> |
||||
|
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightTop]}"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightBottom]"> |
||||
|
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightBottom]}"></ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template #chartTpl> |
||||
|
<div *ngIf="showChart" class="tb-aggregated-value-card-chart"> |
||||
|
<div class="tb-aggregated-value-card-chart-ticks"> |
||||
|
<div>{{tickMax$ | async}}</div> |
||||
|
<div>{{tickMin$ | async}}</div> |
||||
|
</div> |
||||
|
<div class="tb-aggregated-value-card-chart-container"> |
||||
|
<div class="tb-aggregated-value-card-chart-element" #chartElement></div> |
||||
|
<div class="tb-aggregated-value-card-chart-boundary top left"></div> |
||||
|
<div class="tb-aggregated-value-card-chart-boundary top right"></div> |
||||
|
<div class="tb-aggregated-value-card-chart-boundary bottom left"></div> |
||||
|
<div class="tb-aggregated-value-card-chart-boundary bottom right"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template #dateTpl> |
||||
|
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color" [innerHTML]="dateFormat.formatted"></div> |
||||
|
</ng-template> |
||||
|
<ng-template #valueTpl let-value="value"> |
||||
|
<div class="tb-aggregated-value-card-value" [style]="value.style" [style.color]="value.color.color"> |
||||
|
<div *ngIf="value.showArrow" class="value-arrow-container"> |
||||
|
<span *ngIf="!value.upArrow && !value.downArrow" class="value-arrow"></span> |
||||
|
<tb-icon *ngIf="value.upArrow" class="value-arrow">arrow_upward</tb-icon> |
||||
|
<tb-icon *ngIf="value.downArrow" class="value-arrow">arrow_downward</tb-icon> |
||||
|
</div> |
||||
|
<div class="value-text"> |
||||
|
<span>{{ value.value }}</span> |
||||
|
<span class="units" [class]="{'small': value.center}">{{ value.units }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,156 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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-aggregated-value-card-panel { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
gap: 8px; |
||||
|
padding: 20px 24px 24px; |
||||
|
> div:not(.tb-value-card-overlay) { |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.tb-aggregated-value-card-overlay { |
||||
|
position: absolute; |
||||
|
top: 12px; |
||||
|
left: 12px; |
||||
|
bottom: 12px; |
||||
|
right: 12px; |
||||
|
} |
||||
|
> div.tb-aggregated-value-card-title-panel { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
.tb-aggregated-value-card-subtitle { |
||||
|
margin-left: 28px; |
||||
|
} |
||||
|
} |
||||
|
.tb-aggregated-value-card-values, .tb-aggregated-value-card-chart { |
||||
|
flex: 1; |
||||
|
min-height: 0; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
.tb-aggregated-value-card-values-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
padding: 8px 0; |
||||
|
display: grid; |
||||
|
grid-template-columns: minmax(0, 1fr) fit-content(100%) minmax(0, 1fr); |
||||
|
.tb-aggregated-value-card-values-section { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
overflow: hidden; |
||||
|
&.left { |
||||
|
align-items: flex-start; |
||||
|
} |
||||
|
&.center { |
||||
|
align-items: center; |
||||
|
} |
||||
|
&.right { |
||||
|
align-items: flex-end; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-aggregated-value-card-chart { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
flex-direction: row; |
||||
|
.tb-aggregated-value-card-chart-ticks { |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
place-content: flex-end space-between; |
||||
|
align-items: flex-end; |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
font-weight: 400; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
.tb-aggregated-value-card-chart-container { |
||||
|
position: relative; |
||||
|
flex: 1; |
||||
|
margin-top: 8px; |
||||
|
margin-bottom: 8px; |
||||
|
.tb-aggregated-value-card-chart-element { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
.tb-aggregated-value-card-chart-boundary { |
||||
|
position: absolute; |
||||
|
width: 6px; |
||||
|
height: 6px; |
||||
|
&.top { |
||||
|
top: 0; |
||||
|
border-top: 2px solid rgba(0,0,0,0.38); |
||||
|
} |
||||
|
&.left { |
||||
|
left: 0; |
||||
|
border-left: 2px solid rgba(0,0,0,0.38); |
||||
|
} |
||||
|
&.right { |
||||
|
right: 0; |
||||
|
border-right: 2px solid rgba(0,0,0,0.38); |
||||
|
} |
||||
|
&.bottom { |
||||
|
bottom: 0; |
||||
|
border-bottom: 2px solid rgba(0,0,0,0.38); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-aggregated-value-card-value { |
||||
|
white-space: nowrap; |
||||
|
min-height: 0; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center; |
||||
|
align-items: center; |
||||
|
.value-arrow-container { |
||||
|
display: flex; |
||||
|
} |
||||
|
.value-text { |
||||
|
line-height: 1; |
||||
|
} |
||||
|
.value-arrow { |
||||
|
font-size: 1.1em; |
||||
|
height: 1.1em; |
||||
|
line-height: 1.1em; |
||||
|
min-width: 1em; |
||||
|
width: 1em; |
||||
|
} |
||||
|
.units { |
||||
|
font-size: 85%; |
||||
|
padding-left: 0.2em; |
||||
|
&.small { |
||||
|
font-size: 50%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-aggregated-value-card-panel { |
||||
|
> div.tb-aggregated-value-card-title-panel { |
||||
|
.tb-widget-title { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,261 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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 { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
TemplateRef, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
aggregatedValueCardDefaultSettings, |
||||
|
AggregatedValueCardKeyPosition, |
||||
|
AggregatedValueCardValue, |
||||
|
AggregatedValueCardWidgetSettings, |
||||
|
computeAggregatedCardValue, |
||||
|
getTsValueByLatestDataKey |
||||
|
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { |
||||
|
backgroundStyle, |
||||
|
ColorProcessor, |
||||
|
ComponentStyle, |
||||
|
DateFormatProcessor, getDataKey, |
||||
|
getLatestSingleTsValue, |
||||
|
overlayStyle, |
||||
|
textStyle |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { TbFlot } from '@home/components/widget/lib/flot-widget'; |
||||
|
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; |
||||
|
import { DataKey } from '@shared/models/widget.models'; |
||||
|
import { formatNumberValue, formatValue, isDefined, isNumeric } from '@core/utils'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-aggregated-value-card-widget', |
||||
|
templateUrl: './aggregated-value-card-widget.component.html', |
||||
|
styleUrls: ['./aggregated-value-card-widget.component.scss'] |
||||
|
}) |
||||
|
export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit { |
||||
|
|
||||
|
@ViewChild('chartElement', {static: false}) chartElement: ElementRef; |
||||
|
|
||||
|
aggregatedValueCardKeyPosition = AggregatedValueCardKeyPosition; |
||||
|
|
||||
|
settings: AggregatedValueCardWidgetSettings; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetTitlePanel: TemplateRef<any>; |
||||
|
|
||||
|
showSubtitle = true; |
||||
|
subtitle$: Observable<string>; |
||||
|
subtitleStyle: ComponentStyle = {}; |
||||
|
subtitleColor: ColorProcessor; |
||||
|
|
||||
|
showValues = false; |
||||
|
|
||||
|
values: {[key: string]: AggregatedValueCardValue} = {}; |
||||
|
|
||||
|
showChart = true; |
||||
|
chartColor: ColorProcessor; |
||||
|
|
||||
|
showDate = true; |
||||
|
dateFormat: DateFormatProcessor; |
||||
|
dateStyle: ComponentStyle = {}; |
||||
|
dateColor: ColorProcessor; |
||||
|
|
||||
|
backgroundStyle: ComponentStyle = {}; |
||||
|
overlayStyle: ComponentStyle = {}; |
||||
|
|
||||
|
private flot: TbFlot; |
||||
|
private flotDataKey: DataKey; |
||||
|
|
||||
|
private lastUpdateTs: number; |
||||
|
|
||||
|
tickMin$: Observable<string>; |
||||
|
tickMax$: Observable<string>; |
||||
|
|
||||
|
constructor(private date: DatePipe, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.aggregatedValueCardWidget = this; |
||||
|
this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings}; |
||||
|
this.showSubtitle = this.settings.showSubtitle; |
||||
|
const subtitle = this.settings.subtitle; |
||||
|
this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$); |
||||
|
this.subtitleStyle = textStyle(this.settings.subtitleFont, '0.25px'); |
||||
|
this.subtitleColor = ColorProcessor.fromSettings(this.settings.subtitleColor); |
||||
|
|
||||
|
const dataKey = getDataKey(this.ctx.defaultSubscription.datasources); |
||||
|
if (dataKey?.name && this.ctx.defaultSubscription.firstDatasource?.latestDataKeys?.length) { |
||||
|
const dataKeys = this.ctx.defaultSubscription.firstDatasource?.latestDataKeys; |
||||
|
for (const position of Object.keys(AggregatedValueCardKeyPosition)) { |
||||
|
const value = computeAggregatedCardValue(dataKeys, dataKey?.name, AggregatedValueCardKeyPosition[position]); |
||||
|
if (value) { |
||||
|
this.values[position] = value; |
||||
|
} |
||||
|
} |
||||
|
this.showValues = !!Object.keys(this.values).length; |
||||
|
} |
||||
|
|
||||
|
this.showChart = this.settings.showChart; |
||||
|
this.chartColor = ColorProcessor.fromSettings(this.settings.chartColor); |
||||
|
if (this.showChart) { |
||||
|
if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { |
||||
|
this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; |
||||
|
this.flotDataKey.settings = { |
||||
|
fillLines: false, |
||||
|
showLines: true, |
||||
|
lineWidth: 2 |
||||
|
} as TbFlotKeySettings; |
||||
|
this.flotDataKey.color = this.chartColor.color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.showDate = this.settings.showDate; |
||||
|
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat); |
||||
|
this.dateStyle = textStyle(this.settings.dateFont, '0.25px'); |
||||
|
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor); |
||||
|
|
||||
|
this.backgroundStyle = backgroundStyle(this.settings.background); |
||||
|
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
if (this.showChart) { |
||||
|
const settings = { |
||||
|
shadowSize: 0, |
||||
|
enableSelection: false, |
||||
|
smoothLines: false, |
||||
|
grid: { |
||||
|
tickColor: 'rgba(0,0,0,0.12)', |
||||
|
horizontalLines: true, |
||||
|
verticalLines: false, |
||||
|
outlineWidth: 0, |
||||
|
minBorderMargin: 0, |
||||
|
margin: 0 |
||||
|
}, |
||||
|
yaxis: { |
||||
|
showLabels: false, |
||||
|
tickGenerator: 'return [(axis.max + axis.min) / 2];' |
||||
|
}, |
||||
|
xaxis: { |
||||
|
showLabels: false |
||||
|
} |
||||
|
} as TbFlotSettings; |
||||
|
this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); |
||||
|
this.tickMin$ = this.flot.yMin$.pipe( |
||||
|
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), |
||||
|
(this.flotDataKey?.units || this.ctx.units)) |
||||
|
)); |
||||
|
this.tickMax$ = this.flot.yMax$.pipe( |
||||
|
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), |
||||
|
(this.flotDataKey?.units || this.ctx.units)) |
||||
|
)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onInit() { |
||||
|
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
||||
|
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
const tsValue = getLatestSingleTsValue(this.ctx.data); |
||||
|
let ts; |
||||
|
let value; |
||||
|
if (tsValue) { |
||||
|
ts = tsValue[0]; |
||||
|
value = tsValue[1]; |
||||
|
} |
||||
|
this.subtitleColor.update(value); |
||||
|
this.dateColor.update(value); |
||||
|
|
||||
|
if (this.showChart) { |
||||
|
this.chartColor.update(value); |
||||
|
this.flot.updateSeriesColor(this.chartColor.color); |
||||
|
this.flot.update(); |
||||
|
} |
||||
|
|
||||
|
this.updateLastUpdateTs(ts); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
|
||||
|
public onLatestDataUpdated() { |
||||
|
if (this.showValues) { |
||||
|
for (const aggValue of Object.values(this.values)) { |
||||
|
const tsValue = getTsValueByLatestDataKey(this.ctx.latestData, aggValue.key); |
||||
|
let ts; |
||||
|
let value; |
||||
|
if (tsValue) { |
||||
|
ts = tsValue[0]; |
||||
|
value = tsValue[1]; |
||||
|
aggValue.value = formatValue(value, (aggValue.key.decimals || this.ctx.decimals), null, false); |
||||
|
} else { |
||||
|
aggValue.value = 'N/A'; |
||||
|
} |
||||
|
const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals)); |
||||
|
aggValue.color.update(numeric); |
||||
|
if (aggValue.showArrow && isDefined(numeric)) { |
||||
|
aggValue.upArrow = numeric > 0; |
||||
|
aggValue.downArrow = numeric < 0; |
||||
|
} else { |
||||
|
aggValue.upArrow = aggValue.downArrow = false; |
||||
|
} |
||||
|
this.updateLastUpdateTs(ts); |
||||
|
} |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onResize() { |
||||
|
if (this.showChart) { |
||||
|
this.flot.resize(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onEditModeChanged() { |
||||
|
if (this.showChart) { |
||||
|
this.flot.checkMouseEvents(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onDestroy() { |
||||
|
if (this.showChart) { |
||||
|
this.flot.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateLastUpdateTs(ts: number) { |
||||
|
if (ts && (!this.lastUpdateTs || ts > this.lastUpdateTs)) { |
||||
|
this.lastUpdateTs = ts; |
||||
|
this.dateFormat.update(ts); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,232 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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 { |
||||
|
BackgroundSettings, |
||||
|
BackgroundType, |
||||
|
ColorProcessor, |
||||
|
ColorSettings, |
||||
|
ColorType, |
||||
|
ComponentStyle, |
||||
|
constantColor, |
||||
|
DateFormatSettings, |
||||
|
Font, |
||||
|
iconStyle, |
||||
|
lastUpdateAgoDateFormat, |
||||
|
textStyle |
||||
|
} from '@shared/models/widget-settings.models'; |
||||
|
import { ComparisonResultType, DataKey, DatasourceData } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { AggregationType } from '@shared/models/time/time.models'; |
||||
|
|
||||
|
export interface AggregatedValueCardWidgetSettings { |
||||
|
showSubtitle: boolean; |
||||
|
subtitle: string; |
||||
|
subtitleFont: Font; |
||||
|
subtitleColor: ColorSettings; |
||||
|
showDate: boolean; |
||||
|
dateFormat: DateFormatSettings; |
||||
|
dateFont: Font; |
||||
|
dateColor: ColorSettings; |
||||
|
showChart: boolean; |
||||
|
chartColor: ColorSettings; |
||||
|
background: BackgroundSettings; |
||||
|
} |
||||
|
|
||||
|
export enum AggregatedValueCardKeyPosition { |
||||
|
center = 'center', |
||||
|
rightTop = 'rightTop', |
||||
|
rightBottom = 'rightBottom', |
||||
|
leftTop = 'leftTop', |
||||
|
leftBottom = 'leftBottom' |
||||
|
} |
||||
|
|
||||
|
export const aggregatedValueCardKeyPositionTranslations = new Map<AggregatedValueCardKeyPosition, string>( |
||||
|
[ |
||||
|
[AggregatedValueCardKeyPosition.center, 'widgets.aggregated-value-card.position-center'], |
||||
|
[AggregatedValueCardKeyPosition.rightTop, 'widgets.aggregated-value-card.position-right-top'], |
||||
|
[AggregatedValueCardKeyPosition.rightBottom, 'widgets.aggregated-value-card.position-right-bottom'], |
||||
|
[AggregatedValueCardKeyPosition.leftTop, 'widgets.aggregated-value-card.position-left-top'], |
||||
|
[AggregatedValueCardKeyPosition.leftBottom, 'widgets.aggregated-value-card.position-left-bottom'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export interface AggregatedValueCardKeySettings { |
||||
|
position: AggregatedValueCardKeyPosition; |
||||
|
font: Font; |
||||
|
color: ColorSettings; |
||||
|
showArrow: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface AggregatedValueCardValue { |
||||
|
key: DataKey; |
||||
|
value: string; |
||||
|
units: string; |
||||
|
style: ComponentStyle; |
||||
|
color: ColorProcessor; |
||||
|
center: boolean; |
||||
|
showArrow: boolean; |
||||
|
upArrow: boolean; |
||||
|
downArrow: boolean; |
||||
|
} |
||||
|
|
||||
|
export const computeAggregatedCardValue = (dataKeys: DataKey[], keyName: string, position: AggregatedValueCardKeyPosition): AggregatedValueCardValue => { |
||||
|
const key = dataKeys.find(dataKey => ( dataKey.name === keyName && (dataKey.settings?.position === position || |
||||
|
(!dataKey.settings?.position && position === AggregatedValueCardKeyPosition.center)) )); |
||||
|
if (key) { |
||||
|
const settings: AggregatedValueCardKeySettings = key.settings; |
||||
|
return { |
||||
|
key, |
||||
|
value: '', |
||||
|
units: key.units, |
||||
|
style: textStyle(settings.font, '0.25px'), |
||||
|
color: ColorProcessor.fromSettings(settings.color), |
||||
|
center: position === AggregatedValueCardKeyPosition.center, |
||||
|
showArrow: settings.showArrow, |
||||
|
upArrow: false, |
||||
|
downArrow: false |
||||
|
}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dataKey: DataKey): [number, any] => { |
||||
|
if (latestData?.length) { |
||||
|
const dsData = latestData.find(data => data.dataKey === dataKey); |
||||
|
if (dsData?.data?.length) { |
||||
|
return dsData.data[0]; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
|
||||
|
export const aggregatedValueCardDefaultSettings: AggregatedValueCardWidgetSettings = { |
||||
|
showSubtitle: true, |
||||
|
subtitle: '${entityName}', |
||||
|
subtitleFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
subtitleColor: constantColor('rgba(0, 0, 0, 0.38)'), |
||||
|
showDate: true, |
||||
|
dateFormat: lastUpdateAgoDateFormat(), |
||||
|
dateFont: { |
||||
|
family: 'Roboto', |
||||
|
size: 12, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '16px' |
||||
|
}, |
||||
|
dateColor: constantColor('rgba(0, 0, 0, 0.38)'), |
||||
|
showChart: true, |
||||
|
chartColor: constantColor('rgba(0, 0, 0, 0.87)'), |
||||
|
background: { |
||||
|
type: BackgroundType.color, |
||||
|
color: '#fff', |
||||
|
overlay: { |
||||
|
enabled: false, |
||||
|
color: 'rgba(255,255,255,0.72)', |
||||
|
blur: 3 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const aggregatedValueCardDefaultKeySettings: AggregatedValueCardKeySettings = { |
||||
|
position: AggregatedValueCardKeyPosition.center, |
||||
|
font: { |
||||
|
family: 'Roboto', |
||||
|
size: 14, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
color: constantColor('rgba(0, 0, 0, 0.87)'), |
||||
|
showArrow: false |
||||
|
}; |
||||
|
|
||||
|
export const createDefaultAggregatedValueLatestDataKeys = (keyName: string, units): DataKey[] => [ |
||||
|
{ |
||||
|
name: keyName, label: keyName, type: DataKeyType.timeseries, units, decimals: 0, |
||||
|
aggregationType: AggregationType.NONE, |
||||
|
settings: { |
||||
|
position: AggregatedValueCardKeyPosition.center, |
||||
|
font: { |
||||
|
family: 'Roboto', |
||||
|
size: 52, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
color: constantColor('rgba(0, 0, 0, 0.87)'), |
||||
|
showArrow: false |
||||
|
} as AggregatedValueCardKeySettings |
||||
|
}, |
||||
|
{ |
||||
|
name: keyName, label: 'Delta percent ' + keyName, type: DataKeyType.timeseries, units: '%', decimals: 0, |
||||
|
aggregationType: AggregationType.AVG, |
||||
|
comparisonEnabled: true, |
||||
|
timeForComparison: 'previousInterval', |
||||
|
comparisonResultType: ComparisonResultType.DELTA_PERCENT, |
||||
|
settings: { |
||||
|
position: AggregatedValueCardKeyPosition.rightTop, |
||||
|
font: { |
||||
|
family: 'Roboto', |
||||
|
size: 14, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '500', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
color: { |
||||
|
color: 'rgba(0, 0, 0, 0.87)', |
||||
|
type: ColorType.range, |
||||
|
rangeList: [ |
||||
|
{to: 0, color: '#198038'}, |
||||
|
{from: 0, to: 0, color: 'rgba(0, 0, 0, 0.87)'}, |
||||
|
{from: 0, color: '#D12730'} |
||||
|
], |
||||
|
colorFunction: '' |
||||
|
}, |
||||
|
showArrow: true |
||||
|
} as AggregatedValueCardKeySettings |
||||
|
}, |
||||
|
{ |
||||
|
name: keyName, label: 'Delta absolute ' + keyName, type: DataKeyType.timeseries, units, decimals: 1, |
||||
|
aggregationType: AggregationType.AVG, |
||||
|
comparisonEnabled: true, |
||||
|
timeForComparison: 'previousInterval', |
||||
|
comparisonResultType: ComparisonResultType.DELTA_ABSOLUTE, |
||||
|
settings: { |
||||
|
position: AggregatedValueCardKeyPosition.rightBottom, |
||||
|
font: { |
||||
|
family: 'Roboto', |
||||
|
size: 11, |
||||
|
sizeUnit: 'px', |
||||
|
style: 'normal', |
||||
|
weight: '400', |
||||
|
lineHeight: '1' |
||||
|
}, |
||||
|
color: constantColor('rgba(0, 0, 0, 0.38)'), |
||||
|
showArrow: false |
||||
|
} as AggregatedValueCardKeySettings |
||||
|
} |
||||
|
]; |
||||
@ -0,0 +1,48 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="aggregatedValueCardKeySettingsForm"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" translate>widgets.aggregated-value-card.value-appearance</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.aggregated-value-card.position' | translate }}</div> |
||||
|
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="position"> |
||||
|
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position"> |
||||
|
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.aggregated-value-card.font' | translate }}</div> |
||||
|
<tb-font-settings formControlName="font" |
||||
|
previewText="22 °C"> |
||||
|
</tb-font-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div>{{ 'widgets.aggregated-value-card.color' | translate }}</div> |
||||
|
<tb-color-settings formControlName="color"> |
||||
|
</tb-color-settings> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showArrow"> |
||||
|
{{ 'widgets.aggregated-value-card.display-up-down-arrow' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -0,0 +1,64 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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 } from '@angular/core'; |
||||
|
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
aggregatedValueCardDefaultKeySettings, |
||||
|
AggregatedValueCardKeyPosition, |
||||
|
aggregatedValueCardKeyPositionTranslations |
||||
|
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
||||
|
import { constantColor } from '@shared/models/widget-settings.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-aggregated-value-card-key-settings', |
||||
|
templateUrl: './aggregated-value-card-key-settings.component.html', |
||||
|
styleUrls: ['./../widget-settings.scss'] |
||||
|
}) |
||||
|
export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent { |
||||
|
|
||||
|
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = |
||||
|
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); |
||||
|
|
||||
|
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; |
||||
|
|
||||
|
aggregatedValueCardKeySettingsForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
protected settingsForm(): UntypedFormGroup { |
||||
|
return this.aggregatedValueCardKeySettingsForm; |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): WidgetSettings { |
||||
|
return {...aggregatedValueCardDefaultKeySettings}; |
||||
|
} |
||||
|
|
||||
|
protected onSettingsSet(settings: WidgetSettings) { |
||||
|
this.aggregatedValueCardKeySettingsForm = this.fb.group({ |
||||
|
position: [settings.position, []], |
||||
|
font: [settings.font, []], |
||||
|
color: [settings.color, []], |
||||
|
showArrow: [settings.showArrow, []] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 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]="notificationSettingsFormGroup" fxLayout="column"> |
||||
|
<div fxLayout="row" style="height: 48px;"> |
||||
|
<div fxFlex="50" fxLayoutAlign="start center"> |
||||
|
<mat-checkbox color="primary" |
||||
|
[checked]="notificationSettingsFormGroup.get('enabled').value" |
||||
|
(click)="toggleEnabled()"> |
||||
|
<span class="notification-type" |
||||
|
[ngClass]="{'notification-type-disabled': !notificationSettingsFormGroup.get('enabled').value}"> |
||||
|
{{notificationTemplateTypeTranslateMap.get(notificationSettingsFormGroup.get('name').value)?.name | translate}} |
||||
|
</span> |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="row" *ngFor="let deliveryMethods of deliveryMethods"> |
||||
|
<div fxFlex fxLayoutAlign="start center"> |
||||
|
<mat-checkbox color="primary" |
||||
|
[disabled]="!allowDeliveryMethods?.includes(deliveryMethods) || !notificationSettingsFormGroup.get('enabled').value" |
||||
|
[checked]="getChecked(deliveryMethods)" |
||||
|
(click)="toggleDeliviryMethod(deliveryMethods)" |
||||
|
></mat-checkbox> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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 { |
||||
|
.notification-type { |
||||
|
font-size: 14px; |
||||
|
|
||||
|
&-disabled { |
||||
|
color: rgba(0, 0, 0, 0.38) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { Subscription } from 'rxjs'; |
||||
|
import { |
||||
|
NotificationDeliveryMethod, |
||||
|
NotificationTemplateTypeTranslateMap, |
||||
|
NotificationUserSetting |
||||
|
} from '@shared/models/notification.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-notification-setting-form', |
||||
|
templateUrl: './notification-setting-form.component.html', |
||||
|
styleUrls: ['./notification-setting-form.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => NotificationSettingFormComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
deliveryMethods: NotificationDeliveryMethod[] = []; |
||||
|
|
||||
|
@Input() |
||||
|
allowDeliveryMethods: NotificationDeliveryMethod[] = []; |
||||
|
|
||||
|
notificationSettingsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private valueChange$: Subscription = null; |
||||
|
|
||||
|
constructor(private utils: UtilsService, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
const deliveryMethod = {}; |
||||
|
this.deliveryMethods.forEach(value => { |
||||
|
deliveryMethod[value] = true; |
||||
|
}); |
||||
|
this.notificationSettingsFormGroup = this.fb.group( |
||||
|
{ |
||||
|
name: [''], |
||||
|
enabled: [true], |
||||
|
enabledDeliveryMethods: this.fb.group({ |
||||
|
...deliveryMethod |
||||
|
}) |
||||
|
}); |
||||
|
this.valueChange$ = this.notificationSettingsFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.valueChange$) { |
||||
|
this.valueChange$.unsubscribe(); |
||||
|
this.valueChange$ = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.notificationSettingsFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.notificationSettingsFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
toggleEnabled() { |
||||
|
this.notificationSettingsFormGroup.get('enabled').patchValue(!this.notificationSettingsFormGroup.get('enabled').value); |
||||
|
} |
||||
|
|
||||
|
getChecked(deliveryMethod: NotificationDeliveryMethod): boolean { |
||||
|
return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value; |
||||
|
} |
||||
|
|
||||
|
toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) { |
||||
|
this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod) |
||||
|
.patchValue(!this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value); |
||||
|
} |
||||
|
|
||||
|
writeValue(value: NotificationUserSetting): void { |
||||
|
if (isDefinedAndNotNull(value)) { |
||||
|
this.notificationSettingsFormGroup.patchValue(value, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.propagateChange(this.notificationSettingsFormGroup.value); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue