Browse Source

Merge branch 'master' into develop/3.5.1

pull/8568/head
Igor Kulikov 3 years ago
parent
commit
0beb2d7033
  1. 276
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 10
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  4. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 4
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java
  6. 22
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  8. 7
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  9. 43
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  10. 8
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  12. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  13. 7
      application/src/main/resources/thingsboard.yml
  14. 2
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  15. 34
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  16. 3
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java
  17. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
  18. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  19. 15
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
  20. 35
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VcUtils.java
  21. 2
      common/util/pom.xml
  22. 14
      common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java
  23. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  24. 5
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  26. 3
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java
  27. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java
  28. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java
  29. 267
      dao/src/main/resources/sql/schema-entities.sql
  30. 282
      dao/src/main/resources/sql/schema-views-and-functions.sql
  31. 1
      dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
  32. 1
      dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java
  33. 10
      dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java
  34. 19
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  35. 4
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java
  36. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java
  37. 39
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmWidgetElements.java
  38. 72
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupElements.java
  39. 48
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupHelper.java
  40. 35
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java
  41. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java
  42. 6
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java
  43. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java
  44. 146
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java
  45. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java
  46. 2
      msa/tb/pom.xml
  47. 4
      pom.xml
  48. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  49. 51
      ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch
  50. 8
      ui-ngx/src/app/modules/common/modules-map.ts
  51. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  52. 62
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html
  53. 6
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts
  54. 29
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  55. 6
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  56. 2
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts
  57. 2
      ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts
  58. 16
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  59. 2
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html
  60. 11
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts
  61. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  63. 8
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  64. 3
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
  65. 2
      ui-ngx/src/app/modules/home/pages/admin/settings-card.scss
  66. 2
      ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts
  67. 7
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  68. 28
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts
  69. 4
      ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts
  70. 7
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html
  71. 1
      ui-ngx/src/app/shared/components/button/copy-button.component.html
  72. 13
      ui-ngx/src/app/shared/components/button/copy-button.component.scss
  73. 6
      ui-ngx/src/app/shared/components/button/copy-button.component.ts
  74. 2
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  75. 2
      ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts
  76. 2
      ui-ngx/src/app/shared/components/markdown.component.ts
  77. 2
      ui-ngx/src/app/shared/components/notification/notification.component.scss
  78. 2
      ui-ngx/src/app/shared/components/notification/notification.component.ts
  79. 2
      ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts
  80. 2
      ui-ngx/src/app/shared/components/popover.component.ts
  81. 2
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html
  82. 20
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts
  83. 2
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  84. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  85. 2
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  86. 108
      ui-ngx/src/app/shared/decorators/coercion.ts
  87. 21
      ui-ngx/src/app/shared/decorators/public-api.ts
  88. 1
      ui-ngx/src/app/shared/public-api.ts
  89. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -14,32 +14,6 @@
-- 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
@ -203,256 +177,6 @@ CREATE TABLE IF NOT EXISTS user_settings (
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE,
CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type)
);
-- ALARM INFO VIEW
DROP VIEW IF EXISTS alarm_info CASCADE;
CREATE VIEW alarm_info AS
SELECT a.*,
(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK'
WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK'
WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK'
WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END
, 'Deleted') originator_name,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select COALESCE(NULLIF(title, ''), email) from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select COALESCE(NULLIF(label, ''), name) from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select COALESCE(NULLIF(label, ''), name) from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select COALESCE(NULLIF(label, ''), name) from edge where id = a.originator_id) END
, 'Deleted') as originator_label,
u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email
FROM alarm a
LEFT JOIN tb_user u ON u.id = a.assignee_id;
-- ALARM INFO VIEW END
-- ALARM FUNCTIONS START
DROP FUNCTION IF EXISTS create_or_update_active_alarm;
CREATE OR REPLACE FUNCTION create_or_update_active_alarm(
t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint,
a_o_id uuid, a_o_type integer, a_type varchar,
a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar,
a_creation_enabled boolean)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid;
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE;
IF existing.id IS NULL THEN
IF a_creation_enabled = FALSE THEN
RETURN json_build_object('success', false)::text;
END IF;
IF c_id = null_id THEN
c_id = NULL;
end if;
INSERT INTO alarm
(tenant_id, customer_id, id, created_time,
originator_id, originator_type, type,
severity, start_ts, end_ts,
additional_info,
propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types,
acknowledged, ack_ts,
cleared, clear_ts,
assignee_id, assign_ts)
VALUES
(t_id, c_id, a_id, a_created_ts,
a_o_id, a_o_type, a_type,
a_severity, a_start_ts, a_end_ts,
a_details,
a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types,
false, 0, false, 0, NULL, 0);
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text;
ELSE
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = existing.id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text;
END IF;
END IF;
END
$$;
DROP FUNCTION IF EXISTS update_alarm;
CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = a_id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text;
END IF;
END
$$;
DROP FUNCTION IF EXISTS acknowledge_alarm;
CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT (existing.acknowledged) THEN
modified = TRUE;
UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
END
$$;
DROP FUNCTION IF EXISTS clear_alarm;
CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
cleared boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT(existing.cleared) THEN
cleared = TRUE;
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS assign_alarm;
CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS unassign_alarm;
CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NOT NULL THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
-- ALARM FUNCTIONS END
-- TTL DROP PARTITIONS FUNCTIONS UPDATE START

10
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -70,8 +70,8 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override
public void onUpdate(TbActorCtx context) throws Exception {
if (isMyNodePartition()) {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
if (isMyNodePartition(newRuleNode)) {
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
@ -181,7 +181,11 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
}
private boolean isMyNodePartition() {
return !ruleNode.isSingletonMode()
return isMyNodePartition(this.ruleNode);
}
private boolean isMyNodePartition(RuleNode ruleNode) {
return ruleNode == null || !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId());
}

2
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -305,6 +306,7 @@ public class AdminController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/autoCommitSettings")
public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException {
settings.values().forEach(config -> VcUtils.checkBranchName(config.getBranch()));
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
return autoCommitSettingsService.save(getTenantId(), settings);
}

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

@ -249,6 +249,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.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
@ -275,6 +276,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Installing DataBase schema for timeseries...");

4
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallService.java

@ -67,7 +67,9 @@ public class DefaultEdgeInstallService implements EdgeInstallService {
dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl);
}
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", appVersion + "EDGE");
String edgeVersion = appVersion + "EDGE";
edgeVersion = edgeVersion.replace("-SNAPSHOT", "");
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort));

22
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -110,6 +111,8 @@ import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Service
@Profile("install")
@ -682,18 +685,29 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
}
@Override
@SneakyThrows
public void createDefaultNotificationConfigs() {
log.info("Creating default notification configs for system admin");
if (notificationTargetService.findNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID, new PageLink(1)).getTotalElements() == 0) {
if (notificationTargetService.countNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID) == 0) {
notificationSettingsService.createDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
}
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500);
ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
log.info("Creating default notification configs for all tenants");
AtomicInteger count = new AtomicInteger();
for (TenantId tenantId : tenants) {
if (notificationTargetService.findNotificationTargetsByTenantId(tenantId, new PageLink(1)).getTotalElements() == 0) {
notificationSettingsService.createDefaultNotificationConfigs(tenantId);
}
executor.submit(() -> {
if (notificationTargetService.countNotificationTargetsByTenantId(tenantId) == 0) {
notificationSettingsService.createDefaultNotificationConfigs(tenantId);
int n = count.incrementAndGet();
if (n % 500 == 0) {
log.info("{} tenants processed", n);
}
}
});
}
executor.shutdown();
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
}

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

@ -19,4 +19,6 @@ public interface EntityDatabaseSchemaService extends DatabaseSchemaService {
void createOrUpdateDeviceInfoView(boolean activityStateInTelemetry);
void createOrUpdateViewsAndFunctions() throws Exception;
}

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

@ -27,6 +27,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql";
public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql";
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql";
public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
@ -45,4 +46,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
executeQuery("DROP VIEW IF EXISTS device_info_view CASCADE;");
executeQuery("CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM " + sourceViewName + ";");
}
@Override
public void createOrUpdateViewsAndFunctions() throws Exception {
log.info("Installing SQL DataBase schema views and functions: " + SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
executeQueryFromFile(SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
}
}

43
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -15,13 +15,17 @@
*/
package org.thingsboard.server.service.notification.rule;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -49,6 +53,8 @@ import org.thingsboard.server.service.notification.rule.cache.NotificationRulesC
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor;
import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
@ -71,9 +77,19 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
@Autowired @Lazy
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
private final CacheManager cacheManager;
private Cache sentNotifications;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
@PostConstruct
private void init() {
sentNotifications = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE);
if (sentNotifications == null) {
throw new IllegalStateException("Sent notifications cache is not set up");
}
}
@Override
public void process(NotificationRuleTrigger trigger) {
NotificationRuleTriggerType triggerType = trigger.getType();
@ -126,6 +142,9 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return;
}
if (trigger.getType().isDeduplicate() && alreadySent(rule.getId(), trigger)) {
return;
}
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
@ -175,6 +194,23 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger);
}
private boolean alreadySent(NotificationRuleId ruleId, NotificationRuleTrigger trigger) {
String key = ruleId + "_" + trigger.getOriginatorEntityId();
SentNotification sent = sentNotifications.get(key, SentNotification.class);
boolean alreadySent;
if (sent != null && sent.getTrigger().equals(trigger)) {
alreadySent = true;
log.debug("Notification for {} trigger was already sent, ignoring", trigger.getType());
// updating cache anyway so that the value is not removed by ttl
} else {
alreadySent = false;
sent = new SentNotification(trigger);
}
log.trace("[{}] Putting to sentNotifications cache: {}", ruleId, trigger);
sentNotifications.put(key, sent);
return alreadySent;
}
@EventListener(ComponentLifecycleMsg.class)
public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) {
if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED ||
@ -209,4 +245,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
RuleEngineMsgTrigger.msgTypeToTriggerType = ruleEngineMsgTypeToTriggerType;
}
@Data
private static class SentNotification implements Serializable {
private static final long serialVersionUID = 38973480405095422L;
private final NotificationRuleTrigger trigger;
}
}

8
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java

@ -18,26 +18,18 @@ package org.thingsboard.server.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;
@Service
@RequiredArgsConstructor
public class NewPlatformVersionTriggerProcessor implements NotificationRuleTriggerProcessor<NewPlatformVersionTrigger, NewPlatformVersionNotificationRuleTriggerConfig> {
private final PartitionService partitionService;
@Override
public boolean matchesFilter(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) {
if (!partitionService.isMyPartition(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)) {
return false;
}
return trigger.getUpdateInfo().isUpdateAvailable();
}

2
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -473,7 +473,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.ACTIVE_DEVICES, active);
apiUsageReportClient.report(tenantId, null, ApiUsageRecordKey.INACTIVE_DEVICES, inactive);
if (active > 0) {
log.info("[{}] Active devices: {}, inactive devices: {}", tenantId, active, inactive);
log.debug("[{}] Active devices: {}, inactive devices: {}", tenantId, active, inactive);
}
});
} catch (Throwable t) {

6
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@ -32,7 +33,6 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -101,6 +101,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.transform;
import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName;
@Service
@TbCoreComponent
@ -138,6 +139,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@SuppressWarnings("UnstableApiUsage")
@Override
public ListenableFuture<UUID> saveEntitiesVersion(User user, VersionCreateRequest request) throws Exception {
checkBranchName(request.getBranch());
var pendingCommit = gitServiceQueue.prepareCommit(user, request);
DonAsynchron.withCallback(pendingCommit, commit -> {
cachePut(commit.getTxId(), new VersionCreationResult());
@ -504,6 +506,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override
public ListenableFuture<RepositorySettings> saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings) {
checkBranchName(versionControlSettings.getDefaultBranch());
var restoredSettings = this.repositorySettingsService.restore(tenantId, versionControlSettings);
try {
var future = gitServiceQueue.initRepository(tenantId, restoredSettings);
@ -525,6 +528,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override
public ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws ThingsboardException {
checkBranchName(settings.getDefaultBranch());
settings = this.repositorySettingsService.restore(tenantId, settings);
try {
return gitServiceQueue.testRepository(tenantId, settings);

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

@ -132,7 +132,7 @@ security:
githubMapper:
emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
java_cacerts:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}${file.separator}lib${file.separator}security${file.separator}cacerts}"
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
# Usage statistics parameters
@ -159,7 +159,7 @@ ui:
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.4}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.5}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -449,6 +449,9 @@ cache:
notificationSettings:
timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_SETTINGS_TTL:10}"
maxSize: "${CACHE_SPECS_NOTIFICATION_SETTINGS_MAX_SIZE:1000}"
sentNotifications:
timeToLiveInMinutes: "${CACHE_SPECS_SENT_NOTIFICATIONS_TTL:1440}"
maxSize: "${CACHE_SPECS_SENT_NOTIFICATIONS_MAX_SIZE:10000}"
attributes:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}"
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}"

2
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -204,7 +204,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB);
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " [" + Arrays.toString(targets) + "]");
rule.setName(triggerConfig.getTriggerType() + " " + Arrays.toString(targets));
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);

34
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
@ -55,6 +56,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -66,9 +68,11 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
@ -102,6 +106,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
private RateLimitService rateLimitService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;
@Before
public void beforeEach() throws Exception {
@ -433,6 +439,34 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(notificationRequestsLimit);
}
@Test
public void testNotificationsDeduplication() throws Exception {
loginSysAdmin();
NewPlatformVersionNotificationRuleTriggerConfig triggerConfig = new NewPlatformVersionNotificationRuleTriggerConfig();
createNotificationRule(triggerConfig, "Test", "Test", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
assertThat(getMyNotifications(false, 100)).size().isZero();
for (int i = 1; i <= 10; i++) {
notificationRuleProcessor.process(NewPlatformVersionTrigger.builder()
.updateInfo(new UpdateMessage(true, "test", "test",
"test", "test", "test"))
.build());
TimeUnit.MILLISECONDS.sleep(300);
}
TimeUnit.SECONDS.sleep(5);
assertThat(getMyNotifications(false, 100)).size().isOne();
notificationRuleProcessor.process(NewPlatformVersionTrigger.builder()
.updateInfo(new UpdateMessage(true, "CHANGED", "test",
"test", "test", "test"))
.build());
await().atMost(5, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(2);
});
}
private <R> R checkNotificationAfter(Callable<R> action, BiConsumer<Notification, R> check) throws Exception {
if (getWsClient().getLastDataUpdate() == null) {
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);

3
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -167,6 +168,8 @@ class TbelInvokeServiceTest extends AbstractControllerTest {
}
@Test
@Ignore("This test is based on assumption that Caffeine cache is LRU based but in fact it is based on " +
"Tiny LFU which is the cause that the tests fail sometime: https://arxiv.org/pdf/1512.00727.pdf")
public void whenCompiledScriptsCacheIsTooBig_thenRemoveRarelyUsedScripts() throws Exception {
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");

2
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java

@ -50,4 +50,6 @@ public interface NotificationTargetService {
void deleteNotificationTargetsByTenantId(TenantId tenantId);
long countNotificationTargetsByTenantId(TenantId tenantId);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -30,6 +30,7 @@ public class CacheConstants {
public static final String TENANTS_EXIST_CACHE = "tenantsExist";
public static final String DEVICE_PROFILE_CACHE = "deviceProfiles";
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes";

15
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java

@ -16,8 +16,10 @@
package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum NotificationRuleTriggerType {
ENTITY_ACTION,
@ -26,18 +28,15 @@ public enum NotificationRuleTriggerType {
ALARM_ASSIGNMENT,
DEVICE_ACTIVITY,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
NEW_PLATFORM_VERSION(false),
ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false);
NEW_PLATFORM_VERSION(false, true),
ENTITIES_LIMIT(false, false),
API_USAGE_LIMIT(false, false);
private final boolean tenantLevel;
NotificationRuleTriggerType(boolean tenantLevel) {
this.tenantLevel = tenantLevel;
}
private final boolean deduplicate;
NotificationRuleTriggerType() {
this(true);
this(true, false);
}
}

35
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/VcUtils.java

@ -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.
*/
package org.thingsboard.server.common.data.sync.vc;
import org.apache.commons.lang3.StringUtils;
public class VcUtils {
private VcUtils() {}
public static void checkBranchName(String branch) {
if (StringUtils.isEmpty(branch)) return;
boolean invalid = StringUtils.containsWhitespace(branch) ||
StringUtils.containsAny(branch, "..", "~", "^", ":", "\\") ||
StringUtils.endsWithAny(branch, "/", ".lock");
if (invalid) {
throw new IllegalArgumentException("Branch name is invalid");
}
}
}

2
common/util/pom.xml

@ -89,7 +89,7 @@
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>com.github.dblock</groupId>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
</dependency>
</dependencies>

14
common/util/src/main/java/org/thingsboard/common/util/SystemUtil.java

@ -42,7 +42,7 @@ public class SystemUtil {
long total = memory.getTotal();
long available = memory.getAvailable();
return Optional.of(toPercent(total - available, total));
} catch (Exception e) {
} catch (Throwable e) {
log.debug("Failed to get memory usage!!!", e);
}
return Optional.empty();
@ -51,7 +51,7 @@ public class SystemUtil {
public static Optional<Long> getTotalMemory() {
try {
return Optional.of(HARDWARE.getMemory().getTotal());
} catch (Exception e) {
} catch (Throwable e) {
log.debug("Failed to get total memory!!!", e);
}
return Optional.empty();
@ -59,8 +59,8 @@ public class SystemUtil {
public static Optional<Integer> getCpuUsage() {
try {
return Optional.of((int) (HARDWARE.getProcessor().getSystemCpuLoad() * 100.0));
} catch (Exception e) {
return Optional.of((int) (HARDWARE.getProcessor().getSystemCpuLoad(1000) * 100.0));
} catch (Throwable e) {
log.debug("Failed to get cpu usage!!!", e);
}
return Optional.empty();
@ -69,7 +69,7 @@ public class SystemUtil {
public static Optional<Integer> getCpuCount() {
try {
return Optional.of(HARDWARE.getProcessor().getLogicalProcessorCount());
} catch (Exception e) {
} catch (Throwable e) {
log.debug("Failed to get total cpu count!!!", e);
}
return Optional.empty();
@ -81,7 +81,7 @@ public class SystemUtil {
long total = store.getTotalSpace();
long available = store.getUsableSpace();
return Optional.of(toPercent(total - available, total));
} catch (Exception e) {
} catch (Throwable e) {
log.debug("Failed to get free disc space!!!", e);
}
return Optional.empty();
@ -91,7 +91,7 @@ public class SystemUtil {
try {
FileStore store = Files.getFileStore(Paths.get("/"));
return Optional.of(store.getTotalSpace());
} catch (Exception e) {
} catch (Throwable e) {
log.debug("Failed to get total disc space!!!", e);
}
return Optional.empty();

2
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -98,7 +98,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForTenant, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId(), tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId());
return;
}

5
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -192,6 +192,11 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
notificationTargetDao.removeByTenantId(tenantId);
}
@Override
public long countNotificationTargetsByTenantId(TenantId tenantId) {
return notificationTargetDao.countByTenantId(tenantId);
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId())));

2
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -143,7 +143,7 @@ public class DefaultNotifications {
.rule(DefaultRule.builder()
.name("New platform version")
.triggerConfig(new NewPlatformVersionNotificationRuleTriggerConfig())
.description("Send notification to system admins and tenant admins when new platform version is available")
.description("Send notification to system admins when new platform version is available")
.build())
.build();

3
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java

@ -22,10 +22,11 @@ import org.thingsboard.server.common.data.notification.targets.NotificationTarge
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.TenantEntityDao;
import java.util.List;
public interface NotificationTargetDao extends Dao<NotificationTarget> {
public interface NotificationTargetDao extends Dao<NotificationTarget>, TenantEntityDao {
PageData<NotificationTarget> findByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink);

5
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java

@ -71,6 +71,11 @@ public class JpaNotificationTargetDao extends JpaAbstractDao<NotificationTargetE
notificationTargetRepository.deleteByTenantId(tenantId.getId());
}
@Override
public Long countByTenantId(TenantId tenantId) {
return notificationTargetRepository.countByTenantId(tenantId.getId());
}
@Override
protected Class<NotificationTargetEntity> getEntityClass() {
return NotificationTargetEntity.class;

2
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java

@ -50,4 +50,6 @@ public interface NotificationTargetRepository extends JpaRepository<Notification
@Transactional
void deleteByTenantId(UUID tenantId);
long countByTenantId(UUID tenantId);
}

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

@ -858,270 +858,3 @@ CREATE TABLE IF NOT EXISTS user_settings (
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE,
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.*,
(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK'
WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK'
WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK'
WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END
, 'Deleted') originator_name,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select COALESCE(NULLIF(title, ''), email) from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select COALESCE(NULLIF(label, ''), name) from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select COALESCE(NULLIF(label, ''), name) from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select COALESCE(NULLIF(label, ''), name) from edge where id = a.originator_id) END
, 'Deleted') as originator_label,
u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email
FROM alarm a
LEFT JOIN tb_user u ON u.id = a.assignee_id;
CREATE OR REPLACE FUNCTION create_or_update_active_alarm(
t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint,
a_o_id uuid, a_o_type integer, a_type varchar,
a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar,
a_creation_enabled boolean)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid;
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE;
IF existing.id IS NULL THEN
IF a_creation_enabled = FALSE THEN
RETURN json_build_object('success', false)::text;
END IF;
IF c_id = null_id THEN
c_id = NULL;
end if;
INSERT INTO alarm
(tenant_id, customer_id, id, created_time,
originator_id, originator_type, type,
severity, start_ts, end_ts,
additional_info,
propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types,
acknowledged, ack_ts,
cleared, clear_ts,
assignee_id, assign_ts)
VALUES
(t_id, c_id, a_id, a_created_ts,
a_o_id, a_o_type, a_type,
a_severity, a_start_ts, a_end_ts,
a_details,
a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types,
false, 0, false, 0, NULL, 0);
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text;
ELSE
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = existing.id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text;
END IF;
END IF;
END
$$;
DROP FUNCTION IF EXISTS update_alarm;
CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = a_id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text;
END IF;
END
$$;
DROP FUNCTION IF EXISTS acknowledge_alarm;
CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT (existing.acknowledged) THEN
modified = TRUE;
UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
END
$$;
DROP FUNCTION IF EXISTS clear_alarm;
CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
cleared boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT(existing.cleared) THEN
cleared = TRUE;
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS assign_alarm;
CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS unassign_alarm;
CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NOT NULL THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;

282
dao/src/main/resources/sql/schema-views-and-functions.sql

@ -0,0 +1,282 @@
--
-- 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.
--
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.*,
(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK'
WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK'
WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK'
WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END
, 'Deleted') originator_name,
COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id)
WHEN a.originator_type = 1 THEN (select COALESCE(NULLIF(title, ''), email) from customer where id = a.originator_id)
WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id)
WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id)
WHEN a.originator_type = 4 THEN (select COALESCE(NULLIF(label, ''), name) from asset where id = a.originator_id)
WHEN a.originator_type = 5 THEN (select COALESCE(NULLIF(label, ''), name) from device where id = a.originator_id)
WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id)
WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id)
WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id)
WHEN a.originator_type = 18 THEN (select COALESCE(NULLIF(label, ''), name) from edge where id = a.originator_id) END
, 'Deleted') as originator_label,
u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email
FROM alarm a
LEFT JOIN tb_user u ON u.id = a.assignee_id;
CREATE OR REPLACE FUNCTION create_or_update_active_alarm(
t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint,
a_o_id uuid, a_o_type integer, a_type varchar,
a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar,
a_creation_enabled boolean)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid;
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE;
IF existing.id IS NULL THEN
IF a_creation_enabled = FALSE THEN
RETURN json_build_object('success', false)::text;
END IF;
IF c_id = null_id THEN
c_id = NULL;
end if;
INSERT INTO alarm
(tenant_id, customer_id, id, created_time,
originator_id, originator_type, type,
severity, start_ts, end_ts,
additional_info,
propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types,
acknowledged, ack_ts,
cleared, clear_ts,
assignee_id, assign_ts)
VALUES
(t_id, c_id, a_id, a_created_ts,
a_o_id, a_o_type, a_type,
a_severity, a_start_ts, a_end_ts,
a_details,
a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types,
false, 0, false, 0, NULL, 0);
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text;
ELSE
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = existing.id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text;
END IF;
END IF;
END
$$;
DROP FUNCTION IF EXISTS update_alarm;
CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint,
a_details varchar,
a_propagate boolean, a_propagate_to_owner boolean,
a_propagate_to_tenant boolean, a_propagation_types varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
row_count integer;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
UPDATE alarm a
SET severity = a_severity,
start_ts = a_start_ts,
end_ts = a_end_ts,
additional_info = a_details,
propagate = a_propagate,
propagate_to_owner = a_propagate_to_owner,
propagate_to_tenant = a_propagate_to_tenant,
propagate_relation_types = a_propagation_types
WHERE a.id = a_id
AND a.tenant_id = t_id
AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details
OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR
propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types);
GET DIAGNOSTICS row_count = ROW_COUNT;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
IF row_count > 0 THEN
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
ELSE
RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text;
END IF;
END
$$;
DROP FUNCTION IF EXISTS acknowledge_alarm;
CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT (existing.acknowledged) THEN
modified = TRUE;
UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
END
$$;
DROP FUNCTION IF EXISTS clear_alarm;
CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
cleared boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF NOT(existing.cleared) THEN
cleared = TRUE;
UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS assign_alarm;
CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP FUNCTION IF EXISTS unassign_alarm;
CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint)
RETURNS varchar
LANGUAGE plpgsql
AS
$$
DECLARE
existing alarm;
result alarm_info;
modified boolean = FALSE;
BEGIN
SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE;
IF existing IS NULL THEN
RETURN json_build_object('success', false)::text;
END IF;
IF existing.assignee_id IS NOT NULL THEN
modified = TRUE;
UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;

1
dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java

@ -33,6 +33,7 @@ public class PostgreSqlInitializer {
"sql/schema-entities.sql",
"sql/schema-entities-idx.sql",
"sql/schema-entities-idx-psql-addon.sql",
"sql/schema-views-and-functions.sql",
"sql/system-data.sql",
"sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql";

1
dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java

@ -33,6 +33,7 @@ public class TimescaleSqlInitializer {
"sql/schema-entities.sql",
"sql/schema-entities-idx.sql",
"sql/schema-entities-idx-psql-addon.sql",
"sql/schema-views-and-functions.sql",
"sql/system-data.sql",
"sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/timescale/drop-all-tables.sql";

10
dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java

@ -34,17 +34,21 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
public class EntitiesSchemaSqlTest extends AbstractServiceTest {
@Value("${classpath:sql/schema-entities.sql}")
private Path installScriptPath;
private Path installEntitiesPath;
@Value("${classpath:sql/schema-views-and-functions.sql}")
private Path installViewsPath;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testRepeatedInstall() throws IOException {
String installScript = Files.readString(installScriptPath);
String entitiesScript = Files.readString(installEntitiesPath);
String viewsScript = Files.readString(installViewsPath);
try {
for (int i = 1; i <= 2; i++) {
jdbcTemplate.execute(installScript);
jdbcTemplate.execute(entitiesScript);
jdbcTemplate.execute(viewsScript);
}
} catch (Exception e) {
Assertions.fail("Failed to execute reinstall", e);

19
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -27,6 +27,7 @@ import io.restassured.path.json.JsonPath;
import io.restassured.response.ValidatableResponse;
import io.restassured.specification.RequestSpecification;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -489,4 +491,21 @@ public class TestRestClient {
.then()
.statusCode(HTTP_OK);
}
public Dashboard postDashboard(Dashboard dashboard) {
return given().spec(requestSpec)
.body(dashboard)
.post("/api/dashboard")
.then()
.statusCode(HTTP_OK)
.extract()
.as(Dashboard.class);
}
public void deleteDashboard(DashboardId dashboardId) {
given().spec(requestSpec)
.delete("/api/dashboard/{dashboardId}", dashboardId.getId())
.then()
.statusCode(HTTP_OK);
}
}

4
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java

@ -185,4 +185,8 @@ abstract public class AbstractBasePage {
String s = "~`!@#$^&*()_+=-";
return s.charAt(rand.nextInt(s.length()));
}
public void pull(WebElement element, int xOffset, int yOffset) {
actions.clickAndHold(element).moveByOffset(xOffset, yOffset).release().perform();
}
}

7
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.page.PageLink;
@ -262,4 +263,10 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest {
testRestClient.deleteEntityView(entityViewId);
}
}
public void deleteDashboardById(DashboardId dashboardId) {
if (dashboardId != null) {
testRestClient.deleteDashboard(dashboardId);
}
}
}

39
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmWidgetElements.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.ui.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
public class AlarmWidgetElements extends AlarmDetailsEntityTabHelper {
public AlarmWidgetElements(WebDriver driver) {
super(driver);
}
private static final String ASSIGN_USER_DISPLAY_NAME = "//span[contains(@class,'assigned-container')]/span[contains(text(),'%s')]";
private static final String UNASSIGNED = "//span[text() = '%s']/ancestor::mat-row//span[contains(@class,'assigned-container')]" +
"//mat-icon[text() = 'account_circle']/following-sibling::span";
@Override
public WebElement assignedUser(String userEmail) {
return waitUntilElementToBeClickable(String.format(ASSIGN_USER_DISPLAY_NAME, userEmail));
}
@Override
public WebElement unassigned(String alarmType) {
return waitUntilVisibilityOfElementLocated(String.format(UNASSIGNED, alarmType));
}
}

72
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupElements.java

@ -0,0 +1,72 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.ui.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.thingsboard.server.msa.ui.base.AbstractBasePage;
public class CreateWidgetPopupElements extends AbstractBasePage {
public CreateWidgetPopupElements(WebDriver driver) {
super(driver);
}
private static final String ENTITY_ALIAS = "//input[@formcontrolname='entityAlias']";
private static final String CREATE_NEW_ALIAS_BTN = "//a[text() = 'Create a new one!']/parent::span";
private static final String FILTER_TYPE_FIELD = "//div[contains(@class,'tb-entity-filter')]//mat-select//span";
private static final String TYPE_FIELD = "//mat-select[@formcontrolname='entityType']//span";
private static final String OPTION_FROM_DROPDOWN = "//span[text() = ' %s ']";
private static final String ENTITY_FIELD = "//input[@formcontrolname='entity']";
private static final String ADD_ALIAS_BTN = "//tb-entity-alias-dialog//span[text() = ' Add ']/parent::button";
private static final String ADD_WIDGET_BTN = "//tb-add-widget-dialog//span[text() = ' Add ']/parent::button";
private static final String ENTITY_FROM_DROPDOWN = "//b[text() = '%s']";
public WebElement entityAlias() {
return waitUntilElementToBeClickable(ENTITY_ALIAS);
}
public WebElement createNewAliasBtn() {
return waitUntilElementToBeClickable(CREATE_NEW_ALIAS_BTN);
}
public WebElement filterTypeFiled() {
return waitUntilElementToBeClickable(FILTER_TYPE_FIELD);
}
public WebElement typeFiled() {
return waitUntilElementToBeClickable(TYPE_FIELD);
}
public WebElement optionFromDropdown(String type) {
return waitUntilElementToBeClickable(String.format(OPTION_FROM_DROPDOWN, type));
}
public WebElement entityFiled() {
return waitUntilElementToBeClickable(ENTITY_FIELD);
}
public WebElement addAliasBtn() {
return waitUntilElementToBeClickable(ADD_ALIAS_BTN);
}
public WebElement addWidgetBtn() {
return waitUntilElementToBeClickable(ADD_WIDGET_BTN);
}
public WebElement entityFromDropdown(String entityName) {
return waitUntilVisibilityOfElementLocated(String.format(ENTITY_FROM_DROPDOWN, entityName));
}
}

48
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupHelper.java

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.ui.pages;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
public class CreateWidgetPopupHelper extends CreateWidgetPopupElements {
public CreateWidgetPopupHelper(WebDriver driver) {
super(driver);
}
public void goToCreateEntityAliasPopup(String aliasName) {
entityAlias().sendKeys(aliasName + RandomStringUtils.randomAlphanumeric(7));
createNewAliasBtn().click();
}
public void selectFilterType(String filterType) {
filterTypeFiled().click();
optionFromDropdown(filterType).click();
}
public void selectType(String entityType) {
typeFiled().click();
optionFromDropdown(entityType).click();
}
public void selectEntity(String entityName) {
entityFiled().sendKeys(entityName);
entityFromDropdown(entityName);
entityFiled().sendKeys(Keys.ARROW_DOWN);
entityFiled().sendKeys(Keys.ENTER);
}
}

35
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java

@ -30,6 +30,13 @@ public class DashboardPageElements extends OtherPageElementsHelper {
private static final String MANAGE_ASSIGNED_ENTITY_LIST_FIELD = "//input[@formcontrolname='entity']";
private static final String MANAGE_ASSIGNED_ENTITY = "//mat-option//span[contains(text(),'%s')]";
private static final String MANAGE_ASSIGNED_UPDATE_BTN = "//button[@type='submit']";
private static final String EDIT_BTN = "//mat-icon[text() = 'edit']/parent::button";
private static final String ADD_BTN = "//tb-footer-fab-buttons//button";
private static final String CREAT_NEW_DASHBOARD_BTN = "//mat-icon[text() = 'insert_drive_file']/parent::button";
private static final String ALARM_WIDGET_BUNDLE = "//mat-card-title[text() = 'Alarm widgets']/ancestor::mat-card";
private static final String ALARM_TABLE_WIDGET = "//img[@alt='Alarms table']/ancestor::mat-card";
private static final String WIDGET_SE_CORNER = "//div[contains(@class,'handle-se')]";
private static final String DONE_BTN = "//tb-footer-fab-buttons/following-sibling::button//mat-icon[text() = 'done']/parent::button";
public List<WebElement> entityTitles() {
return waitUntilVisibilityOfElementsLocated(TITLES);
@ -50,4 +57,32 @@ public class DashboardPageElements extends OtherPageElementsHelper {
public WebElement manageAssignedUpdateBtn() {
return waitUntilElementToBeClickable(MANAGE_ASSIGNED_UPDATE_BTN);
}
public WebElement editBtn() {
return waitUntilElementToBeClickable(EDIT_BTN);
}
public WebElement addBtn() {
return waitUntilElementToBeClickable(ADD_BTN);
}
public WebElement createNewDashboardBtn() {
return waitUntilElementToBeClickable(CREAT_NEW_DASHBOARD_BTN);
}
public WebElement alarmWidgetBundle() {
return waitUntilElementToBeClickable(ALARM_WIDGET_BUNDLE);
}
public WebElement alarmTableWidget() {
return waitUntilElementToBeClickable(ALARM_TABLE_WIDGET);
}
public WebElement widgetSECorner() {
return waitUntilElementToBeClickable(WIDGET_SE_CORNER);
}
public WebElement doneBtn() {
return waitUntilVisibilityOfElementLocated(DONE_BTN);
}
}

14
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java

@ -37,4 +37,18 @@ public class DashboardPageHelper extends DashboardPageElements {
jsClick(manageAssignedEntity(title));
jsClick(manageAssignedUpdateBtn());
}
public void openSelectWidgetsBundleMenu() {
addBtn().click();
createNewDashboardBtn().click();
}
public void openCreateWidgetPopup() {
alarmWidgetBundle().click();
alarmTableWidget().click();
}
public void increaseSizeOfTheWidget() {
pull(widgetSECorner(), 700, 200);
}
}

6
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java

@ -27,7 +27,7 @@ public class SideBarMenuViewElements extends AbstractBasePage {
private static final String RULE_CHAINS_BTN = "//mat-toolbar//a[@href='/ruleChains']";
private static final String CUSTOMER_BTN = "//mat-toolbar//a[@href='/customers']";
private static final String DASHBOARD_BTN = "//mat-toolbar//a[@href='/dashboards']";
private static final String PROFILES_BTN = "//mat-toolbar//mat-icon[text()='badge']/ancestor::tb-menu-toggle";
private static final String PROFILES_DROPDOWN = "//mat-toolbar//mat-icon[text()='badge']/ancestor::a//span[contains(@class,'pull-right')]";
private static final String DEVICE_PROFILE_BTN = "//mat-toolbar//a[@href='/profiles/deviceProfiles']";
private static final String ASSET_PROFILE_BTN = "//mat-toolbar//a[@href='/profiles/assetProfiles']";
private static final String ALARMS_BTN = "//mat-toolbar//a[@href='/alarms']";
@ -52,8 +52,8 @@ public class SideBarMenuViewElements extends AbstractBasePage {
return waitUntilElementToBeClickable(DASHBOARD_BTN);
}
public WebElement profilesBtn() {
return waitUntilElementToBeClickable(PROFILES_BTN);
public WebElement profilesDropdown() {
return waitUntilElementToBeClickable(PROFILES_DROPDOWN);
}
public WebElement deviceProfileBtn() {

14
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java

@ -24,12 +24,12 @@ public class SideBarMenuViewHelper extends SideBarMenuViewElements {
}
public void openDeviceProfiles() {
profilesBtn().click();
openProfilesDropDown();
deviceProfileBtn().click();
}
public void openAssetProfiles() {
profilesBtn().click();
openProfilesDropDown();
assetProfileBtn().click();
}
@ -54,10 +54,20 @@ public class SideBarMenuViewHelper extends SideBarMenuViewElements {
}
}
public void openProfilesDropDown() {
if (profilesIsClose()) {
profilesDropdown().click();
}
}
public boolean entitiesDropdownIsClose() {
return dropdownIsClose(entitiesDropdown());
}
public boolean profilesIsClose() {
return dropdownIsClose(profilesDropdown());
}
private boolean dropdownIsClose(WebElement dropdown) {
return !dropdown.getAttribute("class").contains("tb-toggled");
}

146
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java

@ -0,0 +1,146 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa.ui.tests.assignee;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.msa.ui.pages.AlarmWidgetElements;
import org.thingsboard.server.msa.ui.pages.CreateWidgetPopupHelper;
import org.thingsboard.server.msa.ui.pages.DashboardPageHelper;
import org.thingsboard.server.msa.ui.utils.Const;
import org.thingsboard.server.msa.ui.utils.EntityPrototypes;
import static org.assertj.core.api.Assertions.assertThat;
@Feature("Assign from details tab of entity")
public class AssignFromAlarmWidgetTest extends AbstractAssignTest {
private Dashboard dashboard;
private DashboardPageHelper dashboardPage;
private AlarmWidgetElements alarmWidget;
@BeforeClass
public void create() {
dashboardPage = new DashboardPageHelper(driver);
CreateWidgetPopupHelper createWidgetPopup = new CreateWidgetPopupHelper(driver);
alarmWidget = new AlarmWidgetElements(driver);
dashboard = testRestClient.postDashboard(EntityPrototypes.defaultDashboardPrototype("Dashboard"));
sideBarMenuView.dashboardBtn().click();
dashboardPage.entity(dashboard.getName()).click();
dashboardPage.editBtn().click();
dashboardPage.openSelectWidgetsBundleMenu();
dashboardPage.openCreateWidgetPopup();
createWidgetPopup.goToCreateEntityAliasPopup("Alias");
createWidgetPopup.selectFilterType("Single entity");
createWidgetPopup.selectType("Device");
createWidgetPopup.selectEntity(deviceName);
createWidgetPopup.addAliasBtn().click();
createWidgetPopup.addWidgetBtn().click();
dashboardPage.increaseSizeOfTheWidget();
dashboardPage.doneBtn().click();
}
@AfterClass
public void delete() {
deleteDashboardById(dashboard.getId());
}
@BeforeMethod
public void goToDashboardPage() {
sideBarMenuView.dashboardBtn().click();
dashboardPage.entity(dashboard.getName()).click();
}
@Description("Can assign alarm to yourself")
@Test
public void assignAlarmToYourself() {
alarmWidget.assignAlarmTo(alarmType, Const.TENANT_EMAIL);
assertIsDisplayed(alarmWidget.assignedUser(Const.TENANT_EMAIL));
}
@Description("Can reassign alarm to another user")
@Test
public void reassignAlarm() {
alarmWidget.assignAlarmTo(assignedAlarmType, userWithNameEmail);
assertIsDisplayed(alarmWidget.assignedUser(userName));
}
@Description("Can unassign alarm")
@Test
public void unassignedAlarm() {
alarmWidget.unassignedAlarm(assignedAlarmType);
assertIsDisplayed(alarmWidget.unassigned(assignedAlarmType));
}
@Description("Assign alarm to yourself from details of alarm")
@Test
public void assignAlarmToYourselfFromDetails() {
alarmWidget.alarmDetailsBtn(alarmType).click();
alarmDetailsView.assignAlarmTo(Const.TENANT_EMAIL);
alarmDetailsView.closeAlarmDetailsViewBtn().click();
assertIsDisplayed(alarmWidget.assignedUser(Const.TENANT_EMAIL));
}
@Description("Reassign alarm to another user from details of alarm")
@Test
public void reassignAlarmFromDetails() {
alarmWidget.alarmDetailsBtn(assignedAlarmType).click();
alarmDetailsView.assignAlarmTo(userWithNameEmail);
alarmDetailsView.closeAlarmDetailsViewBtn().click();
assertIsDisplayed(alarmWidget.assignedUser(userName));
}
@Description("Unassign alarm from details of alarm")
@Test
public void unassignedAlarmFromDetails() {
alarmWidget.alarmDetailsBtn(assignedAlarmType).click();
alarmDetailsView.unassignedAlarm();
alarmDetailsView.closeAlarmDetailsViewBtn().click();
assertIsDisplayed(alarmWidget.unassigned(assignedAlarmType));
}
@Description("Search by email")
@Test
public void searchByEmail() {
alarmWidget.searchAlarm(alarmType, Const.TENANT_EMAIL);
alarmWidget.setUsers();
assertThat(alarmWidget.getUsers()).hasSize(1).as("Search result contains search input").contains(Const.TENANT_EMAIL);
alarmWidget.assignUsers().forEach(this::assertIsDisplayed);
}
@Description("Search by name")
@Test(groups = "broken")
public void searchByName() {
alarmWidget.searchAlarm(alarmType, userName);
alarmWidget.setUsers();
assertThat(alarmWidget.getUsers()).hasSize(1).as("Search result contains search input").contains(userName);
alarmWidget.assignUsers().forEach(this::assertIsDisplayed);
}
}

7
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java

@ -18,6 +18,7 @@ package org.thingsboard.server.msa.ui.utils;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -206,4 +207,10 @@ public class EntityPrototypes {
entityView.setAdditionalInfo(JacksonUtil.newObjectNode().put("entityType", entityType));
return entityView;
}
public static Dashboard defaultDashboardPrototype(String title) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle(title + RandomStringUtils.randomAlphanumeric(7));
return dashboard;
}
}

2
msa/tb/pom.xml

@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.4.4</pkg.upgradeVersion>
<pkg.upgradeVersion>3.5.0</pkg.upgradeVersion>
</properties>
<dependencies>

4
pom.xml

@ -150,7 +150,7 @@
<allure-testng.version>2.21.0</allure-testng.version>
<allure-maven.version>2.12.0</allure-maven.version>
<slack-api.version>1.12.1</slack-api.version>
<oshi.version>3.4.0</oshi.version>
<oshi.version>6.4.2</oshi.version>
</properties>
<modules>
@ -2003,7 +2003,7 @@
<version>${exp4j.version}</version>
</dependency>
<dependency>
<groupId>com.github.dblock</groupId>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

51
ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch

@ -0,0 +1,51 @@
diff --git a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
index e3457ea..a069460 100644
--- a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
+++ b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
@@ -259,9 +259,9 @@ export class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;
diff --git a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
index 7699ff6..01aad13 100644
--- a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
+++ b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
@@ -951,9 +951,9 @@ class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
- return;
- }
+ // if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;
diff --git a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
index 809a57d..f712b84 100644
--- a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
+++ b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
@@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent {
value = 0;
}
// Don't close the minutes view if an invalid minute is clicked.
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
- return;
- }
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
+ // return;
+ // }
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value);
}
this._timeChanged = true;

8
ui-ngx/src/app/modules/common/modules-map.ts

@ -93,6 +93,10 @@ import * as NospacePipe from '@shared/pipe/nospace.pipe';
import * as SelectableColumnsPipe from '@shared/pipe/selectable-columns.pipe';
import * as KeyboardShortcutPipe from '@shared/pipe/keyboard-shortcut.pipe';
import * as coercion from '@shared/decorators/coercion';
import * as enumerable from '@shared/decorators/enumerable';
import * as TbInject from '@shared/decorators/tb-inject';
import * as FooterComponent from '@shared/components/footer.component';
import * as LogoComponent from '@shared/components/logo.component';
import * as FooterFabButtonsComponent from '@shared/components/footer-fab-buttons.component';
@ -384,6 +388,10 @@ class ModulesMap implements IModulesMap {
'@shared/pipe/selectable-columns.pipe': SelectableColumnsPipe,
'@shared/pipe/keyboard-shortcut.pipe': KeyboardShortcutPipe,
'@shared/decorators/coercion': coercion,
'@shared/decorators/enumerable': enumerable,
'@shared/decorators/tb-inject': TbInject,
'@shared/components/footer.component': FooterComponent,
'@shared/components/logo.component': LogoComponent,
'@shared/components/footer-fab-buttons.component': FooterFabButtonsComponent,

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

@ -30,7 +30,7 @@ import {
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import {

62
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html

@ -19,6 +19,26 @@
<mat-form-field class="mat-block">
<mat-label translate>device.client-id</mat-label>
<input matInput formControlName="clientId">
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'device.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!deviceCredentialsMqttFormGroup.get('clientId').value; else copyClientId">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyClientId>
<tb-copy-button
matSuffix
miniButton="false"
[copyText]="deviceCredentialsMqttFormGroup.get('clientId').value"
tooltipText="{{ 'device.copy-client-id' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
</ng-template>
<mat-error *ngIf="deviceCredentialsMqttFormGroup.get('clientId').hasError('pattern')">
{{ 'device.client-id-pattern' | translate }}
</mat-error>
@ -26,6 +46,27 @@
<mat-form-field class="mat-block">
<mat-label translate>device.user-name</mat-label>
<input matInput formControlName="userName" [required]="!!deviceCredentialsMqttFormGroup.get('password').value">
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'device.generate-user-name' | translate }}"
matTooltipPosition="above"
(click)="generate('userName')"
*ngIf="!deviceCredentialsMqttFormGroup.get('userName').value; else copyUserName">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyUserName>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="deviceCredentialsMqttFormGroup.get('userName').value"
[copyText]="deviceCredentialsMqttFormGroup.get('userName').value"
tooltipText="{{ 'device.copy-user-name' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
</ng-template>
<mat-error *ngIf="deviceCredentialsMqttFormGroup.get('userName').hasError('required')">
{{ 'device.user-name-required' | translate }}
</mat-error>
@ -37,6 +78,27 @@
(ngModelChange)="passwordChanged()"
type="password">
<tb-toggle-password matSuffix></tb-toggle-password>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'device.generate-password' | translate }}"
matTooltipPosition="above"
(click)="generate('password')"
*ngIf="!deviceCredentialsMqttFormGroup.get('password').value; else copyPassword">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyPassword>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="deviceCredentialsMqttFormGroup.get('password').value"
[copyText]="deviceCredentialsMqttFormGroup.get('password').value"
tooltipText="{{ 'device.copy-password' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
</ng-template>
</mat-form-field>
<tb-error style="margin-top: -12px; display: block;"
[error]="deviceCredentialsMqttFormGroup.hasError('atLeastOne') ?

6
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts

@ -29,7 +29,7 @@ import {
import { Subject } from 'rxjs';
import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
import { generateSecret, isDefinedAndNotNull, isEmptyStr } from '@core/utils';
@Component({
selector: 'tb-device-credentials-mqtt-basic',
@ -128,4 +128,8 @@ export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
public generate(formControlName: string) {
this.deviceCredentialsMqttFormGroup.get(formControlName).patchValue(generateSecret(20));
}
}

29
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -29,6 +29,26 @@
<mat-form-field class="mat-block">
<mat-label translate>device.access-token</mat-label>
<input matInput formControlName="credentialsId" required>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'device.generate-access-token' | translate }}"
matTooltipPosition="above"
(click)="generate('credentialsId')"
*ngIf="!deviceCredentialsFormGroup.get('credentialsId').value; else copyAccessToken">
<mat-icon>autorenew</mat-icon>
</button>
<ng-template #copyAccessToken>
<tb-copy-button
matSuffix
miniButton="false"
[copyText]="deviceCredentialsFormGroup.get('credentialsId').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
</ng-template>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
{{ 'device.access-token-required' | translate }}
</mat-error>
@ -41,6 +61,15 @@
<mat-form-field class="mat-block">
<mat-label translate>device.certificate-pem-format</mat-label>
<textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="deviceCredentialsFormGroup.get('credentialsValue').value"
[copyText]="deviceCredentialsFormGroup.get('credentialsValue').value"
tooltipText="{{ 'device.copy-certificate' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
{{ 'device.certificate-pem-format-required' | translate }}
</mat-error>

6
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -34,7 +34,7 @@ import {
} from '@shared/models/device.models';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull } from '@core/utils';
import { generateSecret, isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-device-credentials',
@ -181,4 +181,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
break;
}
}
public generate(formControlName: string) {
this.deviceCredentialsFormGroup.get(formControlName).patchValue(generateSecret(20));
}
}

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

@ -30,7 +30,7 @@ import {
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';

2
ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts

@ -131,7 +131,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig<EdgeEvent, TimePa
);
}
},
'40px'),
'48px'),
);
if (updateTableColumns) {
this.getTable().columnsUpdated(true);

16
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -243,7 +243,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
padding: '0 12px 0 0'
})),
new EntityTableColumn<Event>('entityId', 'event.entity-id', '85px',
(entity) => `${entity.body.entityId.substring(0, 6)}…`, () => ({
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.entityId.substring(0, 6)}…</span>`,
() => ({
padding: '0 12px 0 0'
}), false, () => ({
padding: '0 12px 0 0'
@ -261,7 +262,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
type: CellActionDescriptorType.COPY_BUTTON
}),
new EntityTableColumn<Event>('msgId', 'event.message-id', '85px',
(entity) => `${entity.body.msgId.substring(0, 6)}…`, () => ({
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.msgId.substring(0, 6)}…</span>`,
() => ({
padding: '0 12px 0 0'
}), false, () => ({
padding: '0 12px 0 0'
@ -297,7 +299,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.data,
'event.data', entity.body.dataType)
},
'40px'),
'48px'),
new EntityActionTableColumn<Event>('metadata', 'event.metadata',
{
name: this.translate.instant('action.view'),
@ -306,7 +308,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.metadata,
'event.metadata', ContentType.JSON, true)
},
'40px'),
'48px'),
new EntityActionTableColumn<Event>('error', 'event.error',
{
name: this.translate.instant('action.view'),
@ -315,7 +317,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.error,
'event.error')
},
'40px')
'48px')
);
break;
case DebugEventType.DEBUG_RULE_CHAIN:
@ -329,7 +331,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.message,
'event.message')
},
'40px'),
'48px'),
new EntityActionTableColumn<Event>('error', 'event.error',
{
name: this.translate.instant('action.view'),
@ -338,7 +340,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
onAction: ($event, entity) => this.showContent($event, entity.body.error,
'event.error')
},
'40px')
'48px')
);
break;
}

2
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html

@ -55,7 +55,7 @@
<div fxFlex fxLayout="column">
<mat-checkbox #removeOtherEntitiesCheckbox
formControlName="removeOtherEntities"
(click)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup, $event)">
(change)="onRemoveOtherEntities(removeOtherEntitiesCheckbox, entityTypeFormGroup)">
{{ 'version-control.remove-other-entities' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="findExistingEntityByName">

11
ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts

@ -33,7 +33,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';
import { TbPopoverService } from '@shared/components/popover.service';
import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove-other-entities-confirm.component';
@ -210,12 +210,11 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
return res;
}
onRemoveOtherEntities(removeOtherEntitiesCheckbox: MatCheckbox, entityTypeControl: AbstractControl, $event: Event) {
onRemoveOtherEntities(removeOtherEntitiesCheckbox: MatCheckbox, entityTypeControl: AbstractControl) {
const removeOtherEntities: boolean = entityTypeControl.get('config.removeOtherEntities').value;
if (!removeOtherEntities) {
$event.preventDefault();
$event.stopPropagation();
const trigger = $('.mat-checkbox-frame', removeOtherEntitiesCheckbox._elementRef.nativeElement)[0];
if (removeOtherEntities) {
entityTypeControl.get('config').get('removeOtherEntities').patchValue(false, {emitEvent: false});
const trigger = $('.mdc-checkbox__background', removeOtherEntitiesCheckbox._elementRef.nativeElement)[0];
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts

@ -37,7 +37,7 @@ import { MatStepper } from '@angular/material/stepper';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
export interface ToggleHeaderOption {
name: string;

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -156,7 +156,7 @@
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="dataKeysOptional(datasourceControl.value)"
[optDataKeys]="dataKeysOptional(datasourceControl.get('type').value)"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"

8
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -538,17 +538,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
public dataKeysOptional(datasource?: Datasource): boolean {
public dataKeysOptional(type?: DatasourceType): boolean {
if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
return true;
} else {
return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional
&& datasource?.type !== DatasourceType.entityCount && datasource?.type !== DatasourceType.alarmCount;
&& type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount;
}
}
private buildDatasourceForm(datasource?: Datasource): UntypedFormGroup {
const dataKeysRequired = !this.dataKeysOptional(datasource);
const dataKeysRequired = !this.dataKeysOptional(datasource?.type);
const datasourceFormGroup = this.fb.group(
{
type: [datasource ? datasource.type : null, [Validators.required]],
@ -569,7 +569,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
const newDataKeysRequired = !this.dataKeysOptional(datasourceFormGroup.value);
const newDataKeysRequired = !this.dataKeysOptional(type);
datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
datasourceFormGroup.get('dataKeys').updateValueAndValidity();

3
ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html

@ -95,6 +95,7 @@
<input matInput [value]="redirectURI(domainInfo)" readonly>
<tb-copy-button
matSuffix
miniButton="false"
color="primary"
[copyText]="redirectURI(domainInfo)"
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
@ -106,6 +107,7 @@
<input matInput [value]="redirectURIMixed(domainInfo)" readonly>
<tb-copy-button
matSuffix
miniButton="false"
color="primary"
[copyText]="redirectURIMixed(domainInfo)"
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
@ -164,6 +166,7 @@
<textarea matInput formControlName="appSecret" rows="1" required></textarea>
<tb-copy-button
matSuffix
miniButton="false"
color="primary"
[copyText]="mobileInfo.get('appSecret').value"
tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}"

2
ui-ngx/src/app/modules/home/pages/admin/settings-card.scss

@ -20,7 +20,7 @@
margin: 8px;
.fields-group {
padding: 8px 16px 0;
padding: 0 16px 8px;
margin: 10px 0;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;

2
ui-ngx/src/app/modules/home/pages/notification/rule/escalations.component.ts

@ -32,7 +32,7 @@ import { AppState } from '@app/core/core.state';
import { Subject } from 'rxjs';
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-escalations-component',

7
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -134,7 +134,7 @@
<mat-form-field>
<mat-label translate>notification.time</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="datetime" openOnFocus="true" [timeInterval]="1"></mat-datetimepicker>
<mat-datetimepicker #startTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput fxFlex formControlName="time" [min]="minDate()" [max]="maxDate()" [matDatetimepicker]="startTimePicker">
</mat-form-field>
</div>
@ -198,6 +198,11 @@
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">

28
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts

@ -16,7 +16,6 @@
import {
NotificationDeliveryMethod,
NotificationDeliveryMethodTranslateMap,
NotificationRequest,
NotificationRequestPreview,
NotificationTarget,
@ -72,9 +71,6 @@ export class SentNotificationDialogComponent extends
notificationRequestForm: FormGroup;
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
selectedIndex = 0;
preview: NotificationRequestPreview = null;
@ -84,6 +80,8 @@ export class SentNotificationDialogComponent extends
private authUser: AuthUser = getCurrentAuthUser(this.store);
private allowNotificationDeliveryMethods: Array<NotificationDeliveryMethod>;
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<SentNotificationDialogComponent, NotificationRequest>,
@ -130,6 +128,7 @@ export class SentNotificationDialogComponent extends
} else {
this.notificationRequestForm.get('templateId').disable({emitEvent: false});
this.notificationRequestForm.get('template').enable({emitEvent: false});
this.updateDeliveryMethodsDisableState();
}
});
@ -337,15 +336,20 @@ export class SentNotificationDialogComponent extends
refreshAllowDeliveryMethod() {
this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => {
this.notificationDeliveryMethods.forEach(method => {
if (allowMethods.includes(method)) {
this.getDeliveryMethodsTemplatesControl(method).enable({emitEvent: true});
} else {
this.getDeliveryMethodsTemplatesControl(method).disable({emitEvent: true});
this.getDeliveryMethodsTemplatesControl(method).setValue(false, {emitEvent: true}); //used for notify again
}
});
this.allowNotificationDeliveryMethods = allowMethods;
this.updateDeliveryMethodsDisableState();
this.showRefresh = (this.notificationDeliveryMethods.length !== allowMethods.length);
});
}
private updateDeliveryMethodsDisableState() {
this.notificationDeliveryMethods.forEach(method => {
if (this.allowNotificationDeliveryMethods.includes(method)) {
this.getDeliveryMethodsTemplatesControl(method).enable({emitEvent: true});
} else {
this.getDeliveryMethodsTemplatesControl(method).disable({emitEvent: true});
this.getDeliveryMethodsTemplatesControl(method).setValue(false, {emitEvent: true}); //used for notify again
}
});
}
}

4
ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts

@ -99,7 +99,7 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
}),
actionButtonConfig: this.fb.group({
enabled: [false],
text: [{value: '', disabled: true}, Validators.required],
text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]],
linkType: [ActionButtonLinkType.LINK],
link: [{value: '', disabled: true}, Validators.required],
dashboardId: [{value: null, disabled: true}, Validators.required],
@ -190,7 +190,7 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
protected getNotificationTemplateValue(): NotificationTemplate {
const template: NotificationTemplate = deepClone(this.templateNotificationForm.value);
this.notificationDeliveryMethods.forEach(method => {
if (template.configuration.deliveryMethodsTemplates[method].enabled) {
if (template.configuration.deliveryMethodsTemplates[method]?.enabled) {
Object.assign(template.configuration.deliveryMethodsTemplates[method], this.deliveryMethodFormsMap.get(method).value, {method});
} else {
delete template.configuration.deliveryMethodsTemplates[method];

7
ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html

@ -126,6 +126,11 @@
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
@ -189,7 +194,7 @@
<mat-label class="tb-title tb-required"
[ngClass]="{'tb-error': (emailStep.interacted || emailTemplateForm.get('body').dirty) && emailTemplateForm.get('body').hasError('required')}"
translate>notification.message</mat-label>
<editor [init]="tinyMceOptions" matInput formControlName="body"></editor>
<editor [init]="tinyMceOptions" formControlName="body"></editor>
<mat-error class="tb-mat-error"
*ngIf="(emailStep.interacted || emailTemplateForm.get('body').dirty) && emailTemplateForm.get('body').hasError('required')">
{{ 'notification.message-required' | translate }}

1
ui-ngx/src/app/shared/components/button/copy-button.component.html

@ -17,6 +17,7 @@
-->
<button mat-icon-button
type="button"
[ngClass]="{'mini-button': miniButton}"
[color]="color"
[disabled]="disabled"
[matTooltip]="matTooltipText"

13
ui-ngx/src/app/shared/components/button/copy-button.component.scss

@ -13,20 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../theme";
:host {
.mat-mdc-icon-button{
.mini-button {
height: 32px;
width: 32px;
line-height: 32px;
padding: 4px;
vertical-align: middle;
.mat-icon.copied{
color: #00C851 !important;
}
}
.mat-icon.copied{
color: #00C851 !important;
}
&:hover{
.mat-icon{
color: #28567E !important;
color: $tb-primary-color !important;
}
}
}

6
ui-ngx/src/app/shared/components/button/copy-button.component.ts

@ -19,6 +19,7 @@ import { ClipboardService } from 'ngx-clipboard';
import { TooltipPosition } from '@angular/material/tooltip';
import { TranslateService } from '@ngx-translate/core';
import { ThemePalette } from '@angular/material/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-copy-button',
@ -35,6 +36,7 @@ export class CopyButtonComponent {
copyText: string;
@Input()
@coerceBoolean()
disabled = false;
@Input()
@ -55,6 +57,10 @@ export class CopyButtonComponent {
@Input()
color: ThemePalette;
@Input()
@coerceBoolean()
miniButton = true;
@Output()
successCopied = new EventEmitter<string>();

2
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -39,7 +39,7 @@ import { EntityService } from '@core/http/entity.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { isEqual } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-entity-autocomplete',

2
ui-ngx/src/app/shared/components/entity/entity-type-list.component.ts

@ -27,7 +27,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatChipGrid } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
interface EntityTypeInfo {
name: string;

2
ui-ngx/src/app/shared/components/markdown.component.ts

@ -39,7 +39,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { deepClone, guid, isDefinedAndNotNull } from '@core/utils';
import { Observable, of, ReplaySubject } from 'rxjs';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
let defaultMarkdownStyle;

2
ui-ngx/src/app/shared/components/notification/notification.component.scss

@ -44,6 +44,8 @@
.button {
margin-top: 12px;
border-color: initial;
height: fit-content;
line-height: 2;
}
}

2
ui-ngx/src/app/shared/components/notification/notification.component.ts

@ -29,7 +29,7 @@ import { alarmSeverityTranslations } from '@shared/models/alarm.models';
import tinycolor from 'tinycolor2';
import { StateObject } from '@core/api/widget-api.models';
import { objToBase64URI } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-notification',

2
ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts

@ -40,7 +40,7 @@ import {
} from '@home/pages/notification/template/template-notification-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-template-autocomplete',

2
ui-ngx/src/app/shared/components/popover.component.ts

@ -58,7 +58,7 @@ import {
import { distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
import { animate, AnimationBuilder, AnimationMetadata, style } from '@angular/animations';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;

2
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html

@ -34,7 +34,7 @@
#queueAutocomplete="matAutocomplete"
[displayWith]="displayQueueFn"
>
<mat-option *ngFor="let queue of filteredQueues | async" [value]="queue" class="template-option">
<mat-option *ngFor="let queue of filteredQueues | async" [value]="queue" class="queue-option">
<span [innerHTML]="queue.name | highlight:searchText"></span>
<small class="queue-option-description">{{getDescription(queue)}}</small>
</mat-option>

20
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts

@ -15,7 +15,7 @@
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -45,7 +45,7 @@ import { SubscriptSizing } from '@angular/material/form-field';
})
export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit {
selectQueueFormGroup: UntypedFormGroup;
selectQueueFormGroup: FormGroup;
modelValue: string | null;
@ -91,7 +91,7 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit
public truncate: TruncatePipe,
private entityService: EntityService,
private queueService: QueueService,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
this.selectQueueFormGroup = this.fb.group({
queueName: [null]
});
@ -127,8 +127,6 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit
);
}
ngAfterViewInit(): void {}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -145,19 +143,19 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit
writeValue(value: string | null): void {
this.searchText = '';
if (value != null) {
this.queueService.getQueueByName(value, {ignoreLoading: true, ignoreErrors: true}).subscribe(
(entity) => {
this.queueService.getQueueByName(value, {ignoreLoading: true, ignoreErrors: true}).subscribe({
next: (entity) => {
this.modelValue = entity.name;
this.selectQueueFormGroup.get('queueName').patchValue(entity, {emitEvent: false});
},
() => {
error: () => {
this.modelValue = null;
this.selectQueueFormGroup.get('queueName').patchValue('', {emitEvent: false});
if (value !== null) {
this.propagateChange(this.modelValue);
}
}
);
});
} else {
this.modelValue = null;
this.selectQueueFormGroup.get('queueName').patchValue('', {emitEvent: false});
@ -195,9 +193,7 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit
});
return this.queueService.getTenantQueuesByServiceType(pageLink, this.queueType, {ignoreLoading: true}).pipe(
catchError(() => of(emptyPageData<QueueInfo>())),
map(pageData => {
return pageData.data;
})
map(pageData => pageData.data)
);
}

2
ui-ngx/src/app/shared/components/string-items-list.component.ts

@ -19,7 +19,7 @@ import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Valida
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-string-items-list',

2
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -19,7 +19,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimeInterval, TimeService } from '@core/services/time.service';
import { coerceNumberProperty } from '@angular/cdk/coercion';
import { SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-timeinterval',

2
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -53,7 +53,7 @@ import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { coerceBoolean } from '@shared/decorators/coercion';
// @dynamic
@Component({

108
ui-ngx/src/app/shared/decorators/coercion.ts

@ -0,0 +1,108 @@
///
/// 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 {
coerceArray as coerceArrayAngular,
coerceBooleanProperty,
coerceCssPixelValue as coerceCssPixelValueAngular,
coerceNumberProperty,
coerceStringArray as coerceStringArrayAngular
} from '@angular/cdk/coercion';
export const coerceBoolean = () => (target: any, key: string): void => {
const getter = function() {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceBooleanProperty(next);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
export const coerceNumber = () => (target: any, key: string): void => {
const getter = function(): number {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceNumberProperty(next);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
export const coerceCssPixelValue = () => (target: any, key: string): void => {
const getter = function(): string {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceCssPixelValueAngular(next);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
export const coerceArray = () => (target: any, key: string): void => {
const getter = function(): any[] {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceArrayAngular(next);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
export const coerceStringArray = (separator?: string | RegExp) => (target: any, key: string): void => {
const getter = function(): string[] {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceStringArrayAngular(next, separator);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};

21
ui-ngx/src/app/shared/decorators/coerce-boolean.ts → ui-ngx/src/app/shared/decorators/public-api.ts

@ -14,21 +14,6 @@
/// limitations under the License.
///
import { coerceBooleanProperty } from '@angular/cdk/coercion';
export const coerceBoolean = () => (target: any, key: string): void => {
const getter = function() {
return this['__' + key];
};
const setter = function(next: any) {
this['__' + key] = coerceBooleanProperty(next);
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
export * from './coercion';
export * from './enumerable';
export * from './tb-inject';

1
ui-ngx/src/app/shared/public-api.ts

@ -15,6 +15,7 @@
///
export * from './components/public-api';
export * from './decorators/public-api';
export * from './models/public-api';
export * from './pipe/public-api';
export * from './shared.module';

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

@ -1233,6 +1233,15 @@
"access-token-invalid": "Access token length must be from 1 to 32 characters.",
"certificate-pem-format": "Certificate in PEM format",
"certificate-pem-format-required": "Certificate is required.",
"copy-access-token": "Copy Access token",
"copy-certificate": "Copy Certificate",
"copy-client-id": "Copy Client ID",
"copy-user-name": "Copy User name",
"copy-password": "Copy Password",
"generate-client-id": "Generate Client ID",
"generate-user-name": "Generate User name",
"generate-password": "Generate Password",
"generate-access-token": "Generate Access Token",
"lwm2m-security-config": {
"identity": "Client Identity",
"identity-required": "Client Identity is required.",
@ -2799,6 +2808,7 @@
"basic-settings": "Basic settings",
"button-text": "Button text",
"button-text-required": "Button text is required",
"button-text-max-length": "Button text should be less than or equal to {{ length }} characters",
"compose": "Compose",
"conversation": "Conversation",
"conversation-required": "Conversation is required",

Loading…
Cancel
Save