110 changed files with 3307 additions and 755 deletions
@ -0,0 +1,62 @@ |
|||||
|
/** |
||||
|
* 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.cache; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.data.redis.connection.RedisSentinelConfiguration; |
||||
|
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; |
||||
|
|
||||
|
@Configuration |
||||
|
@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) |
||||
|
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel") |
||||
|
public class TBRedisSentinelConfiguration extends TBRedisCacheConfiguration { |
||||
|
|
||||
|
@Value("${redis.sentinel.master:}") |
||||
|
private String master; |
||||
|
|
||||
|
@Value("${redis.sentinel.sentinels:}") |
||||
|
private String sentinels; |
||||
|
|
||||
|
@Value("${redis.sentinel.password:}") |
||||
|
private String sentinelPassword; |
||||
|
|
||||
|
@Value("${redis.sentinel.useDefaultPoolConfig:true}") |
||||
|
private boolean useDefaultPoolConfig; |
||||
|
|
||||
|
@Value("${redis.db:}") |
||||
|
private Integer database; |
||||
|
|
||||
|
@Value("${redis.password:}") |
||||
|
private String password; |
||||
|
|
||||
|
public JedisConnectionFactory loadFactory() { |
||||
|
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); |
||||
|
redisSentinelConfiguration.setMaster(master); |
||||
|
redisSentinelConfiguration.setSentinels(getNodes(sentinels)); |
||||
|
redisSentinelConfiguration.setSentinelPassword(sentinelPassword); |
||||
|
redisSentinelConfiguration.setPassword(password); |
||||
|
redisSentinelConfiguration.setDatabase(database); |
||||
|
if (useDefaultPoolConfig) { |
||||
|
return new JedisConnectionFactory(redisSentinelConfiguration); |
||||
|
} else { |
||||
|
return new JedisConnectionFactory(redisSentinelConfiguration, buildPoolConfig()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
CACHE_TYPE=redis |
||||
|
REDIS_CONNECTION_TYPE=sentinel |
||||
|
REDIS_MASTER=mymaster |
||||
|
REDIS_SENTINELS=redis-sentinel:26379 |
||||
|
REDIS_SENTINEL_PASSWORD=sentinel |
||||
|
REDIS_USE_DEFAULT_POOL_CONFIG=false |
||||
|
REDIS_PASSWORD=thingsboard |
||||
@ -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. |
||||
|
# |
||||
|
|
||||
|
version: '3.0' |
||||
|
|
||||
|
services: |
||||
|
# Redis sentinel |
||||
|
redis-master: |
||||
|
volumes: |
||||
|
- redis-sentinel-data-master:/bitnami/redis/data |
||||
|
redis-slave: |
||||
|
volumes: |
||||
|
- redis-sentinel-data-slave:/bitnami/redis/data |
||||
|
redis-sentinel: |
||||
|
volumes: |
||||
|
- redis-sentinel-data-sentinel:/bitnami/redis/data |
||||
|
|
||||
|
volumes: |
||||
|
redis-sentinel-data-master: |
||||
|
external: |
||||
|
name: ${REDIS_SENTINEL_DATA_VOLUME_MASTER} |
||||
|
redis-sentinel-data-slave: |
||||
|
external: |
||||
|
name: ${REDIS_SENTINEL_DATA_VOLUME_SLAVE} |
||||
|
redis-sentinel-data-sentinel: |
||||
|
external: |
||||
|
name: ${REDIS_SENTINEL_DATA_VOLUME_SENTINEL} |
||||
@ -0,0 +1,119 @@ |
|||||
|
# |
||||
|
# 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. |
||||
|
# |
||||
|
|
||||
|
version: '3.0' |
||||
|
|
||||
|
services: |
||||
|
# Redis sentinel |
||||
|
redis-master: |
||||
|
image: 'bitnami/redis:7.0' |
||||
|
volumes: |
||||
|
- ./tb-node/redis-sentinel-data-master:/bitnami/redis/data |
||||
|
environment: |
||||
|
- 'REDIS_REPLICATION_MODE=master' |
||||
|
- 'REDIS_PASSWORD=thingsboard' |
||||
|
|
||||
|
redis-slave: |
||||
|
image: 'bitnami/redis:7.0' |
||||
|
volumes: |
||||
|
- ./tb-node/redis-sentinel-data-slave:/bitnami/redis/data |
||||
|
environment: |
||||
|
- 'REDIS_REPLICATION_MODE=slave' |
||||
|
- 'REDIS_MASTER_HOST=redis-master' |
||||
|
- 'REDIS_MASTER_PASSWORD=thingsboard' |
||||
|
- 'REDIS_PASSWORD=thingsboard' |
||||
|
depends_on: |
||||
|
- redis-master |
||||
|
|
||||
|
redis-sentinel: |
||||
|
image: 'bitnami/redis-sentinel:7.0' |
||||
|
volumes: |
||||
|
- ./tb-node/redis-sentinel-data-sentinel:/bitnami/redis/data |
||||
|
environment: |
||||
|
- 'REDIS_MASTER_HOST=redis-master' |
||||
|
- 'REDIS_MASTER_SET=mymaster' |
||||
|
- 'REDIS_SENTINEL_PASSWORD=sentinel' |
||||
|
- 'REDIS_MASTER_PASSWORD=thingsboard' |
||||
|
depends_on: |
||||
|
- redis-master |
||||
|
- redis-slave |
||||
|
|
||||
|
# ThingsBoard setup to use redis-sentinel |
||||
|
tb-core1: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-core2: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-rule-engine1: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-rule-engine2: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-mqtt-transport1: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-mqtt-transport2: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-http-transport1: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-http-transport2: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-coap-transport: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-lwm2m-transport: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-snmp-transport: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-vc-executor1: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
|
tb-vc-executor2: |
||||
|
env_file: |
||||
|
- cache-redis-sentinel.env |
||||
|
depends_on: |
||||
|
- redis-sentinel |
||||
@ -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. |
||||
|
*/ |
||||
|
package org.thingsboard.monitoring.config; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public interface MonitoringTarget { |
||||
|
|
||||
|
UUID getDeviceId(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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.monitoring.config.transport; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.monitoring.config.MonitoringTarget; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Data |
||||
|
public class TransportMonitoringTarget implements MonitoringTarget { |
||||
|
|
||||
|
private String baseUrl; |
||||
|
private DeviceConfig device; // set manually during initialization
|
||||
|
|
||||
|
@Override |
||||
|
public UUID getDeviceId() { |
||||
|
return device.getId(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
/** |
||||
|
* 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.monitoring.service; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.context.ApplicationContext; |
||||
|
import org.thingsboard.monitoring.client.TbClient; |
||||
|
import org.thingsboard.monitoring.client.WsClient; |
||||
|
import org.thingsboard.monitoring.client.WsClientFactory; |
||||
|
import org.thingsboard.monitoring.config.MonitoringConfig; |
||||
|
import org.thingsboard.monitoring.config.MonitoringTarget; |
||||
|
import org.thingsboard.monitoring.data.Latencies; |
||||
|
import org.thingsboard.monitoring.data.MonitoredServiceKey; |
||||
|
import org.thingsboard.monitoring.service.transport.TransportHealthChecker; |
||||
|
import org.thingsboard.monitoring.util.TbStopWatch; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import java.util.LinkedList; |
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class BaseMonitoringService<C extends MonitoringConfig<T>, T extends MonitoringTarget> { |
||||
|
|
||||
|
@Autowired |
||||
|
private List<C> configs; |
||||
|
private final List<BaseHealthChecker<C, T>> healthCheckers = new LinkedList<>(); |
||||
|
private final List<UUID> devices = new LinkedList<>(); |
||||
|
|
||||
|
@Autowired |
||||
|
private TbClient tbClient; |
||||
|
@Autowired |
||||
|
private WsClientFactory wsClientFactory; |
||||
|
@Autowired |
||||
|
private TbStopWatch stopWatch; |
||||
|
@Autowired |
||||
|
private MonitoringReporter reporter; |
||||
|
@Autowired |
||||
|
protected ApplicationContext applicationContext; |
||||
|
|
||||
|
@PostConstruct |
||||
|
private void init() { |
||||
|
tbClient.logIn(); |
||||
|
configs.forEach(config -> { |
||||
|
config.getTargets().forEach(target -> { |
||||
|
BaseHealthChecker<C, T> healthChecker = (BaseHealthChecker<C, T>) createHealthChecker(config, target); |
||||
|
log.info("Initializing {}", healthChecker.getClass().getSimpleName()); |
||||
|
healthChecker.initialize(tbClient); |
||||
|
devices.add(target.getDeviceId()); |
||||
|
healthCheckers.add(healthChecker); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public final void runChecks() { |
||||
|
if (healthCheckers.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
log.info("Starting {}", getName()); |
||||
|
stopWatch.start(); |
||||
|
String accessToken = tbClient.logIn(); |
||||
|
reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); |
||||
|
|
||||
|
try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { |
||||
|
wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); |
||||
|
|
||||
|
for (BaseHealthChecker<C, T> healthChecker : healthCheckers) { |
||||
|
healthChecker.check(wsClient); |
||||
|
} |
||||
|
} |
||||
|
reporter.reportLatencies(tbClient); |
||||
|
log.debug("Finished {}", getName()); |
||||
|
} catch (Throwable error) { |
||||
|
try { |
||||
|
reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); |
||||
|
} catch (Throwable reportError) { |
||||
|
log.error("Error occurred during service failure reporting", reportError); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract BaseHealthChecker<?, ?> createHealthChecker(C config, T target); |
||||
|
|
||||
|
protected abstract String getName(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
/** |
||||
|
* 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.monitoring.service.transport; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.TextNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.RandomStringUtils; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.monitoring.client.TbClient; |
||||
|
import org.thingsboard.monitoring.config.transport.DeviceConfig; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportInfo; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportType; |
||||
|
import org.thingsboard.monitoring.service.BaseHealthChecker; |
||||
|
import org.thingsboard.monitoring.util.ResourceUtils; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.TbResource; |
||||
|
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; |
||||
|
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; |
||||
|
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; |
||||
|
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; |
||||
|
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; |
||||
|
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; |
||||
|
import org.thingsboard.server.common.data.device.data.DeviceData; |
||||
|
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentials; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentialsType; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class TransportHealthChecker<C extends TransportMonitoringConfig> extends BaseHealthChecker<C, TransportMonitoringTarget> { |
||||
|
|
||||
|
private static final String DEFAULT_DEVICE_NAME = "[Monitoring] %s transport (%s)"; |
||||
|
private static final String DEFAULT_PROFILE_NAME = "[Monitoring] %s"; |
||||
|
|
||||
|
public TransportHealthChecker(C config, TransportMonitoringTarget target) { |
||||
|
super(config, target); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void initialize(TbClient tbClient) { |
||||
|
String deviceName = String.format(DEFAULT_DEVICE_NAME, config.getTransportType(), target.getBaseUrl()); |
||||
|
Device device = tbClient.getTenantDevice(deviceName) |
||||
|
.orElseGet(() -> { |
||||
|
log.info("Creating new device '{}'", deviceName); |
||||
|
return createDevice(config.getTransportType(), deviceName, tbClient); |
||||
|
}); |
||||
|
DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId()) |
||||
|
.orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId())); |
||||
|
|
||||
|
DeviceConfig deviceConfig = new DeviceConfig(); |
||||
|
deviceConfig.setId(device.getId().toString()); |
||||
|
deviceConfig.setName(deviceName); |
||||
|
deviceConfig.setCredentials(credentials); |
||||
|
target.setDevice(deviceConfig); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected String createTestPayload(String testValue) { |
||||
|
return JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Object getInfo() { |
||||
|
return new TransportInfo(getTransportType(), target.getBaseUrl()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected String getKey() { |
||||
|
return getTransportType().name().toLowerCase() + "Transport"; |
||||
|
} |
||||
|
|
||||
|
protected abstract TransportType getTransportType(); |
||||
|
|
||||
|
|
||||
|
private Device createDevice(TransportType transportType, String name, TbClient tbClient) { |
||||
|
Device device = new Device(); |
||||
|
device.setName(name); |
||||
|
|
||||
|
DeviceCredentials credentials = new DeviceCredentials(); |
||||
|
credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); |
||||
|
|
||||
|
DeviceData deviceData = new DeviceData(); |
||||
|
deviceData.setConfiguration(new DefaultDeviceConfiguration()); |
||||
|
if (transportType != TransportType.LWM2M) { |
||||
|
device.setType("default"); |
||||
|
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); |
||||
|
credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); |
||||
|
} else { |
||||
|
tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() |
||||
|
.stream().findFirst() |
||||
|
.orElseGet(() -> { |
||||
|
TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); |
||||
|
log.info("Creating LwM2M resource"); |
||||
|
return tbClient.saveResource(newResource); |
||||
|
}); |
||||
|
String profileName = String.format(DEFAULT_PROFILE_NAME, transportType); |
||||
|
DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() |
||||
|
.stream().findFirst() |
||||
|
.orElseGet(() -> { |
||||
|
DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); |
||||
|
newProfile.setName(profileName); |
||||
|
log.info("Creating LwM2M device profile"); |
||||
|
return tbClient.saveDeviceProfile(newProfile); |
||||
|
}); |
||||
|
device.setType(profileName); |
||||
|
device.setDeviceProfileId(profile.getId()); |
||||
|
deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); |
||||
|
|
||||
|
credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); |
||||
|
LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); |
||||
|
NoSecClientCredential client = new NoSecClientCredential(); |
||||
|
client.setEndpoint(credentials.getCredentialsId()); |
||||
|
lwm2mCreds.setClient(client); |
||||
|
LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); |
||||
|
bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); |
||||
|
bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); |
||||
|
lwm2mCreds.setBootstrap(bootstrap); |
||||
|
credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); |
||||
|
} |
||||
|
return tbClient.saveDeviceWithCredentials(device, credentials).get(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* 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.monitoring.service.transport; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; |
||||
|
import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; |
||||
|
import org.thingsboard.monitoring.service.BaseHealthChecker; |
||||
|
import org.thingsboard.monitoring.service.BaseMonitoringService; |
||||
|
|
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
@Slf4j |
||||
|
public final class TransportsMonitoringService extends BaseMonitoringService<TransportMonitoringConfig, TransportMonitoringTarget> { |
||||
|
|
||||
|
@Override |
||||
|
protected BaseHealthChecker<?, ?> createHealthChecker(TransportMonitoringConfig config, TransportMonitoringTarget target) { |
||||
|
return applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected String getName() { |
||||
|
return "transports check"; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,198 +0,0 @@ |
|||||
/** |
|
||||
* 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.monitoring.transport; |
|
||||
|
|
||||
import lombok.RequiredArgsConstructor; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.apache.commons.lang3.RandomStringUtils; |
|
||||
import org.apache.commons.lang3.StringUtils; |
|
||||
import org.springframework.beans.factory.annotation.Value; |
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent; |
|
||||
import org.springframework.context.ApplicationContext; |
|
||||
import org.springframework.context.event.EventListener; |
|
||||
import org.springframework.stereotype.Service; |
|
||||
import org.thingsboard.common.util.JacksonUtil; |
|
||||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|
||||
import org.thingsboard.monitoring.client.TbClient; |
|
||||
import org.thingsboard.monitoring.client.WsClient; |
|
||||
import org.thingsboard.monitoring.client.WsClientFactory; |
|
||||
import org.thingsboard.monitoring.config.DeviceConfig; |
|
||||
import org.thingsboard.monitoring.config.MonitoringTargetConfig; |
|
||||
import org.thingsboard.monitoring.config.TransportType; |
|
||||
import org.thingsboard.monitoring.config.service.TransportMonitoringConfig; |
|
||||
import org.thingsboard.monitoring.data.Latencies; |
|
||||
import org.thingsboard.monitoring.data.MonitoredServiceKey; |
|
||||
import org.thingsboard.monitoring.service.MonitoringReporter; |
|
||||
import org.thingsboard.monitoring.util.ResourceUtils; |
|
||||
import org.thingsboard.monitoring.util.TbStopWatch; |
|
||||
import org.thingsboard.server.common.data.Device; |
|
||||
import org.thingsboard.server.common.data.DeviceProfile; |
|
||||
import org.thingsboard.server.common.data.TbResource; |
|
||||
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; |
|
||||
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; |
|
||||
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; |
|
||||
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; |
|
||||
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; |
|
||||
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; |
|
||||
import org.thingsboard.server.common.data.device.data.DeviceData; |
|
||||
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; |
|
||||
import org.thingsboard.server.common.data.id.DeviceId; |
|
||||
import org.thingsboard.server.common.data.page.PageLink; |
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials; |
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType; |
|
||||
|
|
||||
import javax.annotation.PostConstruct; |
|
||||
import java.util.LinkedList; |
|
||||
import java.util.List; |
|
||||
import java.util.UUID; |
|
||||
import java.util.concurrent.Executors; |
|
||||
import java.util.concurrent.ScheduledExecutorService; |
|
||||
import java.util.concurrent.TimeUnit; |
|
||||
|
|
||||
@Service |
|
||||
@RequiredArgsConstructor |
|
||||
@Slf4j |
|
||||
public final class TransportMonitoringService { |
|
||||
|
|
||||
private final List<TransportMonitoringConfig> configs; |
|
||||
private final List<TransportHealthChecker<?>> transportHealthCheckers = new LinkedList<>(); |
|
||||
private final List<UUID> devices = new LinkedList<>(); |
|
||||
|
|
||||
private final TbClient tbClient; |
|
||||
private final WsClientFactory wsClientFactory; |
|
||||
private final TbStopWatch stopWatch; |
|
||||
private final MonitoringReporter reporter; |
|
||||
private final ApplicationContext applicationContext; |
|
||||
private ScheduledExecutorService scheduler; |
|
||||
@Value("${monitoring.transports.monitoring_rate_ms}") |
|
||||
private int monitoringRateMs; |
|
||||
|
|
||||
@PostConstruct |
|
||||
private void init() { |
|
||||
configs.forEach(config -> { |
|
||||
config.getTargets().stream() |
|
||||
.filter(target -> StringUtils.isNotBlank(target.getBaseUrl())) |
|
||||
.peek(target -> checkMonitoringTarget(config, target, tbClient)) |
|
||||
.forEach(target -> { |
|
||||
TransportHealthChecker<?> transportHealthChecker = applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); |
|
||||
transportHealthCheckers.add(transportHealthChecker); |
|
||||
devices.add(target.getDevice().getId()); |
|
||||
}); |
|
||||
}); |
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); |
|
||||
} |
|
||||
|
|
||||
@EventListener(ApplicationReadyEvent.class) |
|
||||
public void startMonitoring() { |
|
||||
scheduler.scheduleWithFixedDelay(() -> { |
|
||||
try { |
|
||||
log.debug("Starting transports check"); |
|
||||
stopWatch.start(); |
|
||||
String accessToken = tbClient.logIn(); |
|
||||
reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); |
|
||||
|
|
||||
try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { |
|
||||
wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); |
|
||||
|
|
||||
for (TransportHealthChecker<?> transportHealthChecker : transportHealthCheckers) { |
|
||||
transportHealthChecker.check(wsClient); |
|
||||
} |
|
||||
} |
|
||||
reporter.reportLatencies(tbClient); |
|
||||
log.debug("Finished transports check"); |
|
||||
} catch (Throwable error) { |
|
||||
try { |
|
||||
reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); |
|
||||
} catch (Throwable reportError) { |
|
||||
log.error("Error occurred during service failure reporting", reportError); |
|
||||
} |
|
||||
} |
|
||||
}, 0, monitoringRateMs, TimeUnit.MILLISECONDS); |
|
||||
} |
|
||||
|
|
||||
private void checkMonitoringTarget(TransportMonitoringConfig config, MonitoringTargetConfig target, TbClient tbClient) { |
|
||||
DeviceConfig deviceConfig = target.getDevice(); |
|
||||
tbClient.logIn(); |
|
||||
|
|
||||
DeviceId deviceId; |
|
||||
if (deviceConfig == null || deviceConfig.getId() == null) { |
|
||||
String deviceName = String.format("[%s] Monitoring device (%s)", config.getTransportType(), target.getBaseUrl()); |
|
||||
Device device = tbClient.getTenantDevice(deviceName) |
|
||||
.orElseGet(() -> { |
|
||||
log.info("Creating new device '{}'", deviceName); |
|
||||
return createDevice(config.getTransportType(), deviceName, tbClient); |
|
||||
}); |
|
||||
deviceId = device.getId(); |
|
||||
target.getDevice().setId(deviceId.toString()); |
|
||||
} else { |
|
||||
deviceId = new DeviceId(deviceConfig.getId()); |
|
||||
} |
|
||||
|
|
||||
log.info("Using device {} for {} monitoring", deviceId, config.getTransportType()); |
|
||||
DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId) |
|
||||
.orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId)); |
|
||||
target.getDevice().setCredentials(credentials); |
|
||||
} |
|
||||
|
|
||||
private Device createDevice(TransportType transportType, String name, TbClient tbClient) { |
|
||||
Device device = new Device(); |
|
||||
device.setName(name); |
|
||||
|
|
||||
DeviceCredentials credentials = new DeviceCredentials(); |
|
||||
credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); |
|
||||
|
|
||||
DeviceData deviceData = new DeviceData(); |
|
||||
deviceData.setConfiguration(new DefaultDeviceConfiguration()); |
|
||||
if (transportType != TransportType.LWM2M) { |
|
||||
device.setType("default"); |
|
||||
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); |
|
||||
credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); |
|
||||
} else { |
|
||||
tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() |
|
||||
.stream().findFirst() |
|
||||
.orElseGet(() -> { |
|
||||
TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); |
|
||||
log.info("Creating LwM2M resource"); |
|
||||
return tbClient.saveResource(newResource); |
|
||||
}); |
|
||||
String profileName = "LwM2M Monitoring"; |
|
||||
DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() |
|
||||
.stream().findFirst() |
|
||||
.orElseGet(() -> { |
|
||||
DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); |
|
||||
newProfile.setName(profileName); |
|
||||
log.info("Creating LwM2M device profile"); |
|
||||
return tbClient.saveDeviceProfile(newProfile); |
|
||||
}); |
|
||||
device.setType(profileName); |
|
||||
device.setDeviceProfileId(profile.getId()); |
|
||||
deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); |
|
||||
|
|
||||
credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); |
|
||||
LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); |
|
||||
NoSecClientCredential client = new NoSecClientCredential(); |
|
||||
client.setEndpoint(credentials.getCredentialsId()); |
|
||||
lwm2mCreds.setClient(client); |
|
||||
LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); |
|
||||
bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); |
|
||||
bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); |
|
||||
lwm2mCreds.setBootstrap(bootstrap); |
|
||||
credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); |
|
||||
} |
|
||||
return tbClient.saveDeviceWithCredentials(device, credentials).get(); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,79 @@ |
|||||
|
/** |
||||
|
* 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.msa.ui.tests.devicessmoke; |
||||
|
|
||||
|
import io.qameta.allure.Description; |
||||
|
import io.qameta.allure.Feature; |
||||
|
import org.openqa.selenium.WebElement; |
||||
|
import org.testng.annotations.AfterClass; |
||||
|
import org.testng.annotations.BeforeMethod; |
||||
|
import org.testng.annotations.Test; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; |
||||
|
import org.thingsboard.server.msa.ui.utils.EntityPrototypes; |
||||
|
|
||||
|
import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; |
||||
|
import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; |
||||
|
import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; |
||||
|
|
||||
|
@Feature("Make device private") |
||||
|
public class MakeDevicePrivateTest extends AbstractDeviceTest { |
||||
|
|
||||
|
private CustomerPageHelper customerPage; |
||||
|
|
||||
|
@BeforeMethod |
||||
|
public void createPublicDevice() { |
||||
|
customerPage = new CustomerPageHelper(driver); |
||||
|
Device device = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())); |
||||
|
testRestClient.setDevicePublic(device.getId()); |
||||
|
deviceName = device.getName(); |
||||
|
} |
||||
|
|
||||
|
@AfterClass |
||||
|
public void deletePublicCustomer() { |
||||
|
deleteCustomerByName(PUBLIC_CUSTOMER_NAME); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke") |
||||
|
@Description("Make device private by right side btn") |
||||
|
public void makeDevicePrivateByRightSideBtn() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.makeDevicePrivateByRightSideBtn(deviceName); |
||||
|
WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); |
||||
|
assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); |
||||
|
assertInvisibilityOfElement(customerInColumn); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
devicePage.assertEntityIsNotPresent(deviceName); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke") |
||||
|
@Description("Make device public by btn on details tab") |
||||
|
public void makeDevicePrivateFromDetailsTab() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.device(deviceName).click(); |
||||
|
WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); |
||||
|
devicePage.makeDevicePrivateFromDetailsTab(); |
||||
|
devicePage.closeDeviceDetailsViewBtn().click(); |
||||
|
assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); |
||||
|
assertInvisibilityOfElement(customerInColumn); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
devicePage.assertEntityIsNotPresent(deviceName); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
/** |
||||
|
* 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.msa.ui.tests.devicessmoke; |
||||
|
|
||||
|
import io.qameta.allure.Description; |
||||
|
import io.qameta.allure.Feature; |
||||
|
import org.testng.annotations.AfterClass; |
||||
|
import org.testng.annotations.BeforeClass; |
||||
|
import org.testng.annotations.BeforeMethod; |
||||
|
import org.testng.annotations.Test; |
||||
|
import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; |
||||
|
import org.thingsboard.server.msa.ui.tabs.AssignDeviceTabHelper; |
||||
|
import org.thingsboard.server.msa.ui.utils.EntityPrototypes; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; |
||||
|
import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; |
||||
|
import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; |
||||
|
|
||||
|
@Feature("Make device public") |
||||
|
public class MakeDevicePublicTest extends AbstractDeviceTest { |
||||
|
|
||||
|
private CustomerPageHelper customerPage; |
||||
|
private AssignDeviceTabHelper assignDeviceTab; |
||||
|
private String deviceName1; |
||||
|
|
||||
|
@BeforeClass |
||||
|
public void createFirstDevice() { |
||||
|
customerPage = new CustomerPageHelper(driver); |
||||
|
assignDeviceTab = new AssignDeviceTabHelper(driver); |
||||
|
|
||||
|
deviceName1 = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); |
||||
|
} |
||||
|
|
||||
|
@AfterClass |
||||
|
public void cleanUp() { |
||||
|
deleteCustomerByName(PUBLIC_CUSTOMER_NAME); |
||||
|
deleteDeviceByName(deviceName1); |
||||
|
} |
||||
|
|
||||
|
@BeforeMethod |
||||
|
public void createSecondDevice() { |
||||
|
deviceName = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke", priority = 10) |
||||
|
@Description("Make device public by right side btn") |
||||
|
public void makeDevicePublicByRightSideBtn() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.makeDevicePublicByRightSideBtn(deviceName); |
||||
|
|
||||
|
assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); |
||||
|
assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); |
||||
|
assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) |
||||
|
.as("Customer in customer column is Public customer") |
||||
|
.isEqualTo(PUBLIC_CUSTOMER_NAME); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
assertIsDisplayed(devicePage.device(deviceName)); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke", priority = 10) |
||||
|
@Description("Make device public by btn on details tab") |
||||
|
public void makeDevicePublicFromDetailsTab() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.device(deviceName).click(); |
||||
|
devicePage.makeDevicePublicFromDetailsTab(); |
||||
|
devicePage.closeDeviceDetailsViewBtn().click(); |
||||
|
|
||||
|
assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); |
||||
|
assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); |
||||
|
assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) |
||||
|
.as("Customer in customer column is Public customer") |
||||
|
.isEqualTo(PUBLIC_CUSTOMER_NAME); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
assertIsDisplayed(devicePage.device(deviceName)); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke", priority = 20) |
||||
|
@Description("Make device public by assign to public customer") |
||||
|
public void makeDevicePublicByAssignToPublicCustomer() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.assignBtn(deviceName).click(); |
||||
|
assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); |
||||
|
assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); |
||||
|
assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); |
||||
|
assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()).isEqualTo(PUBLIC_CUSTOMER_NAME); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
assertIsDisplayed(devicePage.device(deviceName)); |
||||
|
} |
||||
|
|
||||
|
@Test(groups = "smoke", priority = 20) |
||||
|
@Description("Make several devices public by assign to public customer") |
||||
|
public void makePublicSeveralDevicesByAssignOnPublicCustomer() { |
||||
|
sideBarMenuView.goToDevicesPage(); |
||||
|
devicePage.assignSelectedDevices(deviceName, deviceName1); |
||||
|
assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); |
||||
|
assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); |
||||
|
assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); |
||||
|
assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) |
||||
|
.as("Customer in customer column is Public customer") |
||||
|
.isEqualTo(PUBLIC_CUSTOMER_NAME); |
||||
|
|
||||
|
sideBarMenuView.customerBtn().click(); |
||||
|
customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); |
||||
|
assertIsDisplayed(devicePage.device(deviceName)); |
||||
|
assertIsDisplayed(devicePage.device(deviceName1)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="entitiesTableWidgetConfigForm"> |
||||
|
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig" |
||||
|
[onlyHistoryTimewindow]="onlyHistoryTimewindow()" |
||||
|
formControlName="timewindowConfig"> |
||||
|
</tb-timewindow-config-panel> |
||||
|
<tb-datasources |
||||
|
[configMode]="basicMode" |
||||
|
hideDataKeys |
||||
|
formControlName="datasources"> |
||||
|
</tb-datasources> |
||||
|
<tb-data-keys-panel |
||||
|
panelTitle="{{ 'widgets.table.columns' | translate }}" |
||||
|
addKeyTitle="{{ 'widgets.table.add-column' | translate }}" |
||||
|
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}" |
||||
|
noKeysText="{{ 'widgets.table.no-columns' | translate }}" |
||||
|
hideDataKeyColor |
||||
|
[datasourceType]="datasource?.type" |
||||
|
[deviceId]="datasource?.deviceId" |
||||
|
[entityAliasId]="datasource?.entityAliasId" |
||||
|
formControlName="columns"> |
||||
|
</tb-data-keys-panel> |
||||
|
<div class="tb-widget-config-panel"> |
||||
|
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div> |
||||
|
<div class="tb-widget-config-row"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTitle"> |
||||
|
{{ 'widget-config.card-title' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-widget-config-row space-between same-padding"> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon"> |
||||
|
{{ 'widget-config.card-icon' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
||||
|
<tb-material-icon-select asBoxInput |
||||
|
[color]="entitiesTableWidgetConfigForm.get('iconColor').value" |
||||
|
formControlName="titleIcon"> |
||||
|
</tb-material-icon-select> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="iconColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-widget-config-row space-between same-padding"> |
||||
|
<div>{{ 'widget-config.text-color' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="color"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-widget-config-row space-between same-padding"> |
||||
|
<div>{{ 'widget-config.background' | translate }}</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="backgroundColor"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-widget-actions-panel |
||||
|
formControlName="actions"> |
||||
|
</tb-widget-actions-panel> |
||||
|
</ng-container> |
||||
@ -0,0 +1,154 @@ |
|||||
|
///
|
||||
|
/// 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 { UntypedFormBuilder, UntypedFormGroup } 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, |
||||
|
datasourcesHasAggregation, |
||||
|
datasourcesHasOnlyComparisonAggregation |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entities-table-basic-config', |
||||
|
templateUrl: './entities-table-basic-config.component.html', |
||||
|
styleUrls: ['../basic-config.scss'] |
||||
|
}) |
||||
|
export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponent { |
||||
|
|
||||
|
public get displayTimewindowConfig(): boolean { |
||||
|
const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
public onlyHistoryTimewindow(): boolean { |
||||
|
const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; |
||||
|
return datasourcesHasOnlyComparisonAggregation(datasources); |
||||
|
} |
||||
|
|
||||
|
public get datasource(): Datasource { |
||||
|
const datasources: Datasource[] = this.entitiesTableWidgetConfigForm.get('datasources').value; |
||||
|
if (datasources && datasources.length) { |
||||
|
return datasources[0]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
entitiesTableWidgetConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected widgetConfigComponent: WidgetConfigComponent, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store, widgetConfigComponent); |
||||
|
} |
||||
|
|
||||
|
protected configForm(): UntypedFormGroup { |
||||
|
return this.entitiesTableWidgetConfigForm; |
||||
|
} |
||||
|
|
||||
|
protected setupDefaults(configData: WidgetConfigComponentData) { |
||||
|
this.setupDefaultDatasource(configData, [{ name: 'name', type: DataKeyType.entityField }]); |
||||
|
} |
||||
|
|
||||
|
protected onConfigSet(configData: WidgetConfigComponentData) { |
||||
|
this.entitiesTableWidgetConfigForm = this.fb.group({ |
||||
|
timewindowConfig: [{ |
||||
|
useDashboardTimewindow: configData.config.useDashboardTimewindow, |
||||
|
displayTimewindow: configData.config.useDashboardTimewindow, |
||||
|
timewindow: configData.config.timewindow |
||||
|
}, []], |
||||
|
datasources: [configData.config.datasources, []], |
||||
|
columns: [this.getColumns(configData.config.datasources), []], |
||||
|
showTitle: [configData.config.showTitle, []], |
||||
|
title: [configData.config.settings?.entitiesTitle, []], |
||||
|
showTitleIcon: [configData.config.showTitleIcon, []], |
||||
|
titleIcon: [configData.config.titleIcon, []], |
||||
|
iconColor: [configData.config.iconColor, []], |
||||
|
color: [configData.config.color, []], |
||||
|
backgroundColor: [configData.config.backgroundColor, []], |
||||
|
actions: [configData.config.actions || {}, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
||||
|
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; |
||||
|
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; |
||||
|
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; |
||||
|
this.widgetConfig.config.datasources = config.datasources; |
||||
|
this.setColumns(config.columns, this.widgetConfig.config.datasources); |
||||
|
this.widgetConfig.config.actions = config.actions; |
||||
|
this.widgetConfig.config.showTitle = config.showTitle; |
||||
|
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
||||
|
this.widgetConfig.config.settings.entitiesTitle = config.title; |
||||
|
this.widgetConfig.config.showTitleIcon = config.showTitleIcon; |
||||
|
this.widgetConfig.config.titleIcon = config.titleIcon; |
||||
|
this.widgetConfig.config.iconColor = config.iconColor; |
||||
|
this.widgetConfig.config.color = config.color; |
||||
|
this.widgetConfig.config.backgroundColor = config.backgroundColor; |
||||
|
return this.widgetConfig; |
||||
|
} |
||||
|
|
||||
|
protected validatorTriggers(): string[] { |
||||
|
return ['showTitle', 'showTitleIcon']; |
||||
|
} |
||||
|
|
||||
|
protected updateValidators(emitEvent: boolean, trigger?: string) { |
||||
|
const showTitle: boolean = this.entitiesTableWidgetConfigForm.get('showTitle').value; |
||||
|
const showTitleIcon: boolean = this.entitiesTableWidgetConfigForm.get('showTitleIcon').value; |
||||
|
if (showTitle) { |
||||
|
this.entitiesTableWidgetConfigForm.get('title').enable(); |
||||
|
this.entitiesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); |
||||
|
if (showTitleIcon) { |
||||
|
this.entitiesTableWidgetConfigForm.get('titleIcon').enable(); |
||||
|
this.entitiesTableWidgetConfigForm.get('iconColor').enable(); |
||||
|
} else { |
||||
|
this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); |
||||
|
this.entitiesTableWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
} else { |
||||
|
this.entitiesTableWidgetConfigForm.get('title').disable(); |
||||
|
this.entitiesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); |
||||
|
this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); |
||||
|
this.entitiesTableWidgetConfigForm.get('iconColor').disable(); |
||||
|
} |
||||
|
this.entitiesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); |
||||
|
this.entitiesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); |
||||
|
this.entitiesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); |
||||
|
this.entitiesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); |
||||
|
} |
||||
|
|
||||
|
private getColumns(datasources?: Datasource[]): DataKey[] { |
||||
|
if (datasources && datasources.length) { |
||||
|
return datasources[0].dataKeys || []; |
||||
|
} |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
private setColumns(columns: DataKey[], datasources?: Datasource[]) { |
||||
|
if (datasources && datasources.length) { |
||||
|
datasources[0].dataKeys = columns; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-data-key-row"> |
||||
|
<mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic"> |
||||
|
<mat-chip-grid #chipList> |
||||
|
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type" |
||||
|
(removed)="removeKey()"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"> |
||||
|
<div class="tb-chip-labels"> |
||||
|
<div class="tb-chip-label"> |
||||
|
<ng-container *ngIf="isEntityDatasource"> |
||||
|
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.alarm" |
||||
|
matTooltip="{{'datakey.alarm' | translate }}" |
||||
|
matTooltipPosition="above">notifications</mat-icon> |
||||
|
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.attribute" |
||||
|
matTooltip="{{'datakey.attributes' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon> |
||||
|
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.entityField" |
||||
|
matTooltip="{{'datakey.entity-field' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon> |
||||
|
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.timeseries" |
||||
|
matTooltip="{{'datakey.timeseries' | translate }}" |
||||
|
matTooltipPosition="above">timeline</mat-icon> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<div class="tb-chip-label"> |
||||
|
<strong> |
||||
|
<ng-container *ngTemplateOutlet="keyName"></ng-container> |
||||
|
</strong> |
||||
|
</div> |
||||
|
</div> |
||||
|
<button type="button" |
||||
|
(click)="editKey()" mat-icon-button class="tb-mat-24"> |
||||
|
<mat-icon class="tb-mat-18">edit</mat-icon> |
||||
|
</button> |
||||
|
<button matChipRemove |
||||
|
type="button" |
||||
|
mat-icon-button class="tb-mat-24"> |
||||
|
<mat-icon class="tb-mat-18">close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-chip-row> |
||||
|
<input matInput |
||||
|
type="text" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}" |
||||
|
#keyInput |
||||
|
[formControl]="keyFormControl" |
||||
|
matAutocompleteOrigin |
||||
|
[fxHide]="!!modelValue.type" |
||||
|
[readonly]="!!modelValue.type" |
||||
|
#origin="matAutocompleteOrigin" |
||||
|
[matAutocompleteConnectedTo]="origin" |
||||
|
(focusin)="onKeyInputFocus()" |
||||
|
(drop)="$event.preventDefault();" |
||||
|
[matAutocomplete]="keyAutocomplete" |
||||
|
[matChipInputFor]="chipList" |
||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
||||
|
(matChipInputTokenEnd)="addKey($event)" |
||||
|
/> |
||||
|
</mat-chip-grid> |
||||
|
<mat-autocomplete #keyAutocomplete="matAutocomplete" |
||||
|
class="tb-autocomplete" |
||||
|
panelWidth="fit-content" |
||||
|
[displayWith]="displayKeyFn"> |
||||
|
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> |
||||
|
<span style="white-space: nowrap;"> |
||||
|
<ng-container *ngIf="isEntityDatasource"> |
||||
|
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm" |
||||
|
matTooltip="{{'datakey.alarm' | translate }}" |
||||
|
matTooltipPosition="above">notifications</mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute" |
||||
|
matTooltip="{{'datakey.attributes' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField" |
||||
|
matTooltip="{{'datakey.entity-field' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries" |
||||
|
matTooltip="{{'datakey.timeseries' | translate }}" |
||||
|
matTooltipPosition="above">timeline</mat-icon> |
||||
|
</ng-container> |
||||
|
<span [innerHTML]="key.name | highlight:keySearchText"></span> |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredKeys | async)?.length" [value]="null" class="tb-not-found"> |
||||
|
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
||||
|
<div *ngIf="!textIsNotEmpty(keySearchText); else searchNotEmpty"> |
||||
|
<span translate>entity.no-keys-found</span> |
||||
|
</div> |
||||
|
<ng-template #searchNotEmpty> |
||||
|
<span> |
||||
|
{{ translate.get('entity.no-key-matching', |
||||
|
{key: truncate.transform(keySearchText, true, 6, '...')}) | async }} |
||||
|
</span> |
||||
|
<span *ngIf="!isEntityDatasource; else createEntityKey"> |
||||
|
<a translate (click)="createKey(keySearchText)">entity.create-new-key</a> |
||||
|
</span> |
||||
|
<ng-template #createEntityKey> |
||||
|
<span>{{'entity.create-new-key' | translate }} </span> |
||||
|
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.alarm" |
||||
|
matTooltip="{{'datakey.alarm' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="createKey(keySearchText, dataKeyTypes.alarm)">notifications</mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm" |
||||
|
matTooltip="{{'datakey.attributes' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline" |
||||
|
(click)="createKey(keySearchText, dataKeyTypes.attribute)"></mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm" |
||||
|
matTooltip="{{'datakey.entity-field' | translate }}" |
||||
|
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline" |
||||
|
(click)="createKey(keySearchText, dataKeyTypes.entityField)"></mat-icon> |
||||
|
<mat-icon class="tb-datakey-icon new-key" |
||||
|
matTooltip="{{'datakey.timeseries' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="createKey(keySearchText, dataKeyTypes.timeseries)">timeline</mat-icon> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex class="tb-inline-field" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<div *ngIf="!hideDataKeyColor" class="tb-color-field"> |
||||
|
<tb-color-input asBoxInput |
||||
|
formControlName="color"> |
||||
|
</tb-color-input> |
||||
|
</div> |
||||
|
<div class="tb-units-field"> |
||||
|
<tb-widget-units *ngIf="displayUnitsOrDigits" |
||||
|
formControlName="units"> |
||||
|
</tb-widget-units> |
||||
|
</div> |
||||
|
<div class="tb-decimals-field"> |
||||
|
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field center 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> |
||||
|
<ng-template #keyName> |
||||
|
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName"> |
||||
|
<span>f(</span><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><span>)</span> |
||||
|
</ng-container> |
||||
|
<ng-template #keyName> |
||||
|
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
<ng-template #keyNameTemplate> |
||||
|
<ng-container *ngIf="dataKeyHasAggregation(); else keyName;"> |
||||
|
<span class="tb-agg-func">{{ modelValue?.aggregationType }}</span><span>({{ modelValue?.name }})</span> |
||||
|
</ng-container> |
||||
|
<ng-template #keyName> |
||||
|
<span>{{modelValue?.name}}</span> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
@ -0,0 +1,53 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
.tb-data-key-row { |
||||
|
height: 38px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 12px; |
||||
|
padding-left: 12px; |
||||
|
|
||||
|
.mat-mdc-form-field.tb-inline-field.tb-key-field { |
||||
|
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { |
||||
|
.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-color-field, .tb-units-field, .tb-decimals-field { |
||||
|
width: 60px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,426 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
SimpleChanges, |
||||
|
ViewChild, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors |
||||
|
} from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; |
||||
|
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { AggregationType } from '@shared/models/time/time.models'; |
||||
|
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
||||
|
import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
||||
|
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; |
||||
|
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 } from '@core/utils'; |
||||
|
import { Dashboard } from '@shared/models/dashboard.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
|
||||
|
export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => { |
||||
|
const dataKey: DataKey = control.value; |
||||
|
if (!dataKey || !dataKey.type || !dataKey.name) { |
||||
|
return { |
||||
|
dataKey: true |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-data-key-row', |
||||
|
templateUrl: './data-key-row.component.html', |
||||
|
styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DataKeyRowComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { |
||||
|
|
||||
|
dataKeyTypes = DataKeyType; |
||||
|
widgetTypes = widgetType; |
||||
|
|
||||
|
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
||||
|
|
||||
|
@ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>; |
||||
|
@ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; |
||||
|
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; |
||||
|
@ViewChild('chipList') chipList: MatChipGrid; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
datasourceType: DatasourceType; |
||||
|
|
||||
|
@Input() |
||||
|
entityAliasId: string; |
||||
|
|
||||
|
@Input() |
||||
|
deviceId: string; |
||||
|
|
||||
|
keyFormControl: UntypedFormControl; |
||||
|
|
||||
|
keyRowFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
modelValue: DataKey; |
||||
|
|
||||
|
filteredKeys: Observable<Array<DataKey>>; |
||||
|
|
||||
|
keySearchText = ''; |
||||
|
|
||||
|
private latestKeySearchTextResult: Array<DataKey> = null; |
||||
|
private keyFetchObservable$: Observable<Array<DataKey>> = null; |
||||
|
|
||||
|
get dataKeyType(): DataKeyType { |
||||
|
return this.dataKeysPanelComponent.dataKeyType; |
||||
|
} |
||||
|
|
||||
|
get alarmKeys(): Array<DataKey> { |
||||
|
return this.dataKeysPanelComponent.alarmKeys; |
||||
|
} |
||||
|
|
||||
|
get functionTypeKeys(): Array<DataKey> { |
||||
|
return this.dataKeysPanelComponent.functionTypeKeys; |
||||
|
} |
||||
|
|
||||
|
get hideDataKeyColor(): boolean { |
||||
|
return this.dataKeysPanelComponent.hideDataKeyColor; |
||||
|
} |
||||
|
|
||||
|
get widgetType(): widgetType { |
||||
|
return this.widgetConfigComponent.widgetType; |
||||
|
} |
||||
|
|
||||
|
get callbacks(): DataKeysCallbacks { |
||||
|
return this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
} |
||||
|
|
||||
|
get widget(): Widget { |
||||
|
return this.widgetConfigComponent.widget; |
||||
|
} |
||||
|
|
||||
|
get dashboard(): Dashboard { |
||||
|
return this.widgetConfigComponent.dashboard; |
||||
|
} |
||||
|
|
||||
|
get aliasController(): IAliasController { |
||||
|
return this.widgetConfigComponent.aliasController; |
||||
|
} |
||||
|
|
||||
|
get datakeySettingsSchema(): JsonSettingsSchema { |
||||
|
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; |
||||
|
} |
||||
|
|
||||
|
get dataKeySettingsDirective(): string { |
||||
|
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; |
||||
|
} |
||||
|
|
||||
|
get isEntityDatasource(): boolean { |
||||
|
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); |
||||
|
} |
||||
|
|
||||
|
get displayUnitsOrDigits() { |
||||
|
return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); |
||||
|
} |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private dialog: MatDialog, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
public translate: TranslateService, |
||||
|
public truncate: TruncatePipe, |
||||
|
private dataKeysPanelComponent: DataKeysPanelComponent, |
||||
|
private widgetConfigComponent: WidgetConfigComponent) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.keyFormControl = this.fb.control(''); |
||||
|
this.keyRowFormGroup = this.fb.group({ |
||||
|
label: [null, []], |
||||
|
color: [null, []], |
||||
|
units: [null, []], |
||||
|
decimals: [null, []], |
||||
|
}); |
||||
|
this.keyRowFormGroup.valueChanges.subscribe( |
||||
|
() => this.updateModel() |
||||
|
); |
||||
|
this.filteredKeys = this.keyFormControl.valueChanges |
||||
|
.pipe( |
||||
|
tap((value: string | DataKey) => { |
||||
|
if (value && typeof value !== 'string') { |
||||
|
this.addKeyFromChipValue(value); |
||||
|
} else if (value === null) { |
||||
|
this.clearKeyChip(this.keyInput.nativeElement.value); |
||||
|
} |
||||
|
}), |
||||
|
filter((value) => typeof value === 'string'), |
||||
|
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
mergeMap(name => this.fetchKeys(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private reset() { |
||||
|
if (this.keyInput) { |
||||
|
this.keyInput.nativeElement.value = ''; |
||||
|
} |
||||
|
this.keyFormControl.patchValue('', {emitEvent: false}); |
||||
|
this.latestKeySearchTextResult = null; |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['deviceId', 'entityAliasId'].includes(propName)) { |
||||
|
this.clearKeySearchCache(); |
||||
|
} else if (['datasourceType'].includes(propName)) { |
||||
|
if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && |
||||
|
[DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { |
||||
|
this.clearKeySearchCache(); |
||||
|
} else { |
||||
|
this.clearKeySearchCache(); |
||||
|
setTimeout(() => { |
||||
|
this.reset(); |
||||
|
}, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
this.keyRowFormGroup.patchValue( |
||||
|
{ |
||||
|
label: value?.label, |
||||
|
color: value?.color, |
||||
|
units: value?.units, |
||||
|
decimals: value?.decimals |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
dataKeyHasAggregation(): boolean { |
||||
|
return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries |
||||
|
&& this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE; |
||||
|
} |
||||
|
|
||||
|
dataKeyHasPostprocessing(): boolean { |
||||
|
return !!this.modelValue?.postFuncBody; |
||||
|
} |
||||
|
|
||||
|
displayKeyFn(key?: DataKey): string | undefined { |
||||
|
return key ? key.name : undefined; |
||||
|
} |
||||
|
|
||||
|
createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { |
||||
|
this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); |
||||
|
} |
||||
|
|
||||
|
addKey(event: MatChipInputEvent): void { |
||||
|
const value = event.value; |
||||
|
if ((value || '').trim() && this.dataKeyType) { |
||||
|
this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType}); |
||||
|
} else { |
||||
|
this.clearKeyChip(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
editKey() { |
||||
|
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
||||
|
{ |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
dataKey: deepClone(this.modelValue), |
||||
|
dataKeySettingsSchema: this.datakeySettingsSchema, |
||||
|
dataKeySettingsDirective: this.dataKeySettingsDirective, |
||||
|
dashboard: this.dashboard, |
||||
|
aliasController: this.aliasController, |
||||
|
widget: this.widget, |
||||
|
widgetType: this.widgetType, |
||||
|
deviceId: this.deviceId, |
||||
|
entityAliasId: this.entityAliasId, |
||||
|
showPostProcessing: this.widgetType !== widgetType.alarm, |
||||
|
callbacks: this.callbacks, |
||||
|
hideDataKeyLabel: false, |
||||
|
hideDataKeyColor: this.hideDataKeyColor, |
||||
|
hideDataKeyUnits: !this.displayUnitsOrDigits, |
||||
|
hideDataKeyDecimals: !this.displayUnitsOrDigits |
||||
|
} |
||||
|
}).afterClosed().subscribe((updatedDataKey) => { |
||||
|
if (updatedDataKey) { |
||||
|
this.modelValue = updatedDataKey; |
||||
|
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); |
||||
|
this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false}); |
||||
|
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); |
||||
|
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); |
||||
|
this.updateModel(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
removeKey() { |
||||
|
this.modelValue = {} as DataKey; |
||||
|
this.updateModel(); |
||||
|
this.clearKeyChip(); |
||||
|
} |
||||
|
|
||||
|
textIsNotEmpty(text: string): boolean { |
||||
|
return text && text.length > 0; |
||||
|
} |
||||
|
|
||||
|
clearKeyChip(value: string = '', focus = true) { |
||||
|
this.autocomplete.closePanel(); |
||||
|
this.keyInput.nativeElement.value = value; |
||||
|
this.keyFormControl.patchValue(value, {emitEvent: focus}); |
||||
|
if (focus) { |
||||
|
setTimeout(() => { |
||||
|
this.keyInput.nativeElement.blur(); |
||||
|
this.keyInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onKeyInputFocus() { |
||||
|
if (!this.modelValue.type) { |
||||
|
this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fetchKeys(searchText?: string): Observable<Array<DataKey>> { |
||||
|
if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) { |
||||
|
this.keySearchText = searchText; |
||||
|
const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); |
||||
|
return this.getKeys().pipe( |
||||
|
map(name => name.filter(dataKeyFilter)), |
||||
|
tap(res => this.latestKeySearchTextResult = res) |
||||
|
); |
||||
|
} |
||||
|
return of(this.latestKeySearchTextResult); |
||||
|
} |
||||
|
|
||||
|
private getKeys(): Observable<Array<DataKey>> { |
||||
|
if (this.keyFetchObservable$ === null) { |
||||
|
let fetchObservable: Observable<Array<DataKey>>; |
||||
|
if (this.datasourceType === DatasourceType.function) { |
||||
|
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; |
||||
|
fetchObservable = of(targetKeysList); |
||||
|
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || |
||||
|
this.datasourceType === DatasourceType.device && this.deviceId) { |
||||
|
const dataKeyTypes = [DataKeyType.timeseries]; |
||||
|
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { |
||||
|
dataKeyTypes.push(DataKeyType.attribute); |
||||
|
dataKeyTypes.push(DataKeyType.entityField); |
||||
|
if (this.widgetType === widgetType.alarm) { |
||||
|
dataKeyTypes.push(DataKeyType.alarm); |
||||
|
} |
||||
|
} |
||||
|
if (this.datasourceType === DatasourceType.device) { |
||||
|
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); |
||||
|
} else { |
||||
|
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); |
||||
|
} |
||||
|
} else { |
||||
|
fetchObservable = of([]); |
||||
|
} |
||||
|
this.keyFetchObservable$ = fetchObservable.pipe( |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
return this.keyFetchObservable$; |
||||
|
} |
||||
|
|
||||
|
private createDataKeyFilter(query: string): (key: DataKey) => boolean { |
||||
|
const lowercaseQuery = query.toLowerCase(); |
||||
|
return key => key.name.toLowerCase().startsWith(lowercaseQuery); |
||||
|
} |
||||
|
|
||||
|
private addKeyFromChipValue(chip: DataKey) { |
||||
|
this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); |
||||
|
if (!this.keyRowFormGroup.get('label').value) { |
||||
|
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); |
||||
|
} |
||||
|
this.updateModel(); |
||||
|
this.clearKeyChip('', false); |
||||
|
} |
||||
|
|
||||
|
private clearKeySearchCache() { |
||||
|
this.keySearchText = ''; |
||||
|
this.keyFetchObservable$ = null; |
||||
|
this.latestKeySearchTextResult = null; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value: DataKey = this.keyRowFormGroup.value; |
||||
|
this.modelValue = {...this.modelValue, ...value}; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-widget-config-panel"> |
||||
|
<div class="tb-widget-config-panel-title">{{ panelTitle }}</div> |
||||
|
<div class="tb-data-keys-table"> |
||||
|
<div class="tb-data-keys-header"> |
||||
|
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div> |
||||
|
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div> |
||||
|
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div> |
||||
|
<div class="tb-data-keys-header-cell tb-units-header" translate>widget-config.units-short</div> |
||||
|
<div class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> |
||||
|
<div class="tb-data-keys-header-cell tb-actions-header"></div> |
||||
|
</div> |
||||
|
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
||||
|
(cdkDropListDropped)="keyDrop($event)"> |
||||
|
<div cdkDrag class="tb-data-keys-table-row tb-draggable" *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; |
||||
|
let $index = index;"> |
||||
|
<tb-data-key-row fxFlex |
||||
|
[formControl]="keyControl" |
||||
|
[datasourceType]="datasourceType" |
||||
|
[deviceId]="deviceId" |
||||
|
[entityAliasId]="entityAliasId"> |
||||
|
</tb-data-key-row> |
||||
|
<div class="tb-data-keys-table-row-buttons"> |
||||
|
<button type="button" |
||||
|
mat-icon-button |
||||
|
(click)="removeKey($index)" |
||||
|
[matTooltip]="removeKeyTitle" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
type="button" |
||||
|
cdkDragHandle |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>drag_indicator</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
||||
|
{{ addKeyTitle }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #noKeys> |
||||
|
<span fxLayoutAlign="center center" |
||||
|
class="tb-prompt">{{ noKeysText }}</span> |
||||
|
</ng-template> |
||||
@ -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. |
||||
|
*/ |
||||
|
.tb-data-keys-table { |
||||
|
border: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
border-radius: 6px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
padding-bottom: 12px; |
||||
|
.tb-data-keys-header { |
||||
|
height: 48px; |
||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12); |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center flex-start; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
padding-left: 12px; |
||||
|
.tb-data-keys-header-cell { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
&.tb-color-header, &.tb-units-header, &.tb-decimals-header { |
||||
|
width: 60px; |
||||
|
} |
||||
|
&.tb-actions-header { |
||||
|
width: 76px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-data-keys-body { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
.tb-prompt { |
||||
|
height: 38px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-data-keys-table-row { |
||||
|
height: 38px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
gap: 12px; |
||||
|
background: #fff; |
||||
|
|
||||
|
.tb-data-keys-table-row-buttons { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
button.mat-mdc-icon-button.mat-mdc-button-base { |
||||
|
padding: 7px; |
||||
|
width: 38px; |
||||
|
height: 38px; |
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,237 @@ |
|||||
|
///
|
||||
|
/// 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_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
||||
|
import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; |
||||
|
import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { alarmFields } from '@shared/models/alarm.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-data-keys-panel', |
||||
|
templateUrl: './data-keys-panel.component.html', |
||||
|
styleUrls: ['./data-keys-panel.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DataKeysPanelComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DataKeysPanelComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
panelTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
addKeyTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
removeKeyTitle: string; |
||||
|
|
||||
|
@Input() |
||||
|
noKeysText: string; |
||||
|
|
||||
|
@Input() |
||||
|
datasourceType: DatasourceType; |
||||
|
|
||||
|
@Input() |
||||
|
entityAliasId: string; |
||||
|
|
||||
|
@Input() |
||||
|
deviceId: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
hideDataKeyColor = false; |
||||
|
|
||||
|
dataKeyType: DataKeyType; |
||||
|
alarmKeys: Array<DataKey>; |
||||
|
functionTypeKeys: Array<DataKey>; |
||||
|
|
||||
|
keysListFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
get widgetType(): widgetType { |
||||
|
return this.widgetConfigComponent.widgetType; |
||||
|
} |
||||
|
|
||||
|
get callbacks(): DataKeysCallbacks { |
||||
|
return this.widgetConfigComponent.widgetConfigCallbacks; |
||||
|
} |
||||
|
|
||||
|
get datakeySettingsSchema(): JsonSettingsSchema { |
||||
|
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; |
||||
|
} |
||||
|
|
||||
|
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.alarmKeys = []; |
||||
|
for (const name of Object.keys(alarmFields)) { |
||||
|
this.alarmKeys.push({ |
||||
|
name, |
||||
|
type: DataKeyType.alarm |
||||
|
}); |
||||
|
} |
||||
|
this.functionTypeKeys = []; |
||||
|
for (const type of this.utils.getPredefinedFunctionsList()) { |
||||
|
this.functionTypeKeys.push({ |
||||
|
name: type, |
||||
|
type: DataKeyType.function |
||||
|
}); |
||||
|
} |
||||
|
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 { |
||||
|
if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { |
||||
|
this.dataKeyType = DataKeyType.timeseries; |
||||
|
} else { |
||||
|
this.dataKeyType = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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}); |
||||
|
} |
||||
|
|
||||
|
public validate(c: UntypedFormControl) { |
||||
|
return this.keysListFormGroup.valid ? null : { |
||||
|
dataKeyRows: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
keyDrop(event: CdkDragDrop<string[]>) { |
||||
|
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; |
||||
|
const key = keysArray.at(event.previousIndex); |
||||
|
keysArray.removeAt(event.previousIndex); |
||||
|
keysArray.insert(event.currentIndex, key); |
||||
|
} |
||||
|
|
||||
|
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('', null, this.datakeySettingsSchema); |
||||
|
dataKey.label = ''; |
||||
|
dataKey.decimals = 0; |
||||
|
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; |
||||
|
const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]); |
||||
|
keysArray.push(keyControl); |
||||
|
} |
||||
|
|
||||
|
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { |
||||
|
const keysControls: Array<AbstractControl> = []; |
||||
|
if (keys) { |
||||
|
keys.forEach((key) => { |
||||
|
keysControls.push(this.fb.control(key, [dataKeyRowValidator])); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(keysControls); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,169 +0,0 @@ |
|||||
/** |
|
||||
* 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. |
|
||||
*/ |
|
||||
.tb-widget-config-panel { |
|
||||
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); |
|
||||
border-radius: 4px; |
|
||||
padding: 16px; |
|
||||
gap: 16px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
color: rgba(0, 0, 0, 0.87); |
|
||||
letter-spacing: 0.15px; |
|
||||
position: relative; |
|
||||
&.no-padding-bottom { |
|
||||
padding-bottom: 0; |
|
||||
} |
|
||||
&.stroked { |
|
||||
box-shadow: none; |
|
||||
border: 1px solid rgba(0, 0, 0, 0.12); |
|
||||
border-radius: 6px; |
|
||||
} |
|
||||
} |
|
||||
.tb-widget-config-panel-title { |
|
||||
font-weight: 500; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
.tb-widget-config-panel-hint { |
|
||||
font-size: 12px; |
|
||||
color: #808080; |
|
||||
} |
|
||||
.tb-widget-config-row { |
|
||||
height: 56px; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
align-items: center; |
|
||||
gap: 16px; |
|
||||
padding-left: 16px; |
|
||||
padding-right: 12px; |
|
||||
border: 1px solid rgba(0, 0, 0, 0.12); |
|
||||
border-radius: 6px; |
|
||||
&.same-padding { |
|
||||
padding-right: 16px; |
|
||||
} |
|
||||
&.space-between { |
|
||||
justify-content: space-between; |
|
||||
} |
|
||||
.mat-divider-vertical { |
|
||||
height: 56px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
:host ::ng-deep { |
|
||||
|
|
||||
.mat-slide { |
|
||||
margin: 8px 0; |
|
||||
.mdc-form-field>label { |
|
||||
font-weight: 400; |
|
||||
font-size: 16px; |
|
||||
line-height: 24px; |
|
||||
margin-left: 12px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.slide-block { |
|
||||
display: block; |
|
||||
&:not(:last-child) { |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.mat-mdc-form-field { |
|
||||
&.center { |
|
||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|
||||
.mat-mdc-form-field-infix { |
|
||||
.mdc-text-field__input { |
|
||||
text-align: center; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
&.number { |
|
||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|
||||
padding-right: 4px; |
|
||||
.mat-mdc-form-field-infix { |
|
||||
width: 80px; |
|
||||
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, |
|
||||
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { |
|
||||
opacity: 1; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.tb-widget-config-row { |
|
||||
.mat-mdc-form-field { |
|
||||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|
||||
padding-right: 12px; |
|
||||
padding-left: 12px; |
|
||||
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { |
|
||||
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { |
|
||||
border-color: rgba(0, 0, 0, 0.12); |
|
||||
} |
|
||||
} |
|
||||
.mat-mdc-form-field-infix { |
|
||||
padding-top: 7px; |
|
||||
padding-bottom: 7px; |
|
||||
min-height: 38px; |
|
||||
width: 72px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.tb-widget-config-panel { |
|
||||
.mat-expansion-panel { |
|
||||
&.tb-settings { |
|
||||
box-shadow: none; |
|
||||
.mat-content { |
|
||||
overflow: visible; |
|
||||
} |
|
||||
.mat-expansion-panel-header { |
|
||||
font-weight: 500; |
|
||||
font-size: 16px; |
|
||||
line-height: 24px; |
|
||||
letter-spacing: 0.25px; |
|
||||
padding: 0; |
|
||||
.mat-content { |
|
||||
flex: 0; |
|
||||
white-space: nowrap; |
|
||||
} |
|
||||
&:hover { |
|
||||
background: none; |
|
||||
} |
|
||||
.mat-expansion-indicator { |
|
||||
height: 32px; |
|
||||
padding: 2px; |
|
||||
} |
|
||||
} |
|
||||
.mat-expansion-panel-header-description { |
|
||||
align-items: center; |
|
||||
} |
|
||||
> .mat-expansion-panel-content { |
|
||||
> .mat-expansion-panel-body { |
|
||||
padding: 0; |
|
||||
} |
|
||||
} |
|
||||
.tb-json-object-panel, .tb-css-content-panel { |
|
||||
margin: 0 0 8px; |
|
||||
} |
|
||||
} |
|
||||
.mat-expansion-panel-content { |
|
||||
font: inherit; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue