Browse Source

Merge remote-tracking branch 'origin/master' into fix/prod-1975/edit-json-validation

pull/8605/head
deaflynx 3 years ago
parent
commit
9ef743b85b
  1. 276
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java
  4. 7
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  5. 267
      dao/src/main/resources/sql/schema-entities.sql
  6. 282
      dao/src/main/resources/sql/schema-views-and-functions.sql
  7. 1
      dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
  8. 1
      dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java
  9. 10
      dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java
  10. 19
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  11. 4
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java
  12. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java
  13. 39
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmWidgetElements.java
  14. 72
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupElements.java
  15. 48
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupHelper.java
  16. 35
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java
  17. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java
  18. 6
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java
  19. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java
  20. 146
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java
  21. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java
  22. 51
      ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch
  23. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  24. 8
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  25. 7
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  26. 2
      ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts
  27. 7
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html
  28. 2
      ui-ngx/src/app/shared/components/notification/notification.component.scss
  29. 1
      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. -- 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 -- USER CREDENTIALS START
ALTER TABLE user_credentials 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 fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE,
CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) 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 -- TTL DROP PARTITIONS FUNCTIONS UPDATE START

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

@ -249,6 +249,7 @@ public class ThingsboardInstallService {
case "3.4.4": case "3.4.4":
log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ..."); log.info("Upgrading ThingsBoard from version 3.4.4 to 3.5.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4"); databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
@ -275,6 +276,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseSchema(); entityDatabaseSchemaService.createDatabaseSchema();
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Installing DataBase schema for timeseries..."); log.info("Installing DataBase schema for timeseries...");

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 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_SQL = "schema-entities.sql";
public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.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_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() { public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL); 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("DROP VIEW IF EXISTS device_info_view CASCADE;");
executeQuery("CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM " + sourceViewName + ";"); 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);
}
} }

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 fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE,
CONSTRAINT user_settings_pkey PRIMARY KEY (user_id, type) 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.sql",
"sql/schema-entities-idx.sql", "sql/schema-entities-idx.sql",
"sql/schema-entities-idx-psql-addon.sql", "sql/schema-entities-idx-psql-addon.sql",
"sql/schema-views-and-functions.sql",
"sql/system-data.sql", "sql/system-data.sql",
"sql/system-test-psql.sql"); "sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.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.sql",
"sql/schema-entities-idx.sql", "sql/schema-entities-idx.sql",
"sql/schema-entities-idx-psql-addon.sql", "sql/schema-entities-idx-psql-addon.sql",
"sql/schema-views-and-functions.sql",
"sql/system-data.sql", "sql/system-data.sql",
"sql/system-test-psql.sql"); "sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/timescale/drop-all-tables.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 { public class EntitiesSchemaSqlTest extends AbstractServiceTest {
@Value("${classpath:sql/schema-entities.sql}") @Value("${classpath:sql/schema-entities.sql}")
private Path installScriptPath; private Path installEntitiesPath;
@Value("${classpath:sql/schema-views-and-functions.sql}")
private Path installViewsPath;
@Autowired @Autowired
private JdbcTemplate jdbcTemplate; private JdbcTemplate jdbcTemplate;
@Test @Test
public void testRepeatedInstall() throws IOException { public void testRepeatedInstall() throws IOException {
String installScript = Files.readString(installScriptPath); String entitiesScript = Files.readString(installEntitiesPath);
String viewsScript = Files.readString(installViewsPath);
try { try {
for (int i = 1; i <= 2; i++) { for (int i = 1; i <= 2; i++) {
jdbcTemplate.execute(installScript); jdbcTemplate.execute(entitiesScript);
jdbcTemplate.execute(viewsScript);
} }
} catch (Exception e) { } catch (Exception e) {
Assertions.fail("Failed to execute reinstall", 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.response.ValidatableResponse;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import org.thingsboard.server.common.data.Customer; 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.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView; 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.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId; 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.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -489,4 +491,21 @@ public class TestRestClient {
.then() .then()
.statusCode(HTTP_OK); .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 = "~`!@#$^&*()_+=-"; String s = "~`!@#$^&*()_+=-";
return s.charAt(rand.nextInt(s.length())); 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.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; 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.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -262,4 +263,10 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest {
testRestClient.deleteEntityView(entityViewId); 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_LIST_FIELD = "//input[@formcontrolname='entity']";
private static final String MANAGE_ASSIGNED_ENTITY = "//mat-option//span[contains(text(),'%s')]"; 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 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() { public List<WebElement> entityTitles() {
return waitUntilVisibilityOfElementsLocated(TITLES); return waitUntilVisibilityOfElementsLocated(TITLES);
@ -50,4 +57,32 @@ public class DashboardPageElements extends OtherPageElementsHelper {
public WebElement manageAssignedUpdateBtn() { public WebElement manageAssignedUpdateBtn() {
return waitUntilElementToBeClickable(MANAGE_ASSIGNED_UPDATE_BTN); 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(manageAssignedEntity(title));
jsClick(manageAssignedUpdateBtn()); 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 RULE_CHAINS_BTN = "//mat-toolbar//a[@href='/ruleChains']";
private static final String CUSTOMER_BTN = "//mat-toolbar//a[@href='/customers']"; 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 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 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 ASSET_PROFILE_BTN = "//mat-toolbar//a[@href='/profiles/assetProfiles']";
private static final String ALARMS_BTN = "//mat-toolbar//a[@href='/alarms']"; private static final String ALARMS_BTN = "//mat-toolbar//a[@href='/alarms']";
@ -52,8 +52,8 @@ public class SideBarMenuViewElements extends AbstractBasePage {
return waitUntilElementToBeClickable(DASHBOARD_BTN); return waitUntilElementToBeClickable(DASHBOARD_BTN);
} }
public WebElement profilesBtn() { public WebElement profilesDropdown() {
return waitUntilElementToBeClickable(PROFILES_BTN); return waitUntilElementToBeClickable(PROFILES_DROPDOWN);
} }
public WebElement deviceProfileBtn() { 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() { public void openDeviceProfiles() {
profilesBtn().click(); openProfilesDropDown();
deviceProfileBtn().click(); deviceProfileBtn().click();
} }
public void openAssetProfiles() { public void openAssetProfiles() {
profilesBtn().click(); openProfilesDropDown();
assetProfileBtn().click(); assetProfileBtn().click();
} }
@ -54,10 +54,20 @@ public class SideBarMenuViewHelper extends SideBarMenuViewElements {
} }
} }
public void openProfilesDropDown() {
if (profilesIsClose()) {
profilesDropdown().click();
}
}
public boolean entitiesDropdownIsClose() { public boolean entitiesDropdownIsClose() {
return dropdownIsClose(entitiesDropdown()); return dropdownIsClose(entitiesDropdown());
} }
public boolean profilesIsClose() {
return dropdownIsClose(profilesDropdown());
}
private boolean dropdownIsClose(WebElement dropdown) { private boolean dropdownIsClose(WebElement dropdown) {
return !dropdown.getAttribute("class").contains("tb-toggled"); 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.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; 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.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -206,4 +207,10 @@ public class EntityPrototypes {
entityView.setAdditionalInfo(JacksonUtil.newObjectNode().put("entityType", entityType)); entityView.setAdditionalInfo(JacksonUtil.newObjectNode().put("entityType", entityType));
return entityView; return entityView;
} }
public static Dashboard defaultDashboardPrototype(String title) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle(title + RandomStringUtils.randomAlphanumeric(7));
return dashboard;
}
} }

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;

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

@ -156,7 +156,7 @@
[widgetType]="widgetType" [widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value" [datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys" [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="dataKeysOptional(datasourceControl.value)" [optDataKeys]="dataKeysOptional(datasourceControl.get('type').value)"
[aliasController]="aliasController" [aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" [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) { if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
return true; return true;
} else { } else {
return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional 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 { private buildDatasourceForm(datasource?: Datasource): UntypedFormGroup {
const dataKeysRequired = !this.dataKeysOptional(datasource); const dataKeysRequired = !this.dataKeysOptional(datasource?.type);
const datasourceFormGroup = this.fb.group( const datasourceFormGroup = this.fb.group(
{ {
type: [datasource ? datasource.type : null, [Validators.required]], type: [datasource ? datasource.type : null, [Validators.required]],
@ -569,7 +569,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
datasourceFormGroup.get('entityAliasId').setValidators( datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] (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('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
datasourceFormGroup.get('dataKeys').updateValueAndValidity(); datasourceFormGroup.get('dataKeys').updateValueAndValidity();

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

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

2
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({ actionButtonConfig: this.fb.group({
enabled: [false], enabled: [false],
text: [{value: '', disabled: true}, Validators.required], text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]],
linkType: [ActionButtonLinkType.LINK], linkType: [ActionButtonLinkType.LINK],
link: [{value: '', disabled: true}, Validators.required], link: [{value: '', disabled: true}, Validators.required],
dashboardId: [{value: null, disabled: true}, Validators.required], dashboardId: [{value: null, disabled: true}, Validators.required],

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

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

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

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

@ -2808,6 +2808,7 @@
"basic-settings": "Basic settings", "basic-settings": "Basic settings",
"button-text": "Button text", "button-text": "Button text",
"button-text-required": "Button text is required", "button-text-required": "Button text is required",
"button-text-max-length": "Button text should be less than or equal to {{ length }} characters",
"compose": "Compose", "compose": "Compose",
"conversation": "Conversation", "conversation": "Conversation",
"conversation-required": "Conversation is required", "conversation-required": "Conversation is required",

Loading…
Cancel
Save