Browse Source

Merge pull request #8455 from thingsboard/feature/device-info-filter

Feature/device info filter
pull/8458/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
cbb3575e4a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 1
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  3. 60
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  4. 6
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  7. 4
      application/src/main/resources/thingsboard.yml
  8. 7
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  9. 13
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  10. 35
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  11. 13
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  12. 9
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java
  13. 36
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfoFilter.java
  14. 2
      common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java
  15. 4
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java
  16. 8
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  17. 64
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  18. 62
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  19. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  20. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ToData.java
  21. 38
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java
  22. 28
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  23. 115
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  24. 72
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  25. 25
      dao/src/main/resources/sql/schema-entities.sql
  26. 33
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java
  27. 7
      ui-ngx/src/app/core/http/device.service.ts
  28. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  29. 74
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html
  30. 35
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss
  31. 254
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  33. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  34. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  35. 165
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  36. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
  37. 137
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  38. 68
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
  39. 10
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html
  40. 6
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts
  41. 121
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  42. 6
      ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw
  43. 127
      ui-ngx/src/app/shared/models/device.models.ts
  44. 22
      ui-ngx/src/assets/home/no_data_folder_bg.svg
  45. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  46. 2
      ui-ngx/src/styles.scss

26
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -14,6 +14,32 @@
-- limitations under the License.
--
-- DEVICE INFO VIEW START
DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_attribute_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(da.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active';
DROP VIEW IF EXISTS device_info_active_ts_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_ts_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(dt.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from ts_kv_dictionary where key = 'active');
-- DEVICE INFO VIEW END
-- USER CREDENTIALS START
ALTER TABLE user_credentials

1
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -66,6 +66,7 @@ public class ControllerConstants {
protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page";
protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0";
protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
protected static final String DEVICE_ACTIVE_PARAM_DESCRIPTION = "A boolean value representing the device active flag.";
protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type";
protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";

60
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
@ -83,6 +84,7 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ACTIVE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION;
@ -333,6 +335,8 @@ public class DeviceController extends BaseController {
@RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -342,14 +346,15 @@ public class DeviceController extends BaseController {
) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.active(active);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink));
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Get Tenant Device (getTenantDevice)",
@ -415,6 +420,8 @@ public class DeviceController extends BaseController {
@RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -426,14 +433,16 @@ public class DeviceController extends BaseController {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.customerId(customerId);
filter.active(active);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
@ -671,7 +680,7 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Device> getEdgeDevices(
public PageData<DeviceInfo> getEdgeDevices(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@ -680,6 +689,10 @@ public class DeviceController extends BaseController {
@RequestParam int page,
@ApiParam(value = DEVICE_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String deviceProfileId,
@ApiParam(value = DEVICE_ACTIVE_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean active,
@ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -695,25 +708,16 @@ public class DeviceController extends BaseController {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
PageData<Device> nonFilteredResult;
DeviceInfoFilter.DeviceInfoFilterBuilder filter = DeviceInfoFilter.builder();
filter.tenantId(tenantId);
filter.edgeId(edgeId);
filter.active(active);
if (type != null && type.trim().length() > 0) {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeIdAndType(tenantId, edgeId, type, pageLink);
} else {
nonFilteredResult = deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
filter.type(type);
} else if (deviceProfileId != null && deviceProfileId.length() > 0) {
filter.deviceProfileId(new DeviceProfileId(toUUID(deviceProfileId)));
}
List<Device> filteredDevices = nonFilteredResult.getData().stream().filter(device -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, Operation.READ, device.getId(), device);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
PageData<Device> filteredResult = new PageData<>(filteredDevices,
nonFilteredResult.getTotalPages(),
nonFilteredResult.getTotalElements(),
nonFilteredResult.hasNext());
return checkNotNull(filteredResult);
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)",

6
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -53,6 +53,9 @@ public class ThingsboardInstallService {
@Value("${install.load_demo:false}")
private Boolean loadDemo;
@Value("${state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@Autowired
private EntityDatabaseSchemaService entityDatabaseSchemaService;
@ -247,6 +250,7 @@ public class ThingsboardInstallService {
case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) {
@ -272,6 +276,8 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Installing DataBase schema for timeseries...");
if (noSqlKeyspaceService != null) {

3
application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java

@ -16,4 +16,7 @@
package org.thingsboard.server.service.install;
public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry);
}

6
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -39,4 +39,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
}
@Override
public void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry) {
String sourceViewName = activityStateInTelemetry ? "device_info_active_ts_view" : "device_info_active_attribute_view";
executeQuery("DROP VIEW IF EXISTS device_info_view CASCADE;");
executeQuery("CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM " + sourceViewName + ";");
}
}

4
application/src/main/resources/thingsboard.yml

@ -645,6 +645,10 @@ state:
# Should be greater then transport.sessions.report_timeout
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:600}"
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
# Controls whether we store device 'active' flag in attributes (default) or telemetry.
# If you device to change this parameter, you should re-create the device info view as one of the following:
# If 'persistToTelemetry' is changed from 'false' to 'true': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_ts_view;'
# If 'persistToTelemetry' is changed from 'true' to 'false': 'CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;'
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
tbel:

7
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -38,6 +38,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
@ -1287,8 +1288,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
savedDevice.getId().getId().toString(), savedEdge.getId().getId().toString(), savedEdge.getName());
testNotificationUpdateGatewayNever();
pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
PAGE_DATA_DEVICE_TYPE_REF, new PageLink(100));
PageData<DeviceInfo> pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(1, pageData.getData().size());
@ -1303,7 +1304,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
testNotificationUpdateGatewayNever();
pageData = doGetTypedWithPageLink("/api/edge/" + savedEdge.getId().getId() + "/devices?",
PAGE_DATA_DEVICE_TYPE_REF, new PageLink(100));
new TypeReference<>() {}, new PageLink(100));
Assert.assertEquals(0, pageData.getData().size());
}

13
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
@ -322,9 +323,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB());
Device device = doGet("/api/device/" + deviceUUID, Device.class);
Assert.assertNotNull(device);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {}, new PageLink(100)).getData();
Assert.assertTrue(edgeDevices.contains(device));
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {}, new PageLink(100)).getData();
Assert.assertTrue(edgeDevices.stream().map(DeviceInfo::getId).anyMatch(id -> id.equals(device.getId())));
testAutoGeneratedCodeByProtobuf(deviceUpdateMsg);
}
@ -487,10 +488,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
}
protected Device findDeviceByName(String deviceName) throws Exception {
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {
}, new PageLink(100)).getData();
Optional<Device> foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny();
Optional<DeviceInfo> foundDevice = edgeDevices.stream().filter(d -> d.getName().equals(deviceName)).findAny();
Assert.assertTrue(foundDevice.isPresent());
Device device = foundDevice.get();
Assert.assertEquals(deviceName, device.getName());

35
application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java

@ -25,11 +25,13 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.StringUtils;
@ -46,11 +48,14 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -378,18 +383,18 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}",
"key2", "value2");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys","key1, inactivityTimeout");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2");
}
@Test
public void testSendDeleteDeviceOnEdgeToCloud() throws Exception {
Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge();
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
UplinkMsg.Builder upLinkMsgBuilder = UplinkMsg.newBuilder();
DeviceUpdateMsg.Builder deviceDeleteMsgBuilder = DeviceUpdateMsg.newBuilder();
deviceDeleteMsgBuilder.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE);
deviceDeleteMsgBuilder.setIdMSB(device.getId().getId().getMostSignificantBits());
deviceDeleteMsgBuilder.setIdLSB(device.getId().getId().getLeastSignificantBits());
deviceDeleteMsgBuilder.setIdMSB(savedDevice.getId().getId().getMostSignificantBits());
deviceDeleteMsgBuilder.setIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
testAutoGeneratedCodeByProtobuf(deviceDeleteMsgBuilder);
upLinkMsgBuilder.addDeviceUpdateMsg(deviceDeleteMsgBuilder.build());
@ -398,12 +403,12 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(upLinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
device = doGet("/api/device/" + device.getUuidId(), Device.class);
Assert.assertNotNull(device);
List<Device> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<Device>>() {
DeviceInfo deviceInfo = doGet("/api/device/info/" + savedDevice.getUuidId(), DeviceInfo.class);
Assert.assertNotNull(deviceInfo);
List<DeviceInfo> edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?",
new TypeReference<PageData<DeviceInfo>>() {
}, new PageLink(100)).getData();
Assert.assertFalse(edgeDevices.contains(device));
Assert.assertFalse(edgeDevices.contains(deviceInfo));
}
@Test
@ -455,7 +460,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(timeseriesValue, timeseries.get(timeseriesKey).get(0).get("value"));
String attributeValuesUrl = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/attributes/" + DataConstants.SERVER_SCOPE;
List<Map<String, String>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {});
List<Map<String, String>> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {
});
Assert.assertEquals(3, attributes.size());
@ -599,7 +605,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope;
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {});
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {
});
return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey);
});
@ -656,7 +663,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
private Map<String, List<Map<String, String>>> loadDeviceTimeseries(Device device, String timeseriesKey) throws Exception {
return doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/values/timeseries?keys=" + timeseriesKey,
new TypeReference<>() {});
new TypeReference<>() {
});
}
@Test
@ -713,7 +721,8 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest {
.atMost(10, TimeUnit.SECONDS)
.until(() -> {
String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/timeseries";
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {});
List<String> actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {
});
return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains("temperature");
});

13
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
@ -66,7 +67,7 @@ public interface DeviceService extends EntityDaoService {
PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink);
@ -76,10 +77,6 @@ public interface DeviceService extends EntityDaoService {
Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
List<Device> findDevicesByIds(List<DeviceId> deviceIds);
@ -90,14 +87,8 @@ public interface DeviceService extends EntityDaoService {
PageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);

9
common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java

@ -18,18 +18,24 @@ package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.DeviceId;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceInfo extends Device {
private static final long serialVersionUID = -3004579925090663691L;
@ApiModelProperty(position = 13, value = "Title of the Customer that owns the device.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String customerTitle;
@ApiModelProperty(position = 14, value = "Indicates special 'Public' Customer that is auto-generated to use the devices on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private boolean customerIsPublic;
@ApiModelProperty(position = 15, value = "Name of the corresponding Device Profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String deviceProfileName;
@ApiModelProperty(position = 16, value = "Device active flag.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private boolean active;
public DeviceInfo() {
super();
@ -39,10 +45,11 @@ public class DeviceInfo extends Device {
super(deviceId);
}
public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName) {
public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic, String deviceProfileName, boolean active) {
super(device);
this.customerTitle = customerTitle;
this.customerIsPublic = customerIsPublic;
this.deviceProfileName = deviceProfileName;
this.active = active;
}
}

36
common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfoFilter.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@Builder
public class DeviceInfoFilter {
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String type;
private DeviceProfileId deviceProfileId;
private Boolean active;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java

@ -17,9 +17,11 @@ package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.EntityViewId;
@Data
@EqualsAndHashCode(callSuper = true)
public class EntityViewInfo extends EntityView {
@ApiModelProperty(position = 12, value = "Title of the Customer that owns the entity view.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)

4
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java

@ -18,12 +18,16 @@ package org.thingsboard.server.common.data.asset;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.AssetId;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
public class AssetInfo extends Asset {
private static final long serialVersionUID = -4094528227011066194L;
@ApiModelProperty(position = 10, value = "Title of the Customer that owns the asset.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String customerTitle;
@ApiModelProperty(position = 11, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)

8
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
@ -128,4 +129,11 @@ public abstract class DaoUtil {
} while (hasNextBatch);
}
public static String getStringId(UUIDBased id) {
if (id != null) {
return id.toString();
} else {
return null;
}
}
}

64
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.DeviceIdInfo;
@ -74,15 +75,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/
PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find device infos by tenantId and page link.
*
* @param tenantId the tenantId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find devices by tenantId, type and page link.
*
@ -100,26 +92,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(UUID tenantId, UUID deviceProfileId, OtaPackageType otaPackageType);
/**
* Find device infos by tenantId, type and page link.
*
* @param tenantId the tenantId
* @param type the type
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, deviceProfileId and page link.
*
* @param tenantId the tenantId
* @param deviceProfileId the deviceProfileId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink);
/**
* Find devices by tenantId and devices Ids.
*
@ -155,16 +127,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/
PageData<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/**
* Find device infos by tenantId, customerId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/**
* Find devices by tenantId, customerId, type and page link.
*
@ -176,28 +138,6 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
*/
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, customerId, type and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param type the type
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, customerId, deviceProfileId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param deviceProfileId the deviceProfileId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink);
/**
* Find devices by tenantId, customerId and devices Ids.
*
@ -277,5 +217,5 @@ public interface DeviceDao extends Dao<Device>, TenantEntityDao, ExportableEntit
PageData<DeviceIdInfo> findDeviceIdInfos(PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink);
}

62
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -30,6 +30,7 @@ import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -69,6 +70,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -342,12 +344,17 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
log.trace("Executing findDeviceInfosByFilter, filter [{}], pageLink [{}]", filter, pageLink);
if (filter == null) {
throw new IncorrectParameterException("Filter is empty!");
}
validateId(filter.getTenantId(), INCORRECT_TENANT_ID + filter.getTenantId());
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink);
return deviceDao.findDeviceInfosByFilter(filter, pageLink);
}
@Override
@ -387,24 +394,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(tenantId.getId(), deviceProfileId.getId(), type);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndDeviceProfileId, tenantId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, deviceProfileId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId.getId(), deviceProfileId.getId(), pageLink);
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
@ -444,15 +433,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
}
@Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
@ -463,26 +443,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId, tenantId [{}], customerId [{}], deviceProfileId [{}], pageLink [{}]", tenantId, customerId, deviceProfileId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateId(deviceProfileId, INCORRECT_DEVICE_PROFILE_ID + deviceProfileId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId.getId(), customerId.getId(), deviceProfileId.getId(), pageLink);
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);

7
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -169,6 +169,11 @@ public class ModelConstants {
public static final String DEVICE_FIRMWARE_ID_PROPERTY = "firmware_id";
public static final String DEVICE_SOFTWARE_ID_PROPERTY = "software_id";
public static final String DEVICE_CUSTOMER_TITLE_PROPERTY = "customer_title";
public static final String DEVICE_CUSTOMER_IS_PUBLIC_PROPERTY = "customer_is_public";
public static final String DEVICE_DEVICE_PROFILE_NAME_PROPERTY = "device_profile_name";
public static final String DEVICE_ACTIVE_PROPERTY = "active";
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
@ -176,6 +181,8 @@ public class ModelConstants {
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
public static final String DEVICE_INFO_VIEW_COLUMN_FAMILY_NAME = "device_info_view";
/**
* Device profile constants.
*/

1
dao/src/main/java/org/thingsboard/server/dao/model/ToData.java

@ -28,4 +28,5 @@ public interface ToData<T> {
* @return the dto object
*/
T toData();
}

38
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java

@ -15,48 +15,40 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Immutable
@Table(name = ModelConstants.DEVICE_INFO_VIEW_COLUMN_FAMILY_NAME)
public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> {
public static final Map<String,String> deviceInfoColumnMap = new HashMap<>();
static {
deviceInfoColumnMap.put("customerTitle", "c.title");
deviceInfoColumnMap.put("deviceProfileName", "p.name");
}
@Column(name = ModelConstants.DEVICE_CUSTOMER_TITLE_PROPERTY)
private String customerTitle;
@Column(name = ModelConstants.DEVICE_CUSTOMER_IS_PUBLIC_PROPERTY)
private boolean customerIsPublic;
@Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_NAME_PROPERTY)
private String deviceProfileName;
@Column(name = ModelConstants.DEVICE_ACTIVE_PROPERTY)
private boolean active;
public DeviceInfoEntity() {
super();
}
public DeviceInfoEntity(DeviceEntity deviceEntity,
String customerTitle,
Object customerAdditionalInfo,
String deviceProfileName) {
super(deviceEntity);
this.customerTitle = customerTitle;
if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) {
this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean();
} else {
this.customerIsPublic = false;
}
this.deviceProfileName = deviceProfileName;
}
@Override
public DeviceInfo toData() {
return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName);
return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic, deviceProfileName, active);
}
}

28
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -137,10 +137,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
EntityId affectedEntity = query.getAffectedEntityId();
AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) {
return DaoUtil.toPageData(
alarmRepository.findAlarms(
@ -153,7 +149,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -168,7 +164,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -180,10 +176,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink());
AlarmStatusFilter asf = AlarmStatusFilter.from(query);
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData(
alarmRepository.findCustomerAlarms(
tenantId.getId(),
@ -194,7 +186,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -208,10 +200,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) {
return DaoUtil.toPageData(
alarmRepository.findAlarmsV2(
@ -226,7 +214,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -243,7 +231,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
@ -257,10 +245,6 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData(
alarmRepository.findCustomerAlarmsV2(
tenantId.getId(),
@ -273,7 +257,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
DaoUtil.getStringId(query.getAssigneeId()),
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)

115
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.DeviceEntity;
import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
@ -29,16 +28,9 @@ import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import java.util.List;
import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
*/
public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, ExportableEntityRepository<DeviceEntity> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.id = :deviceId")
@Query("SELECT d FROM DeviceInfoEntity d WHERE d.id = :deviceId")
DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
@ -57,10 +49,7 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
@Query("SELECT d FROM DeviceInfoEntity d " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))")
@ -79,19 +68,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(p.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantId(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
@ -105,9 +81,9 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
"AND d.firmwareId = null " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceEntity> findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.deviceProfileId = :deviceProfileId " +
@ -130,34 +106,6 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
Long countByTenantIdAndDeviceProfileIdAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND d.type = :type " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND d.deviceProfileId = :deviceProfileId " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
@ -168,33 +116,26 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
@Query("SELECT d FROM DeviceInfoEntity d " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("type") String type,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo, p.name) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.deviceProfileId = :deviceProfileId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("deviceProfileId") UUID deviceProfileId,
@Param("textSearch") String textSearch,
Pageable pageable);
"AND (:customerId IS NULL OR d.customerId = uuid(:customerId)) " +
"AND (:edgeId IS NULL OR d.id IN (SELECT re.toId FROM RelationEntity re WHERE re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' AND re.relationType = 'Contains' AND re.fromType = 'EDGE' AND re.fromId = uuid(:edgeId))) " +
"AND ((:deviceType) IS NULL OR d.type = :deviceType) " +
"AND (:deviceProfileId IS NULL OR d.deviceProfileId = uuid(:deviceProfileId)) " +
"AND ((:filterByActive) IS FALSE OR d.active = :deviceActive) " +
"AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.type) LIKE LOWER(CONCAT('%', :textSearch, '%')) " +
"OR LOWER(d.customerTitle) LIKE LOWER(CONCAT('%', :textSearch, '%')))")
Page<DeviceInfoEntity> findDeviceInfosByFilter(@Param("tenantId") UUID tenantId,
@Param("customerId") String customerId,
@Param("edgeId") String edgeId,
@Param("deviceType") String type,
@Param("deviceProfileId") String deviceProfileId,
@Param("filterByActive") boolean filterByActive,
@Param("deviceActive") boolean active,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId);
@ -234,13 +175,13 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
/**
* Count devices by tenantId.
* Custom query applied because default QueryDSL produces slow count(id).
* <p>
*
* There is two way to count devices.
* OPTIMAL: count(*)
* - returns _row_count_ and use index-only scan (super fast).
* - returns _row_count_ and use index-only scan (super fast).
* SLOW: count(id)
* - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows.
* */
* - returns _NON_NULL_id_count and performs table scan to verify isNull for each id in filtered rows.
*/
@Query("SELECT count(*) FROM DeviceEntity d WHERE d.tenantId = :tenantId")
Long countByTenantId(@Param("tenantId") UUID tenantId);

72
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -16,8 +16,11 @@
package org.thingsboard.server.dao.sql.device;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@ -26,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
@ -43,9 +47,11 @@ import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -104,12 +110,18 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) {
public PageData<DeviceInfo> findDeviceInfosByFilter(DeviceInfoFilter filter, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantId(
tenantId,
deviceRepository.findDeviceInfosByFilter(
filter.getTenantId().getId(),
DaoUtil.getStringId(filter.getCustomerId()),
DaoUtil.getStringId(filter.getEdgeId()),
filter.getType(),
DaoUtil.getStringId(filter.getDeviceProfileId()),
filter.getActive() != null,
Boolean.TRUE.equals(filter.getActive()),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
DaoUtil.toPageable(pageLink)));
}
@Override
@ -152,16 +164,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
return DaoUtil.pageToPageData(deviceRepository.findIdsByDeviceProfileTransportType(transportType, DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerId(
tenantId,
customerId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
return service.submit(() -> DaoUtil.convertDataList(
@ -208,26 +210,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndType(
tenantId,
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndDeviceProfileId(UUID tenantId, UUID deviceProfileId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndDeviceProfileId(
tenantId,
deviceProfileId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
@ -239,28 +221,6 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndType(
tenantId,
customerId,
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(UUID tenantId, UUID customerId, UUID deviceProfileId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(
tenantId,
customerId,
deviceProfileId,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(tenantId)));

25
dao/src/main/resources/sql/schema-entities.sql

@ -859,6 +859,31 @@ CREATE TABLE IF NOT EXISTS user_settings (
CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type)
);
DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_attribute_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(da.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN attribute_kv da ON da.entity_id = d.id and da.attribute_key = 'active';
DROP VIEW IF EXISTS device_info_active_ts_view CASCADE;
CREATE OR REPLACE VIEW device_info_active_ts_view AS
SELECT d.*
, c.title as customer_title
, COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public
, d.type as device_profile_name
, COALESCE(dt.bool_v, FALSE) as active
FROM device d
LEFT JOIN customer c ON c.id = d.customer_id
LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from ts_kv_dictionary where key = 'active');
DROP VIEW IF EXISTS device_info_view CASCADE;
CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view;
DROP VIEW IF EXISTS alarm_info CASCADE;
CREATE VIEW alarm_info AS
SELECT a.*,

33
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java

@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.OtaPackage;
@ -446,7 +447,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name);
device.setType("default");
devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default"));
devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default", false));
}
String title2 = "Device title 2";
List<DeviceInfo> devicesTitle2 = new ArrayList<>();
@ -458,14 +459,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name);
device.setType("default");
devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default"));
devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false, "default", false));
}
List<DeviceInfo> loadedDevicesTitle1 = new ArrayList<>();
PageLink pageLink = new PageLink(15, 0, title1);
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle1.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -480,7 +481,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>();
pageLink = new PageLink(4, 0, title2);
do {
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
loadedDevicesTitle2.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -497,7 +498,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
pageLink = new PageLink(4, 0, title1);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
@ -506,7 +507,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
pageLink = new PageLink(4, 0, title2);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
@ -605,14 +606,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
device.setName("Device" + i);
device.setType("default");
device = deviceService.saveDevice(device);
devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default"));
devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic(), "default", false));
}
List<DeviceInfo> loadedDevices = new ArrayList<>();
PageLink pageLink = new PageLink(23);
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId(customerId).build(), pageLink);
loadedDevices.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -627,7 +628,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
deviceService.unassignCustomerDevices(tenantId, customerId);
pageLink = new PageLink(33);
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
pageData = deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId(customerId).build(), pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty());
}
@ -848,7 +849,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithLabel).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue(
@ -862,7 +863,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithCustomer).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue(
@ -877,7 +878,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithType = new PageLink(100, 0, device.getType());
List<DeviceInfo> deviceInfosWithType = deviceService
.findDeviceInfosByTenantId(tenantId, pageLinkWithType).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).build(), pageLinkWithType).getData();
Assert.assertFalse(deviceInfosWithType.isEmpty());
Assert.assertTrue(
@ -907,7 +908,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantIdAndType(tenantId, device.getType(), pageLinkWithLabel).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).type(device.getType()).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue(
@ -922,7 +923,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantIdAndType(tenantId, device.getType(), pageLinkWithCustomer).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).type(device.getType()).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue(
@ -953,7 +954,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithLabel = new PageLink(100, 0, "label");
List<DeviceInfo> deviceInfosWithLabel = deviceService
.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, savedDevice.getDeviceProfileId(), pageLinkWithLabel).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).deviceProfileId(savedDevice.getDeviceProfileId()).build(), pageLinkWithLabel).getData();
Assert.assertFalse(deviceInfosWithLabel.isEmpty());
Assert.assertTrue(
@ -968,7 +969,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText());
List<DeviceInfo> deviceInfosWithCustomer = deviceService
.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, savedDevice.getDeviceProfileId(), pageLinkWithCustomer).getData();
.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).deviceProfileId(savedDevice.getDeviceProfileId()).build(), pageLinkWithCustomer).getData();
Assert.assertFalse(deviceInfosWithCustomer.isEmpty());
Assert.assertTrue(

7
ui-ngx/src/app/core/http/device.service.ts

@ -25,7 +25,7 @@ import {
ClaimResult,
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -42,6 +42,11 @@ export class DeviceService {
private http: HttpClient
) { }
public getDeviceInfosByQuery(deviceInfoQuery: DeviceInfoQuery, config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api${deviceInfoQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getTenantDeviceInfos(pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`,

2
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts

@ -190,7 +190,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel',
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',

74
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html

@ -0,0 +1,74 @@
<!--
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 *ngIf="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="deviceFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else deviceFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleDeviceFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #deviceFilterPanel>
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="deviceFilter"></ng-container>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="deviceInfoFilterForm.invalid || !deviceInfoFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #deviceFilter>
<div fxLayout="column" fxLayoutGap="16px" style="min-width: 280px;" [formGroup]="deviceInfoFilterForm">
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
formControlName="deviceProfileId"
[displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<mat-form-field>
<mat-label translate>device.device-state</mat-label>
<mat-select formControlName="active">
<mat-option value="">
{{ 'device.any' | translate }}
</mat-option>
<mat-option [value]="true">
{{ 'device.active' | translate }}
</mat-option>
<mat-option [value]="false">
{{ 'device.inactive' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>

35
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: block;
overflow: hidden;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

254
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts

@ -0,0 +1,254 @@
///
/// 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,
Inject,
InjectionToken,
Input,
OnDestroy,
OnInit,
Optional,
TemplateRef,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { DeviceInfoFilter } from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
import { EntityInfoData } from '@shared/models/entity.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
export const DEVICE_FILTER_CONFIG_DATA = new InjectionToken<any>('DeviceFilterConfigData');
export interface DeviceFilterConfigData {
panelMode: boolean;
deviceInfoFilter: DeviceInfoFilter;
}
// @dynamic
@Component({
selector: 'tb-device-info-filter',
templateUrl: './device-info-filter.component.html',
styleUrls: ['./device-info-filter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoFilterComponent),
multi: true
}
]
})
export class DeviceInfoFilterComponent implements OnInit, OnDestroy, ControlValueAccessor {
@ViewChild('deviceFilterPanel')
deviceFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
panelMode = false;
buttonDisplayValue = this.translate.instant('device.device-filter');
deviceInfoFilterForm: UntypedFormGroup;
deviceFilterOverlayRef: OverlayRef;
panelResult: DeviceInfoFilter = null;
private deviceProfileInfo: EntityInfoData;
private deviceInfoFilter: DeviceInfoFilter;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(DEVICE_FILTER_CONFIG_DATA)
private data: DeviceFilterConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private deviceProfileService: DeviceProfileService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.deviceInfoFilter = this.data.deviceInfoFilter;
}
this.deviceInfoFilterForm = this.fb.group({
deviceProfileId: [null, []],
active: ['', []]
});
this.deviceInfoFilterForm.valueChanges.subscribe(
() => {
this.updateValidators();
if (!this.buttonMode) {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
}
}
);
if (this.panelMode) {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
}
}
ngOnDestroy(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceInfoFilterForm.disable({emitEvent: false});
} else {
this.deviceInfoFilterForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(deviceInfoFilter?: DeviceInfoFilter): void {
this.deviceInfoFilter = deviceInfoFilter;
this.updateButtonDisplayValue();
this.updateDeviceInfoFilterForm(deviceInfoFilter);
}
private updateValidators() {
}
toggleDeviceFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
.withPositions([connectedPosition]);
this.deviceFilterOverlayRef = this.overlay.create(config);
this.deviceFilterOverlayRef.backdropClick().subscribe(() => {
this.deviceFilterOverlayRef.dispose();
});
this.deviceFilterOverlayRef.attach(new TemplatePortal(this.deviceFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
update() {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
if (this.panelMode) {
this.panelResult = this.deviceInfoFilter;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
deviceProfileChanged(deviceProfileInfo: EntityInfoData) {
this.deviceProfileInfo = deviceProfileInfo;
this.updateButtonDisplayValue();
}
private updateDeviceInfoFilterForm(deviceInfoFilter?: DeviceInfoFilter) {
this.deviceInfoFilterForm.patchValue({
deviceProfileId: deviceInfoFilter?.deviceProfileId,
active: isDefinedAndNotNull(deviceInfoFilter?.active) ? deviceInfoFilter?.active : ''
}, {emitEvent: false});
this.updateValidators();
}
private deviceFilterUpdated(deviceInfoFilter: DeviceInfoFilter) {
this.deviceInfoFilter = deviceInfoFilter;
if ((this.deviceInfoFilter.active as any) === '') {
this.deviceInfoFilter.active = null;
}
this.updateButtonDisplayValue();
this.propagateChange(this.deviceInfoFilter);
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (isDefinedAndNotNull(this.deviceInfoFilter?.deviceProfileId)) {
if (!this.deviceProfileInfo) {
this.deviceProfileService.getDeviceProfileInfo(this.deviceInfoFilter?.deviceProfileId.id,
{ignoreLoading: true, ignoreErrors: true}).subscribe(
(deviceProfileInfo) => {
this.deviceProfileChanged(deviceProfileInfo);
});
return;
} else {
filterTextParts.push(this.deviceProfileInfo.name);
}
}
if (isDefinedAndNotNull(this.deviceInfoFilter?.active)) {
const translationKey = this.deviceInfoFilter?.active ? 'device.active' : 'device.inactive';
filterTextParts.push(this.translate.instant(translationKey));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('device.device-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('device.filter-title') + `: ${filterTextParts.join(', ')}`;
}
this.cd.detectChanges();
}
}
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -181,6 +181,7 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
@NgModule({
declarations:
@ -282,6 +283,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
AssetProfileComponent,
AssetProfileDialogComponent,
AssetProfileAutocompleteComponent,
@ -425,6 +427,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
AssetProfileComponent,

2
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -591,7 +591,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.panelClass = 'tb-alarm-filter-panel';
config.panelClass = 'tb-filter-panel';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',

46
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss

@ -67,4 +67,50 @@
color: inherit;
}
}
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
}

165
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -16,91 +16,102 @@
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</div>
</div>
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<ng-template [ngSwitchCase]="'last'">
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;">
<table mat-table
[dataSource]="lastVisitedDashboardsDataSource" matSort
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property"
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="starred">
<mat-header-cell class="star-cell" *matHeaderCellDef>
</mat-header-cell>
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard">
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)"
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.last-viewed' | translate }}
</mat-header-cell>
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard">
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row>
</table>
<ng-container *ngIf="hasDashboardsAccess; else noDataAvailable">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</div>
<ng-template #noLastVisitedDashboards>
<div class="tb-no-dashboards">
<div class="tb-no-last-visited-bg"></div>
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
</div>
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<ng-template [ngSwitchCase]="'last'">
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;">
<table mat-table
[dataSource]="lastVisitedDashboardsDataSource" matSort
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property"
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="starred">
<mat-header-cell class="star-cell" *matHeaderCellDef>
</mat-header-cell>
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard">
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)"
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastVisited">
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header>
{{ 'widgets.recent-dashboards.last-viewed' | translate }}
</mat-header-cell>
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard">
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row>
</table>
</div>
</ng-template>
</ng-template>
<ng-template [ngSwitchCase]="'starred'">
<div style="overflow-y: auto;">
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred">
<div class="tb-cell star-cell">
<mat-icon (click)="toggleDashboardStar(dashboard)"
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
<ng-template #noLastVisitedDashboards>
<div class="tb-no-dashboards">
<div class="tb-no-last-visited-bg"></div>
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</ng-template>
</ng-template>
<ng-template [ngSwitchCase]="'starred'">
<div style="overflow-y: auto;">
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred">
<div class="tb-cell star-cell">
<mat-icon (click)="toggleDashboardStar(dashboard)"
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</div>
</div>
</div>
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete"
#starDashboardAutocomplete
subscriptSizing="dynamic"
appearance="outline"
[useIdValue]="false"
label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete>
</ng-template>
</ng-container>
<ng-template #loading>
<div class="tb-no-dashboards">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner>
</div>
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete"
#starDashboardAutocomplete
subscriptSizing="dynamic"
appearance="outline"
[useIdValue]="false"
label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete>
</ng-template>
</ng-container>
<ng-template #loading>
<div class="tb-no-dashboards">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner>
<ng-template #noDataAvailable>
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.recent-dashboards.title' | translate }}</div>
</div>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widgets.home.no-data-available</div>
</div>
</ng-template>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts

@ -74,6 +74,7 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
lastVisitedDashboardsPageLink: PageLink;
starredDashboardValue = null;
hasDashboardsAccess = true;
dirty = false;
@ -84,7 +85,10 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
}
ngOnInit() {
this.reload();
this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) {
this.reload();
}
}
reload() {

137
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html

@ -16,74 +16,85 @@
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
<ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header>
</div>
<ng-container [ngSwitch]="usageToggle.value">
<ng-template [ngSwitchCase]="'entities'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</ng-template>
<ng-template [ngSwitchCase]="'apiCalls'">
<div fxFlex class="tb-usage-list">
<div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div>
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar>
<div class="tb-usage-items-values">
<div class="tb-usage-items-counts">
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div>
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div>
</div>
<div class="tb-usage-items-progress">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar>
</div>
</div>
</div>
</div>
</ng-template>
</ng-template>
</ng-container>
</ng-container>
<ng-template #noDataAvailable>
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.usage-info.title' | translate }}</div>
</div>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widgets.home.no-data-available</div>
</div>
</ng-template>
</div>

68
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts

@ -44,6 +44,8 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
entityItemCritical: {[key: string]: boolean} = {};
apiCallItemCritical: {[key: string]: boolean} = {};
hasUsageInfoAccess = true;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe,
@ -52,41 +54,43 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
}
ngOnInit() {
(this.authUser.authority === Authority.TENANT_ADMIN ?
this.usageInfoService.getUsageInfo() : of(null)).subscribe(
(usageInfo) => {
this.usageInfo = usageInfo;
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let entitiesHasCriticalItem = false;
let apiCallsHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) {
if (this.entityItemCritical[key]) {
entitiesHasCriticalItem = true;
break;
this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN;
if (this.hasUsageInfoAccess) {
this.usageInfoService.getUsageInfo().subscribe(
(usageInfo) => {
this.usageInfo = usageInfo;
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let entitiesHasCriticalItem = false;
let apiCallsHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) {
if (this.entityItemCritical[key]) {
entitiesHasCriticalItem = true;
break;
}
}
}
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
}
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
);
);
}
}
maxValue(max: number): number | string {

10
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html

@ -15,10 +15,6 @@
limitations under the License.
-->
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
[ngModel]="entitiesTableConfig.componentsData.deviceProfileId"
(ngModelChange)="deviceProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<tb-device-info-filter [ngModel]="entitiesTableConfig.componentsData.deviceInfoFilter"
(ngModelChange)="deviceInfoFilterChanged($event)">
</tb-device-info-filter>

6
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts

@ -18,7 +18,7 @@ import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { DeviceInfo } from '@app/shared/models/device.models';
import { DeviceInfo, DeviceInfoFilter } from '@app/shared/models/device.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id';
@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<Devic
super(store);
}
deviceProfileChanged(deviceProfileId: DeviceProfileId) {
this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId;
deviceInfoFilterChanged(deviceInfoFilter: DeviceInfoFilter) {
this.entitiesTableConfig.componentsData.deviceInfoFilter = deviceInfoFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}

121
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import {
CellActionDescriptor,
checkBoxCell,
@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models';
import {
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfoFilter,
DeviceInfoQuery
} from '@app/shared/models/device.models';
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store';
@ -63,12 +69,21 @@ import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { BaseData, HasId } from '@shared/models/base-data';
import { isDefinedAndNotNull } from '@core/utils';
import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
import { EdgeService } from '@core/http/edge.service';
import {
AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData
} from '@home/dialogs/add-entities-to-edge-dialog.component';
import { EdgeId } from '@shared/models/id/edge-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
interface DevicePageQueryParams extends PageQueryParam {
deviceProfileId?: string;
active?: boolean | string;
}
@Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
@ -103,17 +118,16 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => {
return this.deviceService.saveDevice(device).pipe(
this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe(
tap(() => {
this.broadcast.broadcast('deviceSaved');
}),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
));
};
this.config.onEntityAction = action => this.onDeviceAction(action, this.config);
this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.onLoadAction = (route) => this.onLoadAction(route);
this.config.headerComponent = DeviceTableHeaderComponent;
@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
const routeParams = route.params;
this.config.componentsData = {
deviceScope: route.data.devicesType,
deviceProfileId: null,
deviceInfoFilter: {},
deviceCredentials$: new Subject<DeviceCredentials>(),
edgeId: routeParams.edgeId
};
@ -162,7 +176,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config;
@ -170,48 +185,97 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
onLoadAction(route: ActivatedRoute): void {
const routerQueryParams: DevicePageQueryParams = route.snapshot.queryParams;
if (routerQueryParams) {
const queryParams = deepClone(routerQueryParams);
let replaceUrl = false;
if (routerQueryParams?.deviceProfileId) {
this.config.componentsData.deviceInfoFilter.deviceProfileId = new DeviceProfileId(routerQueryParams?.deviceProfileId);
delete queryParams.deviceProfileId;
replaceUrl = true;
}
if (isDefined(routerQueryParams?.active)) {
this.config.componentsData.deviceInfoFilter.active = (routerQueryParams?.active === true || routerQueryParams?.active === 'true');
delete queryParams.active;
replaceUrl = true;
}
if (replaceUrl) {
this.router.navigate([], {
relativeTo: route,
queryParams,
queryParamsHandling: '',
replaceUrl: true
});
}
}
}
configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
new EntityTableColumn<DeviceInfo>('deviceProfileName', 'device-profile.device-profile', '25%'),
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%')
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%'),
new EntityTableColumn<DeviceInfo>('active', 'device.state', '80px',
entity => this.deviceState(entity), entity => this.deviceStateStyle(entity))
];
if (deviceScope === 'tenant') {
columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
entity => checkBoxCell(entity.customerIsPublic), () => ({})),
);
}
columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => {
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false)
);
return columns;
}
private deviceState(device: DeviceInfo): string {
let translateKey = 'device.active';
let backgroundColor = 'rgba(25, 128, 56, 0.08)';
if (!device.active) {
translateKey = 'device.inactive';
backgroundColor = 'rgba(209, 39, 48, 0.08)';
}
return `<div class="status" style="border-radius: 16px; height: 32px;
line-height: 32px; padding: 0 12px; width: fit-content; background-color: ${backgroundColor}">
${this.translate.instant(translateKey)}
</div>`;
}
private deviceStateStyle(device: DeviceInfo): object {
const styleObj = {
fontSize: '14px',
color: '#198038',
cursor: 'pointer'
};
if (!device.active) {
styleObj.color = '#d12730';
}
return styleObj;
}
configureEntityFunctions(deviceScope: string): void {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink));
if (deviceScope === 'tenant') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink,
this.config.componentsData.deviceProfileId !== null ?
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id);
} else if (deviceScope === 'edge' || deviceScope === 'edge_customer_user') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getEdgeDevices(this.config.componentsData.edgeId, pageLink, this.config.componentsData.edgeType);
} else {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink,
this.config.componentsData.deviceProfileId !== null ?
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id);
this.config.deleteEntity = () => of();
}
}
prepareDeviceInfoQuery(pageLink: PageLink): DeviceInfoQuery {
const deviceInfoFilter: DeviceInfoFilter = deepClone(this.config.componentsData.deviceInfoFilter);
if (this.config.componentsData.deviceScope === 'edge' || this.config.componentsData.deviceScope === 'edge_customer_user') {
deviceInfoFilter.edgeId = new EdgeId(this.config.componentsData.edgeId);
} else if (this.config.componentsData.deviceScope !== 'tenant') {
deviceInfoFilter.customerId = new CustomerId(this.customerId);
}
return new DeviceInfoQuery(pageLink, deviceInfoFilter);
}
configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
@ -540,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
data: {
deviceId: device.id.id,
deviceProfileId: device.deviceProfileId.id,
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user'
}
}).afterClosed().subscribe(deviceCredentials => {
if (isDefinedAndNotNull(deviceCredentials)) {

6
ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw

@ -648,9 +648,9 @@
"padding": "16px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\" [queryParams]=\"{active: false}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\" [queryParams]=\"{active: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
},
"title": "Devices",
"showTitleIcon": false,
@ -804,7 +804,7 @@
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\" [queryParams]=\"{severityList: ['CRITICAL']}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\" [queryParams]=\"{assignedToMe: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n"
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n"
},
"title": "Alarms",
"showTitleIcon": false,

127
ui-ngx/src/app/shared/models/device.models.ts

@ -35,6 +35,9 @@ import {
getDefaultProfileObserveAttrConfig,
PowerMode
} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { PageLink } from '@shared/models/page/page-link';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { EdgeId } from '@shared/models/id/edge-id';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
@ -328,7 +331,7 @@ export interface DeviceProvisionConfiguration {
allowCreateNewDevicesByX509Certificate?: boolean;
}
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
export const createDeviceProfileConfiguration = (type: DeviceProfileType): DeviceProfileConfiguration => {
let configuration: DeviceProfileConfiguration = null;
if (type) {
switch (type) {
@ -339,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic
}
}
return configuration;
}
};
export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration {
export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => {
let configuration: DeviceConfiguration = null;
if (type) {
switch (type) {
@ -352,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig
}
}
return configuration;
}
};
export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration {
export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => {
let transportConfiguration: DeviceProfileTransportConfiguration = null;
if (type) {
switch (type) {
@ -408,9 +411,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
}
}
return transportConfiguration;
}
};
export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration {
export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => {
let transportConfiguration: DeviceTransportConfiguration = null;
if (type) {
switch (type) {
@ -446,7 +449,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
}
}
return transportConfiguration;
}
};
export enum AlarmConditionType {
SIMPLE = 'SIMPLE',
@ -489,7 +492,7 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
export interface AlarmSchedule{
dynamicValue?: {
sourceAttribute: string,
sourceAttribute: string;
sourceType: string;
};
type: AlarmScheduleType;
@ -516,17 +519,13 @@ interface AlarmRule {
export { AlarmRule as DeviceProfileAlarmRule };
export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null {
const alarmRuleValid = (alarmRule: AlarmRule): boolean =>
!(!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length);
export const alarmRuleValidator = (control: AbstractControl): ValidationErrors | null => {
const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true};
}
function alarmRuleValid(alarmRule: AlarmRule): boolean {
if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) {
return false;
}
return true;
}
};
export interface DeviceProfileAlarm {
id: string;
@ -539,7 +538,7 @@ export interface DeviceProfileAlarm {
propagateRelationTypes?: Array<string>;
}
export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null {
export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => {
const deviceProfileAlarm: DeviceProfileAlarm = control.value;
if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType &&
deviceProfileAlarm.createRules) {
@ -564,7 +563,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio
}
}
return {deviceProfileAlarm: true};
}
};
export interface DeviceProfileData {
@ -718,6 +717,47 @@ export interface DeviceInfo extends Device {
customerTitle: string;
customerIsPublic: boolean;
deviceProfileName: string;
active: boolean;
}
export interface DeviceInfoFilter {
customerId?: CustomerId;
edgeId?: EdgeId;
type?: string;
deviceProfileId?: DeviceProfileId;
active?: boolean;
}
export class DeviceInfoQuery {
pageLink: PageLink;
deviceInfoFilter: DeviceInfoFilter;
constructor(pageLink: PageLink, deviceInfoFilter: DeviceInfoFilter) {
this.pageLink = pageLink;
this.deviceInfoFilter = deviceInfoFilter;
}
public toQuery(): string {
let query;
if (this.deviceInfoFilter.customerId) {
query = `/customer/${this.deviceInfoFilter.customerId.id}/deviceInfos`;
} else if (this.deviceInfoFilter.edgeId) {
query = `/edge/${this.deviceInfoFilter.edgeId.id}/devices`;
} else {
query = '/tenant/deviceInfos';
}
query += this.pageLink.toQuery();
if (isNotEmptyStr(this.deviceInfoFilter.type)) {
query += `&type=${this.deviceInfoFilter.type}`;
} else if (this.deviceInfoFilter.deviceProfileId) {
query += `&deviceProfileId=${this.deviceInfoFilter.deviceProfileId.id}`;
}
if (isDefinedAndNotNull(this.deviceInfoFilter.active)) {
query += `&active=${this.deviceInfoFilter.active}`;
}
return query;
}
}
export enum DeviceCredentialsType {
@ -763,13 +803,11 @@ export interface DeviceCredentialMQTTBasic {
password: string;
}
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
return {
clientId: '',
userName: '',
password: ''
};
}
export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({
clientId: '',
userName: '',
password: ''
});
export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>;
@ -800,44 +838,23 @@ export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.sunday'
);
export function getDayString(day: number): string {
switch (day) {
case 0:
return 'device-profile.schedule-day.monday';
case 1:
return this.translate.instant('device-profile.schedule-day.tuesday');
case 2:
return this.translate.instant('device-profile.schedule-day.wednesday');
case 3:
return this.translate.instant('device-profile.schedule-day.thursday');
case 4:
return this.translate.instant('device-profile.schedule-day.friday');
case 5:
return this.translate.instant('device-profile.schedule-day.saturday');
case 6:
return this.translate.instant('device-profile.schedule-day.sunday');
}
}
export function timeOfDayToUTCTimestamp(date: Date | number): number {
export const timeOfDayToUTCTimestamp = (date: Date | number): number => {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
};
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
}
export const utcTimestampToTimeOfDay = (time = 0): Date => new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
function timeOfDayToMoment(date: Date | number): _moment.Moment {
const timeOfDayToMoment = (date: Date | number): _moment.Moment => {
if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]);
}
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
}
};
export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string {
export const getAlarmScheduleRangeText = (startsOn: Date | number, endsOn: Date | number): string => {
const start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn);
if (start < end) {
@ -847,4 +864,4 @@ export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
}
};

22
ui-ngx/src/assets/home/no_data_folder_bg.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="95" height="76" fill="none" version="1.1" viewBox="0 0 95 76" xmlns="http://www.w3.org/2000/svg">
<path d="m51.403 0.0064445c-0.33266 0.0098-0.6688 0.03164-1.0078 0.06445-10.642 1.0299-13.531 15.151-21.564 22.271-9.2267 8.1782-26.213 9.5198-28.119 21.764-1.9245 12.36 9.6705 18.371 19.793 25.592 8.5214 6.0789 19.566 5.925 30 6.0606 10.653 0.1384 21.672 1.6547 30-5.0508 9.0209-7.2631 14.774-14.987 13.91-26.602-0.8211-11.046-10.09-18.582-17.885-26.379-7.4985-7.5013-14.814-18.025-25.127-17.721zm-24.902 28.377h50c1.1046 0 2 0.87988 2 1.9844v33.535c0 0.67107-0.32283 1.2562-0.82227 1.6152-0.2557 0.78477-0.98095 1.3848-1.8945 1.3848h-50.225c-0.8252 0-1.5611-0.49632-1.8594-1.2656-1.5098-3.8931-6.1084-15.77-8.2363-21.449-0.482-1.2863 0.4682-2.627 1.8418-2.627h7.1953v-11.178c0-1.1046 0.8954-2 2-2z" fill="#00664d" fill-opacity=".039216"/>
<path d="m25 63.889v-33.506c0-0.8284 0.6716-1.5 1.5-1.5h50c0.8334 0 1.5 0.6613 1.5 1.4847v33.535c0 0.8329-0.6496 1.4871-1.4736 1.4871h-50.038c-0.8248 0-1.4888-0.6678-1.4888-1.5z" fill="none" stroke="#00695C" stroke-opacity=".56"/>
<path d="m25.56 66.403c-0.6222 0-1.1711-0.3725-1.3936-0.9462-1.5102-3.8941-6.109-15.769-8.2357-21.445-0.3564-0.9513 0.3411-1.951 1.3744-1.951h50.362c1.4933 0 2.8049 0.9447 3.2568 2.3597 1.6176 5.0656 4.8019 15.263 6.2931 20.046 0.2999 0.962-0.4196 1.9369-1.4342 1.9369h-50.223z" fill="none" stroke="#00695C" stroke-opacity=".56"/>
<g fill="#00695C" fill-opacity=".2">
<ellipse cx="49.5" cy="31.014" rx="1" ry="1.0137"/>
<ellipse cx="43" cy="24.52" rx="1.5" ry="1.5205"/>
<circle cx="55" cy="36.5" r="1.5"/>
</g>
<g transform="translate(-15.413 -35.297)">
<path d="m54.413 82.94c-1.9248 0-3.5 1.5928-3.5 3.5488 0 1.9559 1.5752 3.5488 3.5 3.5488 1.9248 0 3.5-1.5929 3.5-3.5488 0-1.956-1.5752-3.5488-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 0.08594 0.0039 1.75 1.774 0 0 1 0.08398 0.0059 1.75 1.774 0 0 1 0.08594 0.0098 1.75 1.774 0 0 1 0.08398 0.01563 1.75 1.774 0 0 1 0.08398 0.01953 1.75 1.774 0 0 1 0.08398 0.02344 1.75 1.774 0 0 1 0.08008 0.02734 1.75 1.774 0 0 1 0.08008 0.03125 1.75 1.774 0 0 1 0.08008 0.03516 1.75 1.774 0 0 1 0.07617 0.03906 1.75 1.774 0 0 1 0.07422 0.04297 1.75 1.774 0 0 1 0.07227 0.04687 1.75 1.774 0 0 1 0.07031 0.04883 1.75 1.774 0 0 1 0.06836 0.05469 1.75 1.774 0 0 1 0.06445 0.05664 1.75 1.774 0 0 1 0.03711 0.03516c1.1127 0.25355 1.9492 1.2586 1.9492 2.4863 0 1.4235-1.1238 2.5488-2.5 2.5488-1.3762 0-2.5-1.1254-2.5-2.5488 0-0.29103 0.04828-0.5691 0.13477-0.82812a1.75 1.774 0 0 1-0.02734-0.12695 1.75 1.774 0 0 1-0.01758-0.25195 1.75 1.774 0 0 1 0.01758-0.25391 1.75 1.774 0 0 1 0.05273-0.24609 1.75 1.774 0 0 1 0.08789-0.23828 1.75 1.774 0 0 1 0.11914-0.2207 1.75 1.774 0 0 1 0.14844-0.20312 1.75 1.774 0 0 1 0.17774-0.17969 1.75 1.774 0 0 1 0.19922-0.15039 1.75 1.774 0 0 1 0.21875-0.12109 1.75 1.774 0 0 1 0.23437-0.08984 1.75 1.774 0 0 1 0.24414-0.05273 1.75 1.774 0 0 1 0.25-0.01953z" color="#000000" fill="#00695c" fill-opacity=".56" style="-inkscape-stroke:none"/>
<path d="m54.413 82.941a3.5 3.5479 0 0 0-3.5 3.5488 3.5 3.5479 0 0 0 3.5 3.5469 3.5 3.5479 0 0 0 3.5-3.5469 3.5 3.5479 0 0 0-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 1.75 1.7734 1.75 1.774 0 0 1-1.75 1.7754 1.75 1.774 0 0 1-1.75-1.7754 1.75 1.774 0 0 1 1.75-1.7734z" fill="#00695a" fill-opacity=".4"/>
</g>
<g transform="translate(-1.4128 -35.28)">
<path d="m54.413 82.94c-1.9248 0-3.5 1.5928-3.5 3.5488 0 1.9559 1.5752 3.5488 3.5 3.5488 1.9248 0 3.5-1.5929 3.5-3.5488 0-1.956-1.5752-3.5488-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 0.08594 0.0039 1.75 1.774 0 0 1 0.08398 0.0059 1.75 1.774 0 0 1 0.08594 0.0098 1.75 1.774 0 0 1 0.08398 0.01563 1.75 1.774 0 0 1 0.08398 0.01953 1.75 1.774 0 0 1 0.08398 0.02344 1.75 1.774 0 0 1 0.08008 0.02734 1.75 1.774 0 0 1 0.08008 0.03125 1.75 1.774 0 0 1 0.08008 0.03516 1.75 1.774 0 0 1 0.07617 0.03906 1.75 1.774 0 0 1 0.07422 0.04297 1.75 1.774 0 0 1 0.07227 0.04687 1.75 1.774 0 0 1 0.07031 0.04883 1.75 1.774 0 0 1 0.06836 0.05469 1.75 1.774 0 0 1 0.06445 0.05664 1.75 1.774 0 0 1 0.03711 0.03516c1.1127 0.25355 1.9492 1.2586 1.9492 2.4863 0 1.4235-1.1238 2.5488-2.5 2.5488-1.3762 0-2.5-1.1254-2.5-2.5488 0-0.29103 0.04828-0.5691 0.13477-0.82812a1.75 1.774 0 0 1-0.02734-0.12695 1.75 1.774 0 0 1-0.01758-0.25195 1.75 1.774 0 0 1 0.01758-0.25391 1.75 1.774 0 0 1 0.05273-0.24609 1.75 1.774 0 0 1 0.08789-0.23828 1.75 1.774 0 0 1 0.11914-0.2207 1.75 1.774 0 0 1 0.14844-0.20312 1.75 1.774 0 0 1 0.17774-0.17969 1.75 1.774 0 0 1 0.19922-0.15039 1.75 1.774 0 0 1 0.21875-0.12109 1.75 1.774 0 0 1 0.23437-0.08984 1.75 1.774 0 0 1 0.24414-0.05273 1.75 1.774 0 0 1 0.25-0.01953z" color="#000000" fill="#00695c" fill-opacity=".56" style="-inkscape-stroke:none"/>
<path d="m54.413 82.941a3.5 3.5479 0 0 0-3.5 3.5488 3.5 3.5479 0 0 0 3.5 3.5469 3.5 3.5479 0 0 0 3.5-3.5469 3.5 3.5479 0 0 0-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 1.75 1.7734 1.75 1.774 0 0 1-1.75 1.7754 1.75 1.774 0 0 1-1.75-1.7754 1.75 1.774 0 0 1 1.75-1.7734z" fill="#00695a" fill-opacity=".4"/>
</g>
<path d="m42.5 59.807c1.098-0.765 4.2353-1.8359 8 0" stroke="#00695C" stroke-linecap="round" stroke-opacity=".56"/>
<circle cx="52.5" cy="20" r="1" fill="#00695C" fill-opacity=".2"/>
<circle cx="45.5" cy="11" r="1" fill="#00695C" fill-opacity=".2"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

11
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1296,6 +1296,14 @@
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"is-gateway": "Is gateway",
"overwrite-activity-time": "Overwrite activity time for connected device",
"device-filter": "Device filter",
"device-filter-title": "Device Filter",
"filter-title": "Filter",
"device-state": "Device state",
"state": "State",
"any": "Any",
"active": "Active",
"inactive": "Inactive",
"public": "Public",
"device-public": "Device is public",
"select-device": "Select device",
@ -5131,6 +5139,9 @@
"color": "Color",
"shadow-color": "Shadow color"
},
"home": {
"no-data-available": "No data available"
},
"system-info": {
"cpu": "CPU",
"ram": "RAM",

2
ui-ngx/src/styles.scss

@ -266,7 +266,7 @@ pre.tb-highlight {
letter-spacing: normal;
}
.tb-timewindow-panel, .tb-legend-config-panel, .tb-alarm-filter-panel {
.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel {
overflow: hidden;
background: #fff;
border-radius: 4px;

Loading…
Cancel
Save