diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 4b452b5de0..aa53f98b74 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/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 diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index e036e267a4..ea13de3779 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/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..."); diff --git a/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java index c05dd243ba..35370d0854 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java +++ b/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; + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index 10ce5dd0ee..b9a62e3dfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/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); + } } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 52d1d23c6b..9e219ed933 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/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 -$$; diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql new file mode 100644 index 0000000000..2de757be54 --- /dev/null +++ b/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 +$$; diff --git a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java index 23a4ebb9a6..88cadbafa1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java +++ b/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"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java index b4a347560b..7ed0c811b2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java +++ b/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"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java index cd30091b6e..07d046639a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java +++ b/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); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index d7e5c807d5..76ce467fd9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/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); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java index 35639bc19f..cfb282b88c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java +++ b/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(); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index 27e5e6b978..cd34e69239 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/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); + } + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmWidgetElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/AlarmWidgetElements.java new file mode 100644 index 0000000000..fd7df70675 --- /dev/null +++ b/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)); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupElements.java new file mode 100644 index 0000000000..9565ef3ebc --- /dev/null +++ b/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)); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CreateWidgetPopupHelper.java new file mode 100644 index 0000000000..50a80c156a --- /dev/null +++ b/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); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java index 77fb2b6b13..8994b2e748 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageElements.java +++ b/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 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); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java index 6449cf0cbb..93e8fa10d4 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DashboardPageHelper.java +++ b/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); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java index 54675bdc7a..fe03df48b3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewElements.java +++ b/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() { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java index 297438c193..d472ea783d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/SideBarMenuViewHelper.java +++ b/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"); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assignee/AssignFromAlarmWidgetTest.java new file mode 100644 index 0000000000..5a2d6cd0c5 --- /dev/null +++ b/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); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java index 99f05e6f1d..6f2d9560f4 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java +++ b/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; + } } diff --git a/ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch b/ui-ngx/patches/@mat-datetimepicker+core+11.0.3.patch new file mode 100644 index 0000000000..990754ef1a --- /dev/null +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index df7dd942a7..7d39455f36 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/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" diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 5cbcf94826..1514c1b929 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 1f369deebf..27b8968344 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -134,7 +134,7 @@ notification.time - + @@ -198,6 +198,11 @@ {{ 'notification.button-text-required' | translate }} + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index e894c62631..b3fa00ef72 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -99,7 +99,7 @@ export abstract class TemplateConfiguration 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], diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index b62bdba015..067258fa24 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -126,6 +126,11 @@ {{ 'notification.button-text-required' | translate }} + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} +
@@ -189,7 +194,7 @@ notification.message - + {{ 'notification.message-required' | translate }} diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.scss b/ui-ngx/src/app/shared/components/notification/notification.component.scss index c2d50ed106..6b196ee9ea 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.scss +++ b/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; } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d41ada03f8..bcaa4a183f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2808,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",