diff --git a/application/src/main/data/json/demo/dashboards/gateways.json b/application/src/main/data/json/demo/dashboards/gateways.json index 0a18b10a52..6051fbd514 100644 --- a/application/src/main/data/json/demo/dashboards/gateways.json +++ b/application/src/main/data/json/demo/dashboards/gateways.json @@ -131,7 +131,7 @@ "name": "Add device", "icon": "add", "type": "customPretty", - "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", + "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customResources": [], @@ -160,7 +160,7 @@ "name": "Edit device", "icon": "edit", "type": "customPretty", - "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", + "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customResources": [], diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 4f486effdf..c014d8875a 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -215,6 +215,8 @@ "displayDetails": true, "allowAcknowledgment": true, "allowClear": true, + "allowAssign": true, + "displayComments": true, "displayPagination": true, "defaultPageSize": 10, "defaultSortOrder": "-createdTime", @@ -277,6 +279,14 @@ "color": "#607d8b", "settings": {}, "_hash": 0.7977920750136249 + }, + { + "name": "assignee", + "type": "alarm", + "label": "Assignee", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8678751039018493 } ] }, diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 448fcaf52c..e7a97b36f8 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -23,7 +23,7 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayComments\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" } } ] 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 d724f21a45..e2c2b10c96 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,6 +14,70 @@ -- limitations under the License. -- +-- USER CREDENTIALS START + +ALTER TABLE user_credentials + ADD COLUMN IF NOT EXISTS additional_info varchar NOT NULL DEFAULT '{}'; + +UPDATE user_credentials + SET additional_info = json_build_object('userPasswordHistory', (u.additional_info::json -> 'userPasswordHistory')) + FROM tb_user u WHERE user_credentials.user_id = u.id AND u.additional_info::jsonb ? 'userPasswordHistory'; + +UPDATE tb_user SET additional_info = tb_user.additional_info::jsonb - 'userPasswordHistory' WHERE additional_info::jsonb ? 'userPasswordHistory'; + +-- USER CREDENTIALS END + +-- ALARM ASSIGN TO USER START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assign_ts BIGINT; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assignee_id UUID; + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); + +-- ALARM ASSIGN TO USER END + +-- ALARM STATUS REFACTORING START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS acknowledged boolean; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS cleared boolean; + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS status varchar; -- to avoid failure of the subsequent upgrade. +UPDATE alarm SET acknowledged = true, cleared = true WHERE status = 'CLEARED_ACK'; +UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK'; +UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; +UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; + +-- Drop index by 'status' column and replace with new one that has only active alarms; +DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type) WHERE cleared = false; + +-- Cover index by alarm type to optimize propagated alarm queries; +DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; +CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm +USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id); + +DROP INDEX IF EXISTS idx_alarm_tenant_status_created_time; +ALTER TABLE alarm DROP COLUMN IF EXISTS status; + +-- Update old alarms and set their state to clear, if there are newer alarms. +UPDATE alarm a +SET cleared = TRUE +WHERE cleared = FALSE + AND id != (SELECT l.id + FROM alarm l + WHERE l.tenant_id = a.tenant_id + AND l.originator_id = a.originator_id + AND l.type = a.type + ORDER BY l.created_time DESC, l.id + LIMIT 1); + +VACUUM FULL ANALYZE alarm; + +-- ALARM STATUS REFACTORING END + +-- ALARM COMMENTS START + CREATE TABLE IF NOT EXISTS alarm_comment ( id uuid NOT NULL, created_time bigint NOT NULL, @@ -101,3 +165,256 @@ CREATE TABLE IF NOT EXISTS user_settings ( settings varchar(100000), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); + +-- ALARM COMMENTS END + +-- 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(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(label, name) from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select COALESCE(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(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))::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 diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index d810fdede7..78ee6620fe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; @@ -41,16 +42,23 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGNEE_ID; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGN_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; @@ -79,6 +87,7 @@ public class AlarmController extends BaseController { private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; + private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; @@ -93,12 +102,8 @@ public class AlarmController extends BaseController { public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmId(alarmId, Operation.READ); } @ApiOperation(value = "Get Alarm Info (getAlarmInfoById)", @@ -110,15 +115,11 @@ public class AlarmController extends BaseController { public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmInfoId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmInfoId(alarmId, Operation.READ); } - @ApiOperation(value = "Create or update Alarm (saveAlarm)", + @ApiOperation(value = "Create or Update Alarm (saveAlarm)", notes = "Creates or Updates the Alarm. " + "When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " + @@ -135,7 +136,9 @@ public class AlarmController extends BaseController { @ResponseBody public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { alarm.setTenantId(getTenantId()); + checkNotNull(alarm.getOriginator()); checkEntity(alarm.getId(), alarm, Resource.ALARM); + checkEntityId(alarm.getOriginator(), Operation.READ); return tbAlarmService.save(alarm, getCurrentUser()); } @@ -158,11 +161,12 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.ack(alarm, getCurrentUser()).get(); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.ack(alarm, getCurrentUser()); } @ApiOperation(value = "Clear Alarm (clearAlarm)", @@ -172,11 +176,50 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.clear(alarm, getCurrentUser()); + } + + @ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)", + notes = "Assign the Alarm. " + + "Once assigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_ASSIGNED' " + + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public Alarm assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = ASSIGN_ID_PARAM_DESCRIPTION) + @PathVariable(ASSIGNEE_ID) String strAssigneeId + ) throws Exception { checkParameter(ALARM_ID, strAlarmId); + checkParameter(ASSIGNEE_ID, strAssigneeId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.clear(alarm, getCurrentUser()).get(); + UserId assigneeId = new UserId(UUID.fromString(strAssigneeId)); + checkUserId(assigneeId, Operation.READ); + return tbAlarmService.assign(alarm, assigneeId, System.currentTimeMillis(), getCurrentUser()); + } + + @ApiOperation(value = "Unassign Alarm (unassignAlarm)", + notes = "Unassign the Alarm. " + + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public Alarm unassignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); } @ApiOperation(value = "Get Alarms (getAlarms)", @@ -194,6 +237,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -221,10 +266,14 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } catch (Exception e) { throw handleException(e); } @@ -244,6 +293,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -267,13 +318,17 @@ public class AlarmController extends BaseController { throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { if (getCurrentUser().isCustomerUser()) { - return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } else { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } } catch (Exception e) { throw handleException(e); @@ -295,7 +350,9 @@ public class AlarmController extends BaseController { @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) - @RequestParam(required = false) String status + @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -308,7 +365,7 @@ public class AlarmController extends BaseController { } checkEntityId(entityId, Operation.READ); try { - return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus); + return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus, assigneeId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 624a4ce3ef..1cdc1062d0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -655,9 +655,7 @@ public abstract class BaseController { } AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException { - return checkEntityId(alarmId, (tenantId, id) -> { - return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId).get(); - }, operation); + return checkEntityId(alarmId, alarmService::findAlarmInfoById, operation); } AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId, AlarmId alarmId) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 768d98e403..53734b8633 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -27,6 +27,7 @@ public class ControllerConstants { protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; + protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "See the 'Model' tab of the Response Class for more details. "; @@ -44,6 +45,7 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ASSIGN_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 716b7ef60f..1d429e9157 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; +import org.thingsboard.server.service.security.permission.Operation; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; @@ -61,11 +63,7 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @@ -76,11 +74,7 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @@ -91,11 +85,12 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.") @RequestBody AlarmDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); + checkNotNull(query.getPageLink()); + UserId assigneeId = query.getPageLink().getAssigneeId(); + if (assigneeId != null) { + checkUserId(assigneeId, Operation.READ); } + return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Keys by Query", @@ -112,15 +107,11 @@ public class EntityQueryController extends BaseController { @RequestParam("attributes") boolean isAttributes) throws ThingsboardException { TenantId tenantId = getTenantId(); checkNotNull(query); - try { - EntityDataPageLink pageLink = query.getPageLink(); - if (pageLink.getPageSize() > MAX_PAGE_SIZE) { - pageLink.setPageSize(MAX_PAGE_SIZE); - } - return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); - } catch (Exception e) { - throw handleException(e); + EntityDataPageLink pageLink = query.getPageLink(); + if (pageLink.getPageSize() > MAX_PAGE_SIZE) { + pageLink.setPageSize(MAX_PAGE_SIZE); } + return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); } } diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index fd1d5530d6..00bbf3b9e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -87,6 +87,12 @@ public class EntityActionService { case ALARM_CLEAR: msgType = DataConstants.ALARM_CLEAR; break; + case ALARM_ASSIGN: + msgType = DataConstants.ALARM_ASSIGN; + break; + case ALARM_UNASSIGN: + msgType = DataConstants.ALARM_UNASSIGN; + break; case ALARM_DELETE: msgType = DataConstants.ALARM_DELETE; break; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index 551412a756..d85750f7d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -48,7 +48,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } try { - Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); + Alarm existentAlarm = alarmService.findLatestActiveByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()); switch (alarmUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: @@ -62,7 +62,9 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); } - existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); + var alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus()); + existentAlarm.setCleared(alarmStatus.isCleared()); + existentAlarm.setAcknowledged(alarmStatus.isAck()); existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); existentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); @@ -70,18 +72,18 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { break; case ALARM_ACK_RPC_MESSAGE: if (existentAlarm != null) { - alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); + alarmService.acknowledgeAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); } break; case ALARM_CLEAR_RPC_MESSAGE: if (existentAlarm != null) { alarmService.clearAlarm(tenantId, existentAlarm.getId(), - JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); + alarmUpdateMsg.getAckTs(), JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); } break; case ENTITY_DELETED_RPC_MESSAGE: if (existentAlarm != null) { - alarmService.deleteAlarm(tenantId, existentAlarm.getId()); + alarmService.delAlarm(tenantId, existentAlarm.getId()); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 334a9aed0b..11ff6a3321 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -74,7 +74,7 @@ public abstract class AbstractTbEntityService { protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); + alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index a6620152ac..03a0671162 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -308,6 +308,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ALARM_ACK; case ALARM_CLEAR: return EdgeEventActionType.ALARM_CLEAR; + case ALARM_ASSIGN: + return EdgeEventActionType.ALARM_ASSIGN; + case ALARM_UNASSIGN: + return EdgeEventActionType.ALARM_UNASSIGN; case DELETED: return EdgeEventActionType.DELETED; case RELATION_ADD_OR_UPDATE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java index bae7ed96e0..26c38188fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -38,8 +38,9 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem @Override public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException { ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED_COMMENT : ActionType.UPDATED_COMMENT; - UserId userId = user.getId(); - alarmComment.setUserId(userId); + if (user != null) { + alarmComment.setUserId(user.getId()); + } try { AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment)); notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 8af0001e0b..154acf1129 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -26,13 +23,19 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import java.util.List; @@ -50,9 +53,34 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = alarm.getTenantId(); try { - Alarm savedAlarm = checkNotNull(alarmSubscriptionService.createOrUpdateAlarm(alarm)); - notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); - return savedAlarm; + AlarmApiCallResult result; + if (alarm.getId() == null) { + result = alarmSubscriptionService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, user.getId())); + } else { + result = alarmSubscriptionService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm, user.getId())); + } + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED; + if (result.isModified()) { + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), actionType, user); + } + AlarmInfo resultAlarm = result.getAlarm(); + if (alarm.isAcknowledged() && !resultAlarm.isAcknowledged()) { + resultAlarm = ack(resultAlarm, alarm.getAckTs(), user); + } + if (alarm.isCleared() && !resultAlarm.isCleared()) { + resultAlarm = clear(resultAlarm, alarm.getClearTs(), user); + } + UserId newAssignee = alarm.getAssigneeId(); + UserId curAssignee = resultAlarm.getAssigneeId(); + if (newAssignee != null && !newAssignee.equals(curAssignee)) { + resultAlarm = assign(alarm, newAssignee, alarm.getAssignTs(), user); + } else if (newAssignee == null && curAssignee != null) { + resultAlarm = unassign(alarm, alarm.getAssignTs(), user); + } + return new Alarm(resultAlarm); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); throw e; @@ -60,51 +88,126 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture ack(Alarm alarm, User user) { - long ackTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); - return Futures.transform(future, result -> { + public AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException { + return ack(alarm, System.currentTimeMillis(), user); + } + + @Override + public AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(ackTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isModified()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ACK")) .build(); try { alarmCommentService.saveAlarmComment(alarm, alarmComment, user); } catch (ThingsboardException e) { log.error("Failed to save alarm comment", e); } - alarm.setAckTs(ackTs); - alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user); + } else { + throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); } @Override - public ListenableFuture clear(Alarm alarm, User user) { - long clearTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); - return Futures.transform(future, result -> { + public AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException { + return clear(alarm, System.currentTimeMillis(), user); + } + + @Override + public AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(clearTs), null); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isCleared()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + .put("userId", user.getId().toString()) + .put("subtype", "CLEAR")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user); + } else { + throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); + } + + @Override + public AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, getOrDefault(assignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmAssignee assignee = alarmInfo.getAssignee(); + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), + (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) + .put("userId", user.getId().toString()) + .put("assigneeId", assignee.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment, user); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; + } + + @Override + public AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(unassignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ASSIGN")) .build(); try { alarmCommentService.saveAlarmComment(alarm, alarmComment, user); } catch (ThingsboardException e) { log.error("Failed to save alarm comment", e); } - alarm.setClearTs(clearTs); - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; } @Override @@ -115,4 +218,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb relatedEdgeIds, user, JacksonUtil.toString(alarm)); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); } -} \ No newline at end of file + + private static long getOrDefault(long ts) { + return ts > 0 ? ts : System.currentTimeMillis(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index f6746e20cc..a2ae9c8cc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -15,18 +15,27 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; - ListenableFuture ack(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException; - ListenableFuture clear(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException; + + AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException; + + AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 13f8672d67..43d6a4df0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -556,7 +556,7 @@ public class DefaultDataUpdateService implements DataUpdateService { }; private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { - AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, null, false); PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); boolean hasNext = true; while (hasNext) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 3d5438f1e5..baded64603 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -27,7 +27,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; @@ -538,13 +538,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 54bc539559..48aee9cb50 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; @@ -258,7 +260,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene updateDeviceInactivityTimeout(tenantId, entityId, attributes); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) + new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) , null); } } @@ -282,7 +284,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } @Override - public void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -291,16 +293,15 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene return null; } }, - s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - false + s -> alarm.getCreatedTime() >= s.getTs() || alarm.getAssignTs() >= s.getTs(), + alarm, false ); notificationRuleProcessingService.process(tenantId, alarm, false); callback.onSuccess(); } @Override - public void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -310,8 +311,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } }, s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - true + alarm, true ); notificationRuleProcessingService.process(tenantId, alarm, true); callback.onSuccess(); @@ -451,19 +451,20 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene private void onLocalAlarmSubUpdate(EntityId entityId, Function castFunction, Predicate filterFunction, - Function processFunction, boolean deleted) { + AlarmInfo alarm, boolean deleted) { Set entitySubscriptions = subscriptionsByEntityId.get(entityId); + if (alarm == null) { + log.warn("[{}] empty alarm update!", entityId); + return; + } if (entitySubscriptions != null) { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { - Alarm alarm = processFunction.apply(s); - if (alarm != null) { - if (serviceId.equals(s.getServiceId())) { - AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); - localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); - } else { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); - toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); - } + if (serviceId.equals(s.getServiceId())) { + AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); + localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); + } else { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); } }); } else { @@ -594,12 +595,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene }); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } - private TbProtoQueueMsg toProto(TbSubscription subscription, Alarm alarm, boolean deleted) { + private TbProtoQueueMsg toProto(TbSubscription subscription, AlarmInfo alarm, boolean deleted) { TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder(); builder.setSessionId(subscription.getSessionId()); @@ -608,8 +609,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene builder.setDeleted(deleted); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder() - .setAlarmSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder() + .setAlarmSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index bbdd1a480a..1db4348828 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -17,12 +17,14 @@ package org.thingsboard.server.service.subscription; import org.springframework.context.ApplicationListener; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; @@ -45,9 +47,9 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback callback); - void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); - void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 790b76fcaf..ccba16fbd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -20,7 +20,9 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -63,10 +65,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { private final AlarmService alarmService; @Getter - @Setter private final LinkedHashMap entitiesMap; @Getter - @Setter private final HashMap alarmsMap; private final int maxEntitiesPerAlarmSubscription; @@ -225,8 +225,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { boolean matchesFilter = filter(alarm); if (onCurrentPage) { if (matchesFilter) { - AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); - updated.getLatest().putAll(current.getLatest()); + AlarmData updated = new AlarmData(subscriptionUpdate.getAlarm(), current); alarmsMap.put(alarmId, updated); sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); } else { @@ -270,8 +269,24 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) { boolean matches = false; for (AlarmSearchStatus status : filter.getStatusList()) { - if (status.getStatuses().contains(alarm.getStatus())) { - matches = true; + switch (status) { + case ANY: + matches = true; + break; + case ACK: + matches = alarm.isAcknowledged(); + break; + case UNACK: + matches = !alarm.isAcknowledged(); + break; + case CLEARED: + matches = alarm.isCleared(); + break; + case ACTIVE: + matches = !alarm.isCleared(); + break; + } + if (matches) { break; } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index efbe57d56a..914c8a7c55 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -17,6 +17,8 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -229,8 +231,8 @@ public class TbSubscriptionUtils { if (proto.getErrorCode() > 0) { return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); } else { - Alarm alarm = JacksonUtil.fromString(proto.getAlarm(), Alarm.class); - return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm); + AlarmInfo alarm = JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class); + return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm, proto.getDeleted()); } } @@ -356,7 +358,7 @@ public class TbSubscriptionUtils { return entry; } - public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -369,7 +371,7 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 52b4adf854..f412e76ad9 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -22,40 +22,39 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageReportClient; -import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; -import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import java.util.Collection; -import java.util.Optional; /** * Created by ashvayka on 27.03.18. @@ -75,6 +74,41 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return "alarm"; } + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) { + boolean creationEnabled = apiUsageStateService.getApiUsageState(request.getTenantId()).isAlarmCreationEnabled(); + var result = alarmService.createAlarm(request, creationEnabled); + if (result.isCreated()) { + apiUsageClient.report(request.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); + } + return withWsCallback(request, result); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + return withWsCallback(alarmService.updateAlarm(request)); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withWsCallback(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { + return withWsCallback(alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { + return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs)); + } + @Override public Alarm createOrUpdateAlarm(Alarm alarm) { AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); @@ -102,29 +136,29 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { - AlarmOperationResult result = alarmService.deleteAlarm(tenantId, alarmId); + AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId); onAlarmDeleted(result); return result.isSuccessful(); } @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { - ListenableFuture result = alarmService.ackAlarm(tenantId, alarmId, ackTs); + ListenableFuture result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = clearAlarmForResult(tenantId, alarmId, details, clearTs); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); - Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return result; + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.immediateFuture(new AlarmOperationResult(result)); } @Override @@ -138,8 +172,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { - return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); + public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { + return alarmService.findAlarmInfoById(tenantId, alarmId); } @Override @@ -153,8 +187,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { - return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); + public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { + return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); } @Override @@ -162,15 +196,21 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); + } + @Override public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); } + @Deprecated private void onAlarmUpdated(AlarmOperationResult result) { wsCallBackExecutor.submit(() -> { - Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + AlarmInfo alarm = new AlarmInfo(result.getAlarm()); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); @@ -181,10 +221,24 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } - private void onAlarmDeleted(AlarmOperationResult result) { + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { - Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { + subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY); + }, () -> { + return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm); + }); + } + }); + } + + private void onAlarmDeleted(AlarmApiCallResult result) { + wsCallBackExecutor.submit(() -> { + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> { subscriptionManagerService.onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY); @@ -195,9 +249,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } - private class AlarmUpdateCallback implements FutureCallback { + private class AlarmUpdateCallback implements FutureCallback { @Override - public void onSuccess(@Nullable AlarmOperationResult result) { + public void onSuccess(@Nullable AlarmApiCallResult result) { onAlarmUpdated(result); } @@ -207,4 +261,31 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } } + private AlarmApiCallResult withWsCallback(AlarmApiCallResult result) { + return withWsCallback(null, result); + } + + private AlarmApiCallResult withWsCallback(AlarmModificationRequest request, AlarmApiCallResult result) { + if (result.isSuccessful() && result.isModified()) { + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + if (result.isSeverityChanged()) { + AlarmInfo alarm = result.getAlarm(); + AlarmComment.AlarmCommentBuilder alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", + String.format("Alarm severity was updated from %s to %s", result.getOldSeverity(), alarm.getSeverity()))); + if (request != null && request.getUserId() != null) { + alarmComment.userId(request.getUserId()); + } + try { + alarmCommentService.saveAlarmComment(alarm, alarmComment.build(), null); + } catch (ThingsboardException e) { + log.error("Failed to save alarm comment", e); + } + } + } + return result; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index 6ea3208208..ea059fcb2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java index 7a1b159947..90041ffa13 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java @@ -16,7 +16,7 @@ package org.thingsboard.server.service.ws.telemetry.sub; import lombok.Getter; -import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; public class AlarmSubscriptionUpdate { @@ -28,15 +28,15 @@ public class AlarmSubscriptionUpdate { @Getter private String errorMsg; @Getter - private Alarm alarm; + private AlarmInfo alarm; @Getter private boolean alarmDeleted; - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm) { + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm) { this(subscriptionId, alarm, false); } - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm, boolean alarmDeleted) { + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm, boolean alarmDeleted) { super(); this.subscriptionId = subscriptionId; this.alarm = alarm; @@ -56,7 +56,7 @@ public class AlarmSubscriptionUpdate { @Override public String toString() { - return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", alarm=" - + alarm + "]"; + return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + + ", alarm=" + alarm + "]"; } -} +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index cb5caba133..cadf2c23be 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -172,6 +172,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected TenantId differentTenantId; protected CustomerId differentCustomerId; protected UserId customerUserId; + protected UserId differentCustomerUserId; @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; @@ -371,7 +372,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { differentCustomerUser.setCustomerId(savedDifferentCustomer.getId()); differentCustomerUser.setEmail(DIFFERENT_CUSTOMER_USER_EMAIL); - createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); + differentCustomerUser = createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); + differentCustomerUserId = differentCustomerUser.getId(); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java index 7c8b3c4db8..8566d9d6a4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java @@ -79,7 +79,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT .tenantId(tenantId) .customerId(customerId) .originator(customerDevice.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("test alarm type") .build(); @@ -316,7 +315,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT Alarm alarm = Alarm.builder() .originator(device.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("Test") .build(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 175f833c1c..777b97d940 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -124,7 +125,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(updatedAlarm); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); - testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED); } @@ -140,8 +142,57 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(updatedAlarm); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); - testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED); + + alarm = updatedAlarm; + alarm.setAcknowledged(true); + alarm.setAckTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertTrue(updatedAlarm.isAcknowledged()); + Assert.assertEquals(alarm.getAckTs(), updatedAlarm.getAckTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK); + + alarm = updatedAlarm; + alarm.setCleared(true); + alarm.setClearTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertTrue(updatedAlarm.isCleared()); + Assert.assertEquals(alarm.getClearTs(), updatedAlarm.getClearTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR); + + alarm = updatedAlarm; + alarm.setAssigneeId(tenantAdminUserId); + alarm.setAssignTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertEquals(tenantAdminUserId, updatedAlarm.getAssigneeId()); + Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + alarm = updatedAlarm; + alarm.setAssigneeId(null); + alarm.setAssignTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertNull(updatedAlarm.getAssigneeId()); + Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); } @Test @@ -187,7 +238,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); } @@ -200,7 +251,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); } @@ -245,7 +296,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); @@ -261,7 +312,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); @@ -278,7 +329,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus()); @@ -348,6 +399,129 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorPermission))); } + @Test + public void testAssignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testAssignAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isForbidden()); + } + + @Test + public void testReassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + logout(); + + loginCustomerUser(); + Mockito.reset(tbClusterService, auditLogService); + beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + customerUserId.getId()).andExpect(status().isOk()); + + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(customerUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testUnassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + beforeAssignmentTs = System.currentTimeMillis(); + + doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); + } + + @Test + public void testUnassignTenantAlarmViaCustomer() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + logout(); + loginCustomerUser(); + + Mockito.reset(tbClusterService, auditLogService); + beforeAssignmentTs = System.currentTimeMillis(); + + doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN); + } + @Test public void testFindAlarmsViaCustomerUser() throws Exception { loginCustomerUser(); @@ -364,7 +538,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { var response = doGetTyped( "/api/alarm/" + EntityType.DEVICE + "/" + customerDevice.getUuidId() + "?page=0&pageSize=" + size, - new TypeReference>() {} + new TypeReference>() { + } ); var foundAlarmInfos = response.getData(); Assert.assertNotNull("Found pageData is null", foundAlarmInfos); @@ -412,7 +587,6 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = Alarm.builder() .originator(device.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("Test") .build(); @@ -431,7 +605,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { this.token = tokens.get("token").asText(); PageData pageData = doGetTyped( - "/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference>() {} + "/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference>() { + } ); Assert.assertNotNull("Found pageData is null", pageData); @@ -457,12 +632,11 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(alarmDao, customerDevice.getId(), alarmId, "/api/alarm/" + alarmId); } - private Alarm createAlarm(String type) throws Exception { + private AlarmInfo createAlarm(String type) throws Exception { Alarm alarm = Alarm.builder() .tenantId(tenantId) .customerId(customerId) .originator(customerDevice.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type(type) .build(); @@ -470,6 +644,10 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { alarm = doPost("/api/alarm", alarm, Alarm.class); Assert.assertNotNull(alarm); - return alarm; + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(alarm, new Alarm(foundAlarm)); + + return foundAlarm; } } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java index 5014280b07..1cf2cd44bb 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java @@ -76,7 +76,6 @@ abstract public class BaseAlarmEdgeTest extends AbstractEdgeTest { Device device = findDeviceByName("Edge Device 1"); Alarm alarm = new Alarm(); alarm.setOriginator(device.getId()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); alarm.setType("alarm"); alarm.setSeverity(AlarmSeverity.CRITICAL); edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index f9916d27a0..93aeb17de6 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -27,9 +27,10 @@ import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; @@ -81,36 +82,41 @@ public class DefaultTbAlarmServiceTest { @Test public void testSave() throws ThingsboardException { - var alarm = new Alarm(); - when(alarmSubscriptionService.createOrUpdateAlarm(alarm)).thenReturn(alarm); + var alarm = new AlarmInfo(); + when(alarmSubscriptionService.createAlarm(any())).thenReturn(AlarmApiCallResult.builder() + .successful(true) + .modified(true) + .alarm(alarm) + .build()); service.save(alarm, new User()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).createOrUpdateAlarm(eq(alarm)); + verify(alarmSubscriptionService, times(1)).createAlarm(any()); } @Test - public void testAck() { + public void testAck() throws ThingsboardException { var alarm = new Alarm(); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); - when(alarmSubscriptionService.ackAlarm(any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmSubscriptionService.acknowledgeAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).build()); service.ack(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).ackAlarm(any(), any(), anyLong()); + verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong()); } @Test - public void testClear() { + public void testClear() throws ThingsboardException { var alarm = new Alarm(); - alarm.setStatus(AlarmStatus.ACTIVE_ACK); - when(alarmSubscriptionService.clearAlarm(any(), any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); + alarm.setAcknowledged(true); + when(alarmSubscriptionService.clearAlarm(any(), any(), anyLong(), any())) + .thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).build()); service.clear(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), any(), anyLong()); + verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any()); } @Test diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java new file mode 100644 index 0000000000..a8aebf16e7 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java @@ -0,0 +1,85 @@ +/** + * 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.dao.alarm; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + + +@Data +public class AlarmApiCallResult { + + private final boolean successful; + private final boolean created; + private final boolean modified; + private final boolean cleared; + private final AlarmInfo alarm; + private final Alarm old; + private final List propagatedEntitiesList; + + @Builder + private AlarmApiCallResult(boolean successful, boolean created, boolean modified, boolean cleared, AlarmInfo alarm, Alarm old, List propagatedEntitiesList) { + this.successful = successful; + this.created = created; + this.modified = modified; + this.cleared = cleared; + this.alarm = alarm; + this.old = old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + public AlarmApiCallResult(AlarmApiCallResult other, List propagatedEntitiesList) { + this.successful = other.successful; + this.created = other.created; + this.modified = other.modified; + this.cleared = other.cleared; + this.alarm = other.alarm; + this.old = other.old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + public boolean isSeverityChanged() { + if (alarm == null || old == null) { + return false; + } else { + return !alarm.getSeverity().equals(old.getSeverity()); + } + } + + public AlarmSeverity getOldSeverity() { + return isSeverityChanged() ? old.getSeverity() : null; + } + + public boolean isPropagationChanged() { + if (created) { + return true; + } + if (alarm == null || old == null) { + return false; + } + return (alarm.isPropagate() != old.isPropagate()) || + (alarm.isPropagateToOwner() != old.isPropagateToOwner()) || + (alarm.isPropagateToTenant() != old.isPropagateToTenant()) || + (!alarm.getPropagateRelationTypes().equals(old.getPropagateRelationTypes())); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java index 6125d4a255..e3a05a4e2f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -16,16 +16,20 @@ package org.thingsboard.server.dao.alarm; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityId; import java.util.Collections; import java.util.List; +@Builder @Data @AllArgsConstructor +@Deprecated public class AlarmOperationResult { private final Alarm alarm; private final boolean successful; @@ -40,4 +44,21 @@ public class AlarmOperationResult { public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { this(alarm, successful, false, null, propagatedEntitiesList); } + + public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List propagatedEntitiesList) { + this.alarm = alarm; + this.successful = successful; + this.created = created; + this.propagatedEntitiesList = propagatedEntitiesList; + this.oldSeverity = null; + } + + //Temporary while we have not removed the AlarmOperationResult. + public AlarmOperationResult(AlarmApiCallResult result) { + this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null; + this.successful = result.isSuccessful() && (result.isCreated() || result.isModified()); + this.created = result.isCreated(); + this.oldSeverity = result.getOldSeverity(); + this.propagatedEntitiesList = result.getPropagatedEntitiesList(); + } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 2c60c41062..ad2aa1814f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -23,10 +23,13 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -34,35 +37,76 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; -/** - * Created by ashvayka on 11.05.17. - */ + public interface AlarmService extends EntityDaoService { + /* + * New API, since 3.5. + */ + + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled); + + /** + * Designed to update existing alarm. Accepts only part of the alarm fields. + */ + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long ts); + + AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId); + + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5.0", forRemoval = true) AlarmOperationResult createOrUpdateAlarm(Alarm alarm); + @Deprecated(since = "3.5.0", forRemoval = true) AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); - AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); - + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + @Deprecated(since = "3.5.0", forRemoval = true) + AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); + + @Deprecated(since = "3.5.0", forRemoval = true) + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + + // Other API Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); - ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); + AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus); + AlarmStatus alarmStatus, String assigneeId); - ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 9a9e000e4c..f2317efa05 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -29,10 +30,13 @@ public interface EntityService { Optional fetchEntityName(TenantId tenantId, EntityId entityId); + Optional fetchEntityLabel(TenantId tenantId, EntityId entityId); + Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId); + Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId); + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index efe92977cc..d8904f03e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) -public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { +public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasEmail { private static final long serialVersionUID = 5047448057830660988L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index c9847b0078..d194fb7755 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) -public class Customer extends ContactBased implements HasTenantId, ExportableEntity { +public class Customer extends ContactBased implements HasTenantId, ExportableEntity, HasTitle { private static final long serialVersionUID = -1599722990298929275L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index e273e1b97e..7c166e53e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -31,7 +31,7 @@ import java.util.Objects; import java.util.Set; @ApiModel -public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { +public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId, HasTitle { private TenantId tenantId; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 8f480bcf57..9eb18c76c5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -74,6 +74,8 @@ public class DataConstants { public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_CLEAR = "ALARM_CLEAR"; + public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; + public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String COMMENT_CREATED = "COMMENT_CREATED"; public static final String COMMENT_UPDATED = "COMMENT_UPDATED"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index c1517e2230..27457bda34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -40,7 +40,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) @Slf4j -public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { +public class Device extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 216c33b2e8..472d73adb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,7 +19,30 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, - TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE, - NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE, NOTIFICATION_REQUEST, NOTIFICATION, NOTIFICATION_RULE; + TENANT, + CUSTOMER, + USER, + DASHBOARD, + ASSET, + DEVICE, + ALARM, + RULE_CHAIN, + RULE_NODE, + ENTITY_VIEW, + WIDGETS_BUNDLE, + WIDGET_TYPE, + TENANT_PROFILE, + DEVICE_PROFILE, + ASSET_PROFILE, + API_USAGE_STATE, + TB_RESOURCE, + OTA_PACKAGE, + EDGE, + RPC, + QUEUE, + NOTIFICATION_TARGET, + NOTIFICATION_TEMPLATE, + NOTIFICATION_REQUEST, + NOTIFICATION, + NOTIFICATION_RULE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java new file mode 100644 index 0000000000..fd7fbf19fd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasEmail extends HasName { + + String getEmail(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java new file mode 100644 index 0000000000..fe4f46c9de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasLabel extends HasName { + + String getLabel(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java new file mode 100644 index 0000000000..aab1c6c346 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasTitle { + + String getTitle(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index c917d15910..c468cb24f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId { +public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasTitle { private static final long serialVersionUID = 3168391583570815419L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index f9c70a2c19..7e5210e344 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class Tenant extends ContactBased implements HasTenantId { +public class Tenant extends ContactBased implements HasTenantId, HasTitle { private static final long serialVersionUID = 8057243243859922101L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 9db30fd466..34fec2cd82 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModel; @@ -22,6 +23,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; @@ -30,6 +32,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -40,8 +43,10 @@ import java.util.List; */ @ApiModel @Data +@EqualsAndHashCode(callSuper = true) @Builder @AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Alarm extends BaseData implements HasName, HasTenantId, HasCustomerId { @ApiModelProperty(position = 3, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @@ -58,25 +63,31 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha private EntityId originator; @ApiModelProperty(position = 8, required = true, value = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; - @ApiModelProperty(position = 9, required = true, value = "Alarm status", example = "CLEARED_UNACK") - private AlarmStatus status; - @ApiModelProperty(position = 10, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + @ApiModelProperty(position = 9, required = true, value = "Acknowledged", example = "true") + private boolean acknowledged; + @ApiModelProperty(position = 10, required = true, value = "Cleared", example = "false") + private boolean cleared; + @ApiModelProperty(position = 11, value = "Alarm assignee user id") + private UserId assigneeId; + @ApiModelProperty(position = 12, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; - @ApiModelProperty(position = 11, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + @ApiModelProperty(position = 13, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; - @ApiModelProperty(position = 12, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") + @ApiModelProperty(position = 14, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") private long ackTs; - @ApiModelProperty(position = 13, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") + @ApiModelProperty(position = 15, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") private long clearTs; - @ApiModelProperty(position = 14, value = "JSON object with alarm details") + @ApiModelProperty(position = 16, value = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") + private long assignTs; + @ApiModelProperty(position = 17, value = "JSON object with alarm details") private transient JsonNode details; - @ApiModelProperty(position = 15, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") + @ApiModelProperty(position = 18, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; - @ApiModelProperty(position = 16, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") + @ApiModelProperty(position = 19, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") private boolean propagateToOwner; - @ApiModelProperty(position = 17, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") + @ApiModelProperty(position = 20, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") private boolean propagateToTenant; - @ApiModelProperty(position = 18, value = "JSON array of relation types that should be used for propagation. " + + @ApiModelProperty(position = 21, value = "JSON array of relation types that should be used for propagation. " + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") private List propagateRelationTypes; @@ -97,11 +108,14 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha this.type = alarm.getType(); this.originator = alarm.getOriginator(); this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); + this.assigneeId = alarm.getAssigneeId(); this.startTs = alarm.getStartTs(); this.endTs = alarm.getEndTs(); + this.acknowledged = alarm.isAcknowledged(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.cleared = alarm.isCleared(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); @@ -119,7 +133,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @ApiModelProperty(position = 1, value = "JSON object with the alarm Id. " + "Specify this field to update the alarm. " + "Referencing non-existing alarm Id will cause error. " + - "Omit this field to create new alarm." ) + "Omit this field to create new alarm.") @Override public AlarmId getId() { return super.getId(); @@ -132,4 +146,19 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha return super.getCreatedTime(); } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @ApiModelProperty(position = 22, required = true, value = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + public AlarmStatus getStatus() { + return toStatus(cleared, acknowledged); + } + + public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) { + + if (cleared) { + return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; + } else { + return acknowledged ? AlarmStatus.ACTIVE_ACK : AlarmStatus.ACTIVE_UNACK; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java new file mode 100644 index 0000000000..20ed7b549c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +import java.io.Serializable; + +@Builder +@AllArgsConstructor +@Data +public class AlarmAssignee implements Serializable { + + private static final long serialVersionUID = 6628286223963972860L; + + private final UserId id; + private final String firstName; + private final String lastName; + private final String email; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java new file mode 100644 index 0000000000..d1f32e927b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class AlarmAssigneeUpdate implements Serializable { + + private static final long serialVersionUID = -2391676304697483808L; + + private final boolean deleted; + private final AlarmAssignee assignee; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java new file mode 100644 index 0000000000..ecf882e2c9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@Data +@Builder +public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationRequest { + + @NotNull + @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private TenantId tenantId; + @ApiModelProperty(position = 2, value = "JSON object with Customer Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private CustomerId customerId; + @NotNull + @ApiModelProperty(position = 3, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm") + @Length(fieldName = "type") + private String type; + @NotNull + @ApiModelProperty(position = 4, required = true, value = "JSON object with alarm originator id") + private EntityId originator; + @NotNull + @ApiModelProperty(position = 5, required = true, value = "Alarm severity", example = "CRITICAL") + private AlarmSeverity severity; + @ApiModelProperty(position = 6, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + private long startTs; + @ApiModelProperty(position = 7, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + private long endTs; + @NoXss + @ApiModelProperty(position = 8, value = "JSON object with alarm details") + private JsonNode details; + @Valid + @ApiModelProperty(position = 9, value = "JSON object with propagation details") + private AlarmPropagationInfo propagation; + + private UserId userId; + + public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a) { + return fromAlarm(a, null); + } + + public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a, UserId userId) { + return AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(a.getTenantId()) + .customerId(a.getCustomerId()) + .type(a.getType()) + .originator(a.getOriginator()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .userId(userId) + .build(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 5483c519db..405097f6f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -17,15 +17,36 @@ package org.thingsboard.server.common.data.alarm; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.User; +import java.util.Objects; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @ApiModel public class AlarmInfo extends Alarm { private static final long serialVersionUID = 2807343093519543363L; + @Getter + @Setter @ApiModelProperty(position = 19, value = "Alarm originator name", example = "Thermostat") private String originatorName; + @Getter + @Setter + @ApiModelProperty(position = 20, value = "Alarm originator label", example = "Thermostat label") + private String originatorLabel; + + @Getter + @Setter + @ApiModelProperty(position = 21, value = "Alarm assignee") + private AlarmAssignee assignee; + public AlarmInfo() { super(); } @@ -34,35 +55,18 @@ public class AlarmInfo extends Alarm { super(alarm); } - public AlarmInfo(Alarm alarm, String originatorName) { - super(alarm); - this.originatorName = originatorName; - } - - public String getOriginatorName() { - return originatorName; + public AlarmInfo(AlarmInfo alarmInfo) { + super(alarmInfo); + this.originatorName = alarmInfo.originatorName; + this.originatorLabel = alarmInfo.originatorLabel; + this.assignee = alarmInfo.getAssignee(); } - public void setOriginatorName(String originatorName) { + public AlarmInfo(Alarm alarm, String originatorName, String originatorLabel, AlarmAssignee assignee) { + super(alarm); this.originatorName = originatorName; + this.originatorLabel = originatorLabel; + this.assignee = assignee; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - AlarmInfo alarmInfo = (AlarmInfo) o; - - return originatorName != null ? originatorName.equals(alarmInfo.originatorName) : alarmInfo.originatorName == null; - - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (originatorName != null ? originatorName.hashCode() : 0); - return result; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java new file mode 100644 index 0000000000..e40cb15d22 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +public interface AlarmModificationRequest { + + TenantId getTenantId(); + + long getStartTs(); + + long getEndTs(); + + void setStartTs(long startTs); + + void setEndTs(long endTs); + + UserId getUserId(); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java new file mode 100644 index 0000000000..10069f8dbe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.util.Collections; +import java.util.List; + +@Builder +@Data +public class AlarmPropagationInfo { + + public static AlarmPropagationInfo EMPTY = new AlarmPropagationInfo(false, false, false, Collections.emptyList()); + + @ApiModelProperty(position = 1, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") + private boolean propagate; + @ApiModelProperty(position = 2, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") + private boolean propagateToOwner; + @ApiModelProperty(position = 3, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") + private boolean propagateToTenant; + @NoXss + @ApiModelProperty(position = 4, value = "JSON array of relation types that should be used for propagation. " + + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") + private List propagateRelationTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index a923b54abc..e4d0fb32df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; /** @@ -33,6 +34,8 @@ public class AlarmQuery { private TimePageLink pageLink; private AlarmSearchStatus searchStatus; private AlarmStatus status; + private UserId assigneeId; + @Deprecated private Boolean fetchOriginator; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java index 92af75c8f0..4225f6efbb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java @@ -15,26 +15,12 @@ */ package org.thingsboard.server.common.data.alarm; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; - public enum AlarmSearchStatus { - ANY(AlarmStatus.values()), - ACTIVE(AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UNACK), - CLEARED(AlarmStatus.CLEARED_ACK, AlarmStatus.CLEARED_UNACK), - ACK(AlarmStatus.ACTIVE_ACK, AlarmStatus.CLEARED_ACK), - UNACK(AlarmStatus.ACTIVE_UNACK, AlarmStatus.CLEARED_UNACK); - - @JsonIgnore - @Getter - private Set statuses; + ANY, + ACTIVE, + CLEARED, + ACK, + UNACK; - AlarmSearchStatus(AlarmStatus... statuses) { - this.statuses = new LinkedHashSet<>(Arrays.asList(statuses)); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java new file mode 100644 index 0000000000..c8f40fa048 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import java.util.List; +import java.util.Optional; + +public class AlarmStatusFilter { + + private static final AlarmStatusFilter EMPTY = new AlarmStatusFilter(Optional.empty(), Optional.empty()); + + private final Optional clearFilter; + private final Optional ackFilter; + + private AlarmStatusFilter(Optional clearFilter, Optional ackFilter) { + this.clearFilter = clearFilter; + this.ackFilter = ackFilter; + } + + public static AlarmStatusFilter from(AlarmQuery query) { + if (query.getSearchStatus() != null) { + return AlarmStatusFilter.from(query.getSearchStatus()); + } else if (query.getStatus() != null) { + return AlarmStatusFilter.from(query.getStatus()); + } + return AlarmStatusFilter.empty(); + } + + public static AlarmStatusFilter from(AlarmSearchStatus alarmSearchStatus) { + switch (alarmSearchStatus) { + case ACK: + return new AlarmStatusFilter(Optional.empty(), Optional.of(true)); + case UNACK: + return new AlarmStatusFilter(Optional.empty(), Optional.of(false)); + case ACTIVE: + return new AlarmStatusFilter(Optional.of(false), Optional.empty()); + case CLEARED: + return new AlarmStatusFilter(Optional.of(true), Optional.empty()); + default: + return EMPTY; + } + } + + public static AlarmStatusFilter from(AlarmStatus alarmStatus) { + switch (alarmStatus) { + case ACTIVE_UNACK: + return new AlarmStatusFilter(Optional.of(false), Optional.of(false)); + case ACTIVE_ACK: + return new AlarmStatusFilter(Optional.of(false), Optional.of(true)); + case CLEARED_UNACK: + return new AlarmStatusFilter(Optional.of(true), Optional.of(false)); + case CLEARED_ACK: + return new AlarmStatusFilter(Optional.of(true), Optional.of(true)); + default: + return EMPTY; + } + } + + public static AlarmStatusFilter empty() { + return EMPTY; + } + + public boolean hasAnyFilter() { + return clearFilter.isPresent() || ackFilter.isPresent(); + } + + public boolean hasClearFilter() { + return clearFilter.isPresent(); + } + + public boolean hasAckFilter() { + return ackFilter.isPresent(); + } + + public boolean getClearFilter() { + return clearFilter.orElseThrow(() -> new RuntimeException("Clear filter is not set! Use `hasClearFilter` to check.")); + } + + public boolean getAckFilter() { + return ackFilter.orElseThrow(() -> new RuntimeException("Ack filter is not set! Use `hasAckFilter` to check.")); + } + + + public static AlarmStatusFilter fromList(List list) { + if (list == null || list.isEmpty() || list.contains(AlarmSearchStatus.ANY)) { + return EMPTY; + } + boolean clearFilter = list.contains(AlarmSearchStatus.CLEARED); + boolean activeFilter = list.contains(AlarmSearchStatus.ACTIVE); + Optional clear = Optional.empty(); + if (clearFilter && !activeFilter || !clearFilter && activeFilter) { + clear = Optional.of(clearFilter); + } + + boolean ackFilter = list.contains(AlarmSearchStatus.ACK); + boolean unackFilter = list.contains(AlarmSearchStatus.UNACK); + Optional ack = Optional.empty(); + if (ackFilter && !unackFilter || !ackFilter && unackFilter) { + ack = Optional.of(ackFilter); + } + return new AlarmStatusFilter(clear, ack); + } + + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java new file mode 100644 index 0000000000..23edafd6d2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@Data +@Builder +public class AlarmUpdateRequest implements AlarmModificationRequest { + + @NotNull + @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private TenantId tenantId; + @NotNull + @ApiModelProperty(position = 2, value = "JSON object with the alarm Id. " + + "Specify this field to update the alarm. " + + "Referencing non-existing alarm Id will cause error. " + + "Omit this field to create new alarm.") + private AlarmId alarmId; + @NotNull + @ApiModelProperty(position = 3, required = true, value = "Alarm severity", example = "CRITICAL") + private AlarmSeverity severity; + @ApiModelProperty(position = 4, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + private long startTs; + @ApiModelProperty(position = 5, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + private long endTs; + @NoXss + @ApiModelProperty(position = 6, value = "JSON object with alarm details") + private JsonNode details; + @Valid + @ApiModelProperty(position = 7, value = "JSON object with propagation details") + private AlarmPropagationInfo propagation; + + private UserId userId; + + public static AlarmUpdateRequest fromAlarm(Alarm a) { + return fromAlarm(a, null); + } + + public static AlarmUpdateRequest fromAlarm(Alarm a, UserId userId) { + return AlarmUpdateRequest.builder() + .tenantId(a.getTenantId()) + .alarmId(a.getId()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .userId(userId) + .build(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java index e15557cdfe..8f25c0cbc5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; @Data @NoArgsConstructor @@ -35,6 +36,7 @@ public class EntityAlarm implements HasTenantId { private String alarmType; private CustomerId customerId; + private UserId assigneeId; private AlarmId alarmId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 0721b1b310..d5f4efa6c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -23,6 +23,7 @@ import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; @@ -37,7 +38,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) -public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, ExportableEntity { +public class Asset extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 876444a1dd..b479f1a236 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -40,6 +40,8 @@ public enum ActionType { ALARM_ACK(false), ALARM_CLEAR(false), ALARM_DELETE(false), + ALARM_ASSIGN(false), + ALARM_UNASSIGN(false), LOGIN(false), LOGOUT(false), LOCKOUT(false), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index 34c0c31428..4304abfb1d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -21,6 +21,7 @@ import lombok.EqualsAndHashCode; import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; @@ -35,7 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @ToString @Setter -public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { +public class Edge extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId { private static final long serialVersionUID = 4934987555236873728L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index f57881a63b..9b813ae89c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -31,6 +31,8 @@ public enum EdgeEventActionType { RPC_CALL, ALARM_ACK, ALARM_CLEAR, + ALARM_ASSIGN, + ALARM_UNASSIGN, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, CREDENTIALS_REQUEST, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index d384c1acb5..3aaba1cf61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -25,6 +25,14 @@ import java.util.UUID; */ public class EntityIdFactory { + public static EntityId getByTypeAndUuid(int type, String uuid) { + return getByTypeAndUuid(EntityType.values()[type], UUID.fromString(uuid)); + } + + public static EntityId getByTypeAndUuid(String type, String uuid) { + return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); + } + public static EntityId getByTypeAndId(String type, String uuid) { return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java new file mode 100644 index 0000000000..8d7413b438 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NameLabelAndCustomerDetails { + private final String name; + private final String label; + private final CustomerId customerId; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java index d63a5a95bd..1ae434f28c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java @@ -15,24 +15,36 @@ */ package org.thingsboard.server.common.data.query; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import java.util.HashMap; import java.util.Map; -import java.util.UUID; +@EqualsAndHashCode(callSuper = true) public class AlarmData extends AlarmInfo { + private static final long serialVersionUID = -7042457913823369638L; + @Getter private final EntityId entityId; @Getter private final Map> latest; - public AlarmData(Alarm alarm, String originatorName, EntityId entityId) { - super(alarm, originatorName); + public AlarmData(AlarmInfo main, AlarmData prototype) { + super(main); + this.entityId = prototype.entityId; + this.latest = new HashMap<>(); + this.latest.putAll(prototype.getLatest()); + } + + public AlarmData(Alarm alarm, EntityId entityId) { + super(alarm); this.entityId = entityId; this.latest = new HashMap<>(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java index e46deddfd7..9c0253272c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java @@ -19,11 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.ToString; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.UserId; import java.util.List; @@ -41,6 +40,7 @@ public class AlarmDataPageLink extends EntityDataPageLink { private List statusList; private List severityList; private boolean searchPropagatedAlarms; + private UserId assigneeId; public AlarmDataPageLink() { super(); @@ -49,7 +49,8 @@ public class AlarmDataPageLink extends EntityDataPageLink { public AlarmDataPageLink(int pageSize, int page, String textSearch, EntityDataSortOrder sortOrder, boolean dynamic, boolean searchPropagatedAlarms, long startTs, long endTs, long timeWindow, - List typeList, List statusList, List severityList) { + List typeList, List statusList, List severityList, + UserId assigneeId) { super(pageSize, page, textSearch, sortOrder, dynamic); this.searchPropagatedAlarms = searchPropagatedAlarms; this.startTs = startTs; @@ -58,6 +59,7 @@ public class AlarmDataPageLink extends EntityDataPageLink { this.typeList = typeList; this.statusList = statusList; this.severityList = severityList; + this.assigneeId = assigneeId; } @JsonIgnore @@ -65,7 +67,8 @@ public class AlarmDataPageLink extends EntityDataPageLink { return new AlarmDataPageLink(this.getPageSize(), this.getPage() + 1, this.getTextSearch(), this.getSortOrder(), this.isDynamic(), this.searchPropagatedAlarms, this.startTs, this.endTs, this.timeWindow, - this.typeList, this.statusList, this.severityList + this.typeList, this.statusList, this.severityList, + this.assigneeId ); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 900fd966e8..c9bd9fbad2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -15,10 +15,20 @@ */ package org.thingsboard.server.common.data.security; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.util.Arrays; +import java.util.Objects; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; @EqualsAndHashCode(callSuper = true) public class UserCredentials extends BaseData { @@ -30,6 +40,20 @@ public class UserCredentials extends BaseData { private String password; private String activateToken; private String resetToken; + + @NoXss + private transient JsonNode additionalInfo; + + @JsonIgnore + private byte[] additionalInfoBytes; + + public JsonNode getAdditionalInfo() { + return getJson(() -> additionalInfo, () -> additionalInfoBytes); + } + + public void setAdditionalInfo(JsonNode settings) { + setJson(settings, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); + } public UserCredentials() { super(); @@ -46,6 +70,7 @@ public class UserCredentials extends BaseData { this.enabled = userCredentials.isEnabled(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); + setAdditionalInfo(userCredentials.getAdditionalInfo()); } public UserId getUserId() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index d47876b268..a7e9fee82a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.widget; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -25,7 +26,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data -public class BaseWidgetType extends BaseData implements HasTenantId { +public class BaseWidgetType extends BaseData implements HasName, HasTenantId { private static final long serialVersionUID = 8388684344603660756L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index 876438f834..679dead962 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -25,6 +25,7 @@ import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasTitle; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -33,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class WidgetsBundle extends SearchTextBased implements HasName, HasTenantId, ExportableEntity { +public class WidgetsBundle extends SearchTextBased implements HasName, HasTenantId, ExportableEntity, HasTitle { private static final long serialVersionUID = -7627368878362410489L; diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 103f25a4ac..3d2e7078a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -15,17 +15,21 @@ */ package org.thingsboard.server.dao.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -44,12 +48,16 @@ public interface AlarmDao extends Dao { Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); Alarm findAlarmById(TenantId tenantId, UUID key); ListenableFuture findAlarmByIdAsync(TenantId tenantId, UUID key); + AlarmInfo findAlarmInfoById(TenantId tenantId, UUID key); + Alarm save(TenantId tenantId, Alarm alarm); PageData findAlarms(TenantId tenantId, AlarmQuery query); @@ -58,7 +66,7 @@ public interface AlarmDao extends Dao { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); - Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status); + Set findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); @@ -67,4 +75,17 @@ public interface AlarmDao extends Dao { List findEntityAlarmRecords(TenantId tenantId, AlarmId id); void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); + + AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled); + + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 606eee468d..0dd2eebf87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -20,20 +20,22 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; @@ -41,6 +43,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -50,11 +53,11 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; -import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -63,8 +66,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -73,32 +74,52 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AlarmDaoService") @Slf4j +@RequiredArgsConstructor public class BaseAlarmService extends AbstractEntityService implements AlarmService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; - @Autowired - private AlarmDao alarmDao; + private final TenantService tenantService; + private final AlarmDao alarmDao; + private final EntityService entityService; + private final DataValidator alarmDataValidator; - @Autowired - private EntityService entityService; + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + validateAlarmRequest(request); + return withPropagated(alarmDao.updateAlarm(request)); + } - @Autowired - private DataValidator alarmDataValidator; + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) { + return createAlarm(request, true); + } - protected ExecutorService readResultsProcessingExecutor; + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { + validateAlarmRequest(request); + CustomerId customerId = entityService.fetchEntityCustomerId(request.getTenantId(), request.getOriginator()).orElse(null); + if (customerId == null && request.getCustomerId() != null) { + throw new DataValidationException("Can't assign alarm to customer. Originator is not assigned to customer!"); + } else if (customerId != null && request.getCustomerId() != null && !customerId.equals(request.getCustomerId())) { + throw new DataValidationException("Can't assign alarm to customer. Originator belongs to different customer!"); + } + request.setCustomerId(customerId); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, alarmCreationEnabled); + if (!result.isSuccessful() && !alarmCreationEnabled) { + throw new ApiUsageLimitsExceededException("Alarms creation is disabled"); + } + return withPropagated(result); + } - @PostConstruct - public void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("alarm-service")); + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs)); } - @PreDestroy - public void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); } @Override @@ -116,8 +137,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (alarm.getEndTs() == 0L) { alarm.setEndTs(alarm.getStartTs()); } - alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).get()); + alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).orElse(null)); if (alarm.getId() == null) { + // Atomic update and return alarm + assignee. Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()); if (existing == null || existing.getStatus().isCleared()) { if (!alarmCreationEnabled) { @@ -135,6 +157,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmDao.findLatestActiveByOriginatorAndType(tenantId, originator, type); + } + + @Override public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmDao.findLatestByOriginatorAndTypeAsync(tenantId, originator, type); } @@ -147,6 +175,20 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + @Transactional + public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId) { + log.debug("Deleting Alarm Id: {}", alarmId); + AlarmInfo alarm = alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); + if (alarm == null) { + return AlarmApiCallResult.builder().successful(false).build(); + } else { + deleteEntityRelations(tenantId, alarm.getId()); + alarmDao.removeById(tenantId, alarm.getUuidId()); + return AlarmApiCallResult.builder().alarm(alarm).successful(true).build(); + } + } + @Override @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { @@ -165,10 +207,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); List propagatedEntitiesList = createEntityAlarmRecords(saved); - return new AlarmOperationResult(saved, true, true, null, propagatedEntitiesList); + return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } - private List createEntityAlarmRecords(Alarm alarm) throws InterruptedException, ExecutionException { + private List createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException { Set propagatedEntitiesSet = new LinkedHashSet<>(); propagatedEntitiesSet.add(alarm.getOriginator()); if (alarm.isPropagate()) { @@ -226,45 +268,41 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) { - return getAndUpdateAsync(tenantId, alarmId, new Function() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || alarm.getStatus().isAck()) { - return new AlarmOperationResult(alarm, false); - } else { - AlarmStatus oldStatus = alarm.getStatus(); - AlarmStatus newStatus = oldStatus.isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK; - alarm.setStatus(newStatus); - alarm.setAckTs(ackTime); - alarm = alarmDao.save(alarm.getTenantId(), alarm); - return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); - } - } - }); + Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId()); + if (alarm == null || alarm.getStatus().isAck()) { + return Futures.immediateFuture(new AlarmOperationResult(alarm, false)); + } else { + alarm.setAcknowledged(true); + alarm.setAckTs(ackTime); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)))); + } } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) { - return getAndUpdateAsync(tenantId, alarmId, new Function() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || alarm.getStatus().isCleared()) { - return new AlarmOperationResult(alarm, false); - } else { - AlarmStatus oldStatus = alarm.getStatus(); - AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; - alarm.setStatus(newStatus); - alarm.setClearTs(clearTime); - if (details != null) { - alarm.setDetails(details); - } - alarm = alarmDao.save(alarm.getTenantId(), alarm); - return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); - } + Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId()); + if (alarm == null || alarm.getStatus().isCleared()) { + return Futures.immediateFuture(new AlarmOperationResult(alarm, false)); + } else { + alarm.setCleared(true); + alarm.setClearTs(clearTime); + if (details != null) { + alarm.setDetails(details); } - }); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)))); + } + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { + return withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime) { + return withPropagated(alarmDao.unassignAlarm(tenantId, alarmId, unassignTime)); } @Override @@ -282,60 +320,35 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { + public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId); validateId(alarmId, "Incorrect alarmId " + alarmId); - return Futures.transform(alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()), - a -> { - AlarmInfo alarmInfo = new AlarmInfo(a); - alarmInfo.setOriginatorName( - entityService.fetchEntityName(tenantId, alarmInfo.getOriginator()).orElse("N/A")); - return alarmInfo; - }, MoreExecutors.directExecutor()); + return alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); } @Override public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { - PageData alarms = alarmDao.findAlarms(tenantId, query); - if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - return fetchAlarmsOriginators(tenantId, alarms); - } - return Futures.immediateFuture(alarms); + return Futures.immediateFuture(alarmDao.findAlarms(tenantId, query)); } @Override public ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { - PageData alarms = alarmDao.findCustomerAlarms(tenantId, customerId, query); - if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - return fetchAlarmsOriginators(tenantId, alarms); - } - return Futures.immediateFuture(alarms); - } - - private ListenableFuture> fetchAlarmsOriginators(TenantId tenantId, PageData alarms) { - List> alarmFutures = new ArrayList<>(alarms.getData().size()); - for (AlarmInfo alarmInfo : alarms.getData()) { - alarmInfo.setOriginatorName( - entityService.fetchEntityName(tenantId, alarmInfo.getOriginator()).orElse("Deleted")); - alarmFutures.add(Futures.immediateFuture(alarmInfo)); - } - return Futures.transform(Futures.successfulAsList(alarmFutures), - alarmInfos -> new PageData<>(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), - alarms.hasNext()), MoreExecutors.directExecutor()); + return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query)); } @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus) { - Set statusList = null; + AlarmStatus alarmStatus, String assigneeId) { + AlarmStatusFilter asf; if (alarmSearchStatus != null) { - statusList = alarmSearchStatus.getStatuses(); + asf = AlarmStatusFilter.from(alarmSearchStatus); } else if (alarmStatus != null) { - statusList = Collections.singleton(alarmStatus); + asf = AlarmStatusFilter.from(alarmStatus); + } else { + asf = AlarmStatusFilter.empty(); } - Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList); - + Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, asf, assigneeId); return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } @@ -357,10 +370,15 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (alarm.getAckTs() > existing.getAckTs()) { existing.setAckTs(alarm.getAckTs()); } - existing.setStatus(alarm.getStatus()); + if (alarm.getAssignTs() > existing.getAssignTs()) { + existing.setAssignTs(alarm.getAssignTs()); + } + existing.setAcknowledged(alarm.isAcknowledged()); + existing.setCleared(alarm.isCleared()); existing.setSeverity(alarm.getSeverity()); existing.setDetails(alarm.getDetails()); existing.setCustomerId(alarm.getCustomerId()); + existing.setAssigneeId(alarm.getAssigneeId()); existing.setPropagate(existing.isPropagate() || alarm.isPropagate()); existing.setPropagateToOwner(existing.isPropagateToOwner() || alarm.isPropagateToOwner()); existing.setPropagateToTenant(existing.isPropagateToTenant() || alarm.isPropagateToTenant()); @@ -378,6 +396,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return existing; } + private List getPropagationEntityIdsList(Alarm alarm) { + return new ArrayList<>(getPropagationEntityIds(alarm)); + } + private Set getPropagationEntityIds(Alarm alarm) { if (alarm.isPropagate() || alarm.isPropagateToOwner() || alarm.isPropagateToTenant()) { List entityAlarms = alarmDao.findEntityAlarmRecords(alarm.getTenantId(), alarm.getId()); @@ -388,7 +410,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } private void createEntityAlarmRecord(TenantId tenantId, EntityId entityId, Alarm alarm) { - EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), alarm.getId()); + EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), null, alarm.getId()); try { alarmDao.createEntityAlarmRecord(entityAlarm); } catch (Exception e) { @@ -396,12 +418,6 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } } - private ListenableFuture getAndUpdateAsync(TenantId tenantId, AlarmId alarmId, Function function) { - validateId(alarmId, "Alarm id should be specified!"); - ListenableFuture entity = alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()); - return Futures.transform(entity, function, readResultsProcessingExecutor); - } - private T getAndUpdate(TenantId tenantId, AlarmId alarmId, Function function) { validateId(alarmId, "Alarm id should be specified!"); Alarm entity = alarmDao.findAlarmById(tenantId, alarmId.getId()); @@ -418,4 +434,39 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return EntityType.ALARM; } + //TODO: refactor to use efficient caching. + private AlarmApiCallResult withPropagated(AlarmApiCallResult result) { + if (result.isSuccessful() && result.getAlarm() != null) { + List propagationEntities; + if (result.isPropagationChanged()) { + try { + propagationEntities = createEntityAlarmRecords(result.getAlarm()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } else { + propagationEntities = getPropagationEntityIdsList(result.getAlarm()); + } + return new AlarmApiCallResult(result, propagationEntities); + } else { + return result; + } + } + + private void validateAlarmRequest(AlarmModificationRequest request) { + ConstraintValidator.validateFields(request); + if (request.getEndTs() > 0 && request.getStartTs() > request.getEndTs()) { + throw new DataValidationException("Alarm start ts can't be greater then alarm end ts!"); + } + if (!tenantService.tenantExists(request.getTenantId())) { + throw new DataValidationException("Alarm is referencing to non-existent tenant!"); + } + if (request.getStartTs() == 0L) { + request.setStartTs(System.currentTimeMillis()); + } + if (request.getEndTs() == 0L) { + request.setEndTs(request.getStartTs()); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index e6caddd80f..7aa785107e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -166,6 +166,8 @@ public class AuditLogServiceImpl implements AuditLogService { case UPDATED: case ALARM_ACK: case ALARM_CLEAR: + case ALARM_ASSIGN: + case ALARM_UNASSIGN: case RELATIONS_DELETED: case ASSIGNED_TO_TENANT: if (entity != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index de6843e3a9..cbce9e48a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -20,7 +20,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasEmail; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTitle; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -34,6 +39,7 @@ import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.Optional; +import java.util.function.Function; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; @@ -77,35 +83,67 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Override public Optional fetchEntityName(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityName [{}]", entityId); - EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); - Optional> hasIdOpt = entityDaoService.findEntity(tenantId, entityId); - if (hasIdOpt.isPresent()) { - HasId hasId = hasIdOpt.get(); - if (hasId instanceof HasName) { - HasName hasName = (HasName) hasId; - return Optional.ofNullable(hasName.getName()); - } - } - return Optional.empty(); + return fetchAndConvert(tenantId, entityId, this::getName); + } + + @Override + public Optional fetchEntityLabel(TenantId tenantId, EntityId entityId) { + log.trace("Executing fetchEntityLabel [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getLabel); } @Override public Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityCustomerId [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getCustomerId); + } + + @Override + public Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) { + log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getNameLabelAndCustomerDetails); + } + + private Optional fetchAndConvert(TenantId tenantId, EntityId entityId, Function, T> converter) { EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); - Optional> hasIdOpt = entityDaoService.findEntity(tenantId, entityId); - if (hasIdOpt.isPresent()) { - HasId hasId = hasIdOpt.get(); - if (hasId instanceof HasCustomerId) { - HasCustomerId hasCustomerId = (HasCustomerId) hasId; - CustomerId customerId = hasCustomerId.getCustomerId(); - if (customerId == null) { - customerId = NULL_CUSTOMER_ID; - } - return Optional.of(customerId); + Optional> entityOpt = entityDaoService.findEntity(tenantId, entityId); + return entityOpt.map(converter); + } + + private String getName(HasId entity) { + return entity instanceof HasName ? ((HasName) entity).getName() : null; + } + + private String getLabel(HasId entity) { + if (entity instanceof HasTitle && StringUtils.isNotEmpty(((HasTitle) entity).getTitle())) { + return ((HasTitle) entity).getTitle(); + } + if (entity instanceof HasLabel && StringUtils.isNotEmpty(((HasLabel) entity).getLabel())) { + return ((HasLabel) entity).getLabel(); + } + if (entity instanceof HasEmail && StringUtils.isNotEmpty(((HasEmail) entity).getEmail())) { + return ((HasEmail) entity).getEmail(); + } + if (entity instanceof HasName && StringUtils.isNotEmpty(((HasName) entity).getName())) { + return ((HasName) entity).getName(); + } + return null; + } + + private CustomerId getCustomerId(HasId entity) { + if (entity instanceof HasCustomerId) { + HasCustomerId hasCustomerId = (HasCustomerId) entity; + CustomerId customerId = hasCustomerId.getCustomerId(); + if (customerId == null) { + customerId = NULL_CUSTOMER_ID; } + return customerId; } - return Optional.of(NULL_CUSTOMER_ID); + return NULL_CUSTOMER_ID; + } + + private NameLabelAndCustomerDetails getNameLabelAndCustomerDetails(HasId entity) { + return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity)); } private static void validateEntityCountQuery(EntityCountQuery query) { @@ -126,7 +164,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } private static void validateRelationQuery(RelationsQueryFilter queryFilter) { - if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() ==null){ + if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() == null) { throw new IncorrectParameterException("Multi-root relation query filter should contain 'multiRootEntitiesType'"); } if (queryFilter.isMultiRoot() && CollectionUtils.isEmpty(queryFilter.getMultiRootEntityIds())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6aa51bf7a3..0d4a5aa1a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -41,6 +41,7 @@ public class ModelConstants { public static final String USER_ID_PROPERTY = "user_id"; public static final String TENANT_ID_PROPERTY = "tenant_id"; public static final String CUSTOMER_ID_PROPERTY = "customer_id"; + public static final String ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String DEVICE_ID_PROPERTY = "device_id"; public static final String TITLE_PROPERTY = "title"; public static final String NAME_PROPERTY = "name"; @@ -82,6 +83,7 @@ public class ModelConstants { public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password"; //NOSONAR, the constant used to identify password column name (not password value itself) public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token"; public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token"; + public static final String USER_CREDENTIALS_ADDITIONAL_PROPERTY = "additional_info"; public static final String USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME = "user_credentials_by_user"; public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token"; @@ -287,24 +289,35 @@ public class ModelConstants { */ public static final String ENTITY_ALARM_COLUMN_FAMILY_NAME = "entity_alarm"; public static final String ALARM_COLUMN_FAMILY_NAME = "alarm"; + public static final String ALARM_VIEW_NAME = "alarm_info"; public static final String ALARM_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String ALARM_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ALARM_TYPE_PROPERTY = "type"; - public static final String ALARM_DETAILS_PROPERTY = "details"; + public static final String ALARM_DETAILS_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String ALARM_STATUS_PROPERTY = "status"; public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; + public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; - public static final String ALARM_STATUS_PROPERTY = "status"; + public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; + public static final String ALARM_ASSIGNEE_FIRST_NAME_PROPERTY = "assignee_first_name"; + public static final String ALARM_ASSIGNEE_LAST_NAME_PROPERTY = "assignee_last_name"; + public static final String ALARM_ASSIGNEE_EMAIL_PROPERTY = "assignee_email"; public static final String ALARM_START_TS_PROPERTY = "start_ts"; public static final String ALARM_END_TS_PROPERTY = "end_ts"; + public static final String ALARM_ACKNOWLEDGED_PROPERTY = "acknowledged"; public static final String ALARM_ACK_TS_PROPERTY = "ack_ts"; + public static final String ALARM_CLEARED_PROPERTY = "cleared"; public static final String ALARM_CLEAR_TS_PROPERTY = "clear_ts"; + public static final String ALARM_ASSIGN_TS_PROPERTY = "assign_ts"; public static final String ALARM_PROPAGATE_PROPERTY = "propagate"; public static final String ALARM_PROPAGATE_TO_OWNER_PROPERTY = "propagate_to_owner"; public static final String ALARM_PROPAGATE_TO_TENANT_PROPERTY = "propagate_to_tenant"; public static final String ALARM_PROPAGATE_RELATION_TYPES = "propagate_relation_types"; + public static final String ALARM_OPERATION_RESULT_PROPERTY = "operation_result"; + public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; public static final String ALARM_COMMENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index cf05ccda02..164e1518a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -25,11 +25,11 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -43,7 +43,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGN_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEARED_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; @@ -55,7 +59,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_TO import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; @@ -84,9 +87,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_SEVERITY_PROPERTY) private AlarmSeverity severity; - @Enumerated(EnumType.STRING) - @Column(name = ALARM_STATUS_PROPERTY) - private AlarmStatus status; + @Type(type="pg-uuid") + @Column(name = ALARM_ASSIGNEE_ID_PROPERTY) + private UUID assigneeId; @Column(name = ALARM_START_TS_PROPERTY) private Long startTs; @@ -94,14 +97,23 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_END_TS_PROPERTY) private Long endTs; + @Column(name = ALARM_ACKNOWLEDGED_PROPERTY) + private boolean acknowledged; + @Column(name = ALARM_ACK_TS_PROPERTY) private Long ackTs; + @Column(name = ALARM_CLEARED_PROPERTY) + private boolean cleared; + @Column(name = ALARM_CLEAR_TS_PROPERTY) private Long clearTs; + @Column(name = ALARM_ASSIGN_TS_PROPERTY) + private Long assignTs; + @Type(type = "json") - @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) + @Column(name = ModelConstants.ALARM_DETAILS_PROPERTY) private JsonNode details; @Column(name = ALARM_PROPAGATE_PROPERTY) @@ -136,7 +148,11 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.originatorType = alarm.getOriginator().getEntityType(); this.type = alarm.getType(); this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); + this.acknowledged = alarm.isAcknowledged(); + this.cleared = alarm.isCleared(); + if (alarm.getAssigneeId() != null) { + this.assigneeId = alarm.getAssigneeId().getId(); + } this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); this.propagateToTenant = alarm.isPropagateToTenant(); @@ -144,11 +160,12 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarm.getEndTs(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); } else { - this.propagateRelationTypes = null; + this.propagateRelationTypes = ""; } } @@ -162,7 +179,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.originatorType = alarmEntity.getOriginatorType(); this.type = alarmEntity.getType(); this.severity = alarmEntity.getSeverity(); - this.status = alarmEntity.getStatus(); + this.acknowledged = alarmEntity.isAcknowledged(); + this.cleared = alarmEntity.isCleared(); + this.assigneeId = alarmEntity.getAssigneeId(); this.propagate = alarmEntity.getPropagate(); this.propagateToOwner = alarmEntity.getPropagateToOwner(); this.propagateToTenant = alarmEntity.getPropagateToTenant(); @@ -170,6 +189,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarmEntity.getEndTs(); this.ackTs = alarmEntity.getAckTs(); this.clearTs = alarmEntity.getClearTs(); + this.assignTs = alarmEntity.getAssignTs(); this.details = alarmEntity.getDetails(); this.propagateRelationTypes = alarmEntity.getPropagateRelationTypes(); } @@ -186,7 +206,11 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); alarm.setType(type); alarm.setSeverity(severity); - alarm.setStatus(status); + alarm.setAcknowledged(acknowledged); + alarm.setCleared(cleared); + if (assigneeId != null) { + alarm.setAssigneeId(new UserId(assigneeId)); + } alarm.setPropagate(propagate); alarm.setPropagateToOwner(propagateToOwner); alarm.setPropagateToTenant(propagateToTenant); @@ -194,6 +218,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setEndTs(endTs); alarm.setAckTs(ackTs); alarm.setClearTs(clearTs); + alarm.setAssignTs(assignTs); alarm.setDetails(details); if (!StringUtils.isEmpty(propagateRelationTypes)) { alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java index 1218613e2a..40ecd19a02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java @@ -17,24 +17,63 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.NamedNativeQueries; +import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; +import javax.persistence.Table; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_VIEW_NAME; @Data @EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ALARM_VIEW_NAME) public class AlarmInfoEntity extends AbstractAlarmEntity { + @Column(name = ALARM_ORIGINATOR_NAME_PROPERTY) private String originatorName; + @Column(name = ALARM_ORIGINATOR_LABEL_PROPERTY) + private String originatorLabel; + @Column(name = ALARM_ASSIGNEE_FIRST_NAME_PROPERTY) + private String assigneeFirstName; + @Column(name = ALARM_ASSIGNEE_LAST_NAME_PROPERTY) + private String assigneeLastName; + @Column(name = ALARM_ASSIGNEE_EMAIL_PROPERTY) + private String assigneeEmail; + @Column(name = ALARM_STATUS_PROPERTY) + private String status; public AlarmInfoEntity() { super(); } - public AlarmInfoEntity(AlarmEntity alarmEntity) { - super(alarmEntity); - } - @Override public AlarmInfo toData() { - return new AlarmInfo(super.toAlarm(), this.originatorName); + AlarmInfo alarmInfo = new AlarmInfo(super.toAlarm()); + alarmInfo.setOriginatorName(originatorName); + alarmInfo.setOriginatorLabel(originatorLabel); + if (getAssigneeId() != null) { + alarmInfo.setAssignee(new AlarmAssignee(new UserId(getAssigneeId()), assigneeFirstName, assigneeLastName, assigneeEmail)); + } + return alarmInfo; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java index ff16525235..282bca89e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.ToData; import javax.persistence.Column; @@ -30,6 +31,7 @@ import javax.persistence.IdClass; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.ASSIGNEE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CREATED_TIME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ALARM_COLUMN_FAMILY_NAME; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java index 97868a1e07..5229c2cad6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; @@ -50,6 +52,10 @@ public final class UserCredentialsEntity extends BaseSqlEntity @Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true) private String resetToken; + @Type(type = "json") + @Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY) + private JsonNode additionalInfo; + public UserCredentialsEntity() { super(); } @@ -66,6 +72,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity this.password = userCredentials.getPassword(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); + this.additionalInfo = userCredentials.getAdditionalInfo(); } @Override @@ -79,6 +86,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity userCredentials.setPassword(password); userCredentials.setActivateToken(activateToken); userCredentials.setResetToken(resetToken); + userCredentials.setAdditionalInfo(additionalInfo); return userCredentials; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 43e9069fac..49e0257daf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -18,10 +18,11 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -39,7 +40,13 @@ public interface AlarmRepository extends JpaRepository { @Param("alarmType") String alarmType, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType AND a.cleared = FALSE ORDER BY a.createdTime DESC") + List findLatestActiveByOriginatorAndType(@Param("originatorId") UUID originatorId, + @Param("alarmType") String alarmType, + Pageable pageable); + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + "AND ea.tenantId = :tenantId " + @@ -47,14 +54,16 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + "SELECT count(a) " + //alarms with relations only - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + "AND ea.tenantId = :tenantId " + @@ -62,7 +71,9 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -71,51 +82,69 @@ public interface AlarmRepository extends JpaRepository { @Param("affectedEntityType") String affectedEntityType, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ", countQuery = "" + "SELECT count(a) " + - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") Page findAllAlarms(@Param("tenantId") UUID tenantId, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + "SELECT count(a) " + - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -123,7 +152,11 @@ public interface AlarmRepository extends JpaRepository { @Param("customerId") UUID customerId, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -133,13 +166,49 @@ public interface AlarmRepository extends JpaRepository { "AND ea.tenantId = :tenantId " + "AND ea.entityId = :affectedEntityId " + "AND ea.entityType = :affectedEntityType " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId))") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, - @Param("alarmStatuses") Set alarmStatuses); + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId); @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.createdTime < :time AND a.endTs < :time") Page findAlarmsIdsByEndTsBeforeAndTenantId(@Param("time") Long time, @Param("tenantId") UUID tenantId, Pageable pageable); + @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") + AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); + + @Procedure(procedureName = "create_or_update_active_alarm") + String createOrUpdateActiveAlarm(@Param("t_id") UUID tenantId, @Param("c_id") UUID customerId, + @Param("a_id") UUID alarmId, @Param("a_created_ts") long createdTime, + @Param("a_o_id") UUID originatorId, @Param("a_o_type") int originatorType, + @Param("a_type") String type, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes, + @Param("a_creation_enabled") boolean alarmCreationEnabled); + + @Procedure(procedureName = "update_alarm") + String updateAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes); + + @Procedure(procedureName = "acknowledge_alarm") + String acknowledgeAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts); + + @Procedure(procedureName = "clear_alarm") + String clearAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts, @Param("a_details") String details); + + @Procedure(procedureName = "assign_alarm") + String assignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("u_id") UUID userId, @Param("a_ts") long assignTime); + + @Procedure(procedureName = "unassign_alarm") + String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index b8f508fef8..f1abd07d95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -15,39 +15,54 @@ */ package org.thingsboard.server.dao.sql.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.EntityAlarmEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.query.AlarmQueryRepository; import org.thingsboard.server.dao.util.SqlDao; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -87,6 +102,15 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + List latest = alarmRepository.findLatestActiveByOriginatorAndType( + originator.getId(), + type, + PageRequest.of(0, 1)); + return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); + } + @Override public ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { return service.submit(() -> findLatestByOriginatorAndType(tenantId, originator, type)); @@ -97,6 +121,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return findById(tenantId, key); } + @Override + public AlarmInfo findAlarmInfoById(TenantId tenantId, UUID key) { + return DaoUtil.getData(alarmRepository.findAlarmInfoById(tenantId.getId(), key)); + } + @Override public ListenableFuture findAlarmByIdAsync(TenantId tenantId, UUID key) { return findByIdAsync(tenantId, key); @@ -106,11 +135,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A public PageData findAlarms(TenantId tenantId, AlarmQuery query) { log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink()); EntityId affectedEntity = query.getAffectedEntityId(); - Set statusSet = null; - if (query.getSearchStatus() != null) { - statusSet = query.getSearchStatus().getStatuses(); - } else if (query.getStatus() != null) { - statusSet = Collections.singleton(query.getStatus()); + AlarmStatusFilter asf = AlarmStatusFilter.from(query); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); } if (affectedEntity != null) { return DaoUtil.toPageData( @@ -120,7 +148,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A affectedEntity.getEntityType().name(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -131,7 +163,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A tenantId.getId(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -142,11 +178,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public PageData findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink()); - Set statusSet = null; - if (query.getSearchStatus() != null) { - statusSet = query.getSearchStatus().getStatuses(); - } else if (query.getStatus() != null) { - statusSet = Collections.singleton(query.getStatus()); + AlarmStatusFilter asf = AlarmStatusFilter.from(query); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); } return DaoUtil.toPageData( alarmRepository.findCustomerAlarms( @@ -154,7 +189,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A customerId.getId(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -167,8 +206,13 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set statuses) { - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); + public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId) { + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId); } @Override @@ -195,6 +239,174 @@ public class JpaAlarmDao extends JpaAbstractDao implements A entityAlarmRepository.deleteByEntityId(entityId.getId()); } + @Override + public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm( + request.getTenantId().getId(), + request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID, + UUID.randomUUID(), + System.currentTimeMillis(), + request.getOriginator().getId(), + request.getOriginator().getEntityType().ordinal(), + request.getType(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap), + alarmCreationEnabled + )); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.updateAlarm( + request.getTenantId().getId(), + request.getAlarmId().getId(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap) + )); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) { + return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { + return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, getDetailsAsString(details))); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId id, UserId assigneeId, long assignTime) { + return toAlarmApiResult(alarmRepository.assignAlarm(tenantId.getId(), id.getId(), assigneeId.getId(), assignTime)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId id, long unassignTime) { + return toAlarmApiResult(alarmRepository.unassignAlarm(tenantId.getId(), id.getId(), unassignTime)); + } + + @NotNull + private static String getPropagationTypes(AlarmPropagationInfo ap) { + String propagateRelationTypes; + if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { + propagateRelationTypes = String.join(",", ap.getPropagateRelationTypes()); + } else { + propagateRelationTypes = ""; + } + return propagateRelationTypes; + } + + private static AlarmPropagationInfo getSafePropagationInfo(AlarmPropagationInfo ap) { + return ap != null ? ap : AlarmPropagationInfo.EMPTY; + } + + private static String getDetailsAsString(JsonNode details) { + var detailsStr = JacksonUtil.toString(details); + if (StringUtils.isEmpty(detailsStr)) { + detailsStr = "{}"; + } + return detailsStr; + } + + private AlarmApiCallResult toAlarmApiResult(String str) { + var json = JacksonUtil.toJsonNode(str); + var result = AlarmApiCallResult.builder(); + boolean success = json.get("success").asBoolean(); + result.successful(success); + if (success) { + boolean modified = false; + boolean created = false; + boolean cleared = false; + if (json.has("modified")) { + modified = json.get("modified").asBoolean(); + } + + if (json.has("created")) { + created = json.get("created").asBoolean(); + } + + if (json.has("cleared")) { + cleared = json.get("cleared").asBoolean(); + } + result.created(created); + result.cleared(cleared); + result.modified(created || cleared || modified); + if (json.has("alarm") && !json.get("alarm").isNull()) { + result.alarm(toAlarmInfo(json.get("alarm"))); + } + if (json.has("old") && !json.get("old").isNull()) { + result.old(toAlarm(json.get("old"))); + } + } + return result.build(); + } + + private AlarmInfo toAlarmInfo(JsonNode json) { + AlarmInfo alarmInfo = new AlarmInfo(toAlarm(json)); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY).ifPresent(alarmInfo::setOriginatorName); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY).ifPresent(alarmInfo::setOriginatorLabel); + if (alarmInfo.getAssigneeId() != null) { + var assigneeBuilder = AlarmAssignee.builder().id(alarmInfo.getAssigneeId()); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY).ifPresent(assigneeBuilder::firstName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY).ifPresent(assigneeBuilder::lastName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY).ifPresent(assigneeBuilder::email); + alarmInfo.setAssignee(assigneeBuilder.build()); + } + return alarmInfo; + } + + private Alarm toAlarm(JsonNode json) { + Alarm alarm = new Alarm(new AlarmId(UUID.fromString(json.get(ModelConstants.ID_PROPERTY).asText()))); + alarm.setCreatedTime(json.get(ModelConstants.CREATED_TIME_PROPERTY).asLong()); + getSafe(json, ModelConstants.TENANT_ID_COLUMN).ifPresent(s -> alarm.setTenantId(TenantId.fromUUID(UUID.fromString(s)))); + getSafe(json, ModelConstants.CUSTOMER_ID_PROPERTY).ifPresent(s -> alarm.setCustomerId(new CustomerId(UUID.fromString(s)))); + getSafe(json, ModelConstants.ASSIGNEE_ID_PROPERTY).ifPresent(s -> alarm.setAssigneeId(new UserId(UUID.fromString(s)))); + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid( + json.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY).asInt(), + json.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY).asText())); + getSafe(json, ModelConstants.ALARM_TYPE_PROPERTY).ifPresent(alarm::setType); + getSafe(json, ModelConstants.ALARM_SEVERITY_PROPERTY).map(AlarmSeverity::valueOf).ifPresent(alarm::setSeverity); + alarm.setAcknowledged(json.get(ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY).asBoolean()); + alarm.setCleared(json.get(ModelConstants.ALARM_CLEARED_PROPERTY).asBoolean()); + alarm.setPropagate(json.get(ModelConstants.ALARM_PROPAGATE_PROPERTY).asBoolean()); + alarm.setPropagateToOwner(json.get(ModelConstants.ALARM_PROPAGATE_TO_OWNER_PROPERTY).asBoolean()); + alarm.setPropagateToTenant(json.get(ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY).asBoolean()); + alarm.setStartTs(json.get(ModelConstants.ALARM_START_TS_PROPERTY).asLong()); + alarm.setEndTs(json.get(ModelConstants.ALARM_END_TS_PROPERTY).asLong()); + alarm.setAckTs(json.get(ModelConstants.ALARM_ACK_TS_PROPERTY).asLong()); + alarm.setClearTs(json.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY).asLong()); + alarm.setAssignTs(json.get(ModelConstants.ALARM_ASSIGN_TS_PROPERTY).asLong()); + getSafe(json, ModelConstants.ALARM_DETAILS_PROPERTY).map(JacksonUtil::toJsonNode).ifPresent(alarm::setDetails); + alarm.setPropagateRelationTypes(getSafe(json, ModelConstants.ALARM_PROPAGATE_RELATION_TYPES).filter(StringUtils::isNoneEmpty) + .map(s -> Arrays.asList(s.split(","))).orElse(Collections.emptyList())); + return alarm; + } + + private static Optional getSafe(JsonNode json, String fieldName) { + if (json.has(fieldName)) { + var element = json.get(fieldName); + if (element.isNull() || !element.isTextual()) { + return Optional.empty(); + } else { + return Optional.of(element.asText()); + } + } else { + return Optional.empty(); + } + } + @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index 7943ce5178..a07887fb57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -68,6 +70,7 @@ public class AlarmDataAdapter { alarm.setCreatedTime((long) row.get(ModelConstants.CREATED_TIME_PROPERTY)); alarm.setAckTs((long) row.get(ModelConstants.ALARM_ACK_TS_PROPERTY)); alarm.setClearTs((long) row.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY)); + alarm.setAssignTs((long) row.get(ModelConstants.ALARM_ASSIGN_TS_PROPERTY)); alarm.setStartTs((long) row.get(ModelConstants.ALARM_START_TS_PROPERTY)); alarm.setEndTs((long) row.get(ModelConstants.ALARM_END_TS_PROPERTY)); Object additionalInfo = row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY); @@ -81,12 +84,23 @@ public class AlarmDataAdapter { EntityType originatorType = EntityType.values()[(int) row.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY)]; UUID originatorId = (UUID) row.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); + Object assigneeIdObj = row.get(ModelConstants.ASSIGNEE_ID_PROPERTY); + String assigneeFirstName = null; + String assigneeLastName = null; + String assigneeEmail = null; + if (assigneeIdObj != null) { + alarm.setAssigneeId(new UserId((UUID) row.get(ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY))); + assigneeFirstName = (String) row.get(ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); + assigneeLastName = (String) row.get(ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); + assigneeEmail = (String) row.get(ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); + } alarm.setPropagate((boolean) row.get(ModelConstants.ALARM_PROPAGATE_PROPERTY)); alarm.setPropagateToOwner((boolean) row.get(ModelConstants.ALARM_PROPAGATE_TO_OWNER_PROPERTY)); alarm.setPropagateToTenant((boolean) row.get(ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY)); alarm.setType(row.get(ModelConstants.ALARM_TYPE_PROPERTY).toString()); alarm.setSeverity(AlarmSeverity.valueOf(row.get(ModelConstants.ALARM_SEVERITY_PROPERTY).toString())); - alarm.setStatus(AlarmStatus.valueOf(row.get(ModelConstants.ALARM_STATUS_PROPERTY).toString())); + alarm.setAcknowledged((boolean) row.get(ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY)); + alarm.setCleared((boolean) row.get(ModelConstants.ALARM_CLEARED_PROPERTY)); alarm.setTenantId(TenantId.fromUUID((UUID) row.get(ModelConstants.TENANT_ID_PROPERTY))); Object customerIdObj = row.get(ModelConstants.CUSTOMER_ID_PROPERTY); CustomerId customerId = customerIdObj != null ? new CustomerId((UUID) customerIdObj) : null; @@ -105,7 +119,17 @@ public class AlarmDataAdapter { EntityId entityId = entityIdMap.get(entityUuid); Object originatorNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY); String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; - return new AlarmData(alarm, originatorName, entityId); + Object originatorLabelObj = row.get(ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); + String originatorLabel = originatorLabelObj != null ? originatorLabelObj.toString() : null; + + AlarmData alarmData = new AlarmData(alarm, entityId); + alarmData.setOriginatorName(originatorName); + alarmData.setOriginatorLabel(originatorLabel); + if (alarm.getAssigneeId() != null) { + alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); + } + + return alarmData; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 543b8549c4..ecf9c1c95f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -19,11 +19,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -51,46 +54,44 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { private static final Map alarmFieldColumnMap = new HashMap<>(); + private static final String ASSIGNEE_EMAIL_KEY = "assigneeEmail"; + private static final String ASSIGNEE_LAST_NAME_KEY = "assigneeLastName"; + private static final String ASSIGNEE_FIRST_NAME_KEY = "assigneeFirstName"; + private static final String ASSIGNEE_ID_KEY = "assigneeId"; + private static final String ASSIGNEE_KEY = "assignee"; + static { alarmFieldColumnMap.put("createdTime", ModelConstants.CREATED_TIME_PROPERTY); alarmFieldColumnMap.put("ackTs", ModelConstants.ALARM_ACK_TS_PROPERTY); alarmFieldColumnMap.put("ackTime", ModelConstants.ALARM_ACK_TS_PROPERTY); alarmFieldColumnMap.put("clearTs", ModelConstants.ALARM_CLEAR_TS_PROPERTY); alarmFieldColumnMap.put("clearTime", ModelConstants.ALARM_CLEAR_TS_PROPERTY); + alarmFieldColumnMap.put("assignTime", ModelConstants.ALARM_ASSIGN_TS_PROPERTY); alarmFieldColumnMap.put("details", ModelConstants.ADDITIONAL_INFO_PROPERTY); alarmFieldColumnMap.put("endTs", ModelConstants.ALARM_END_TS_PROPERTY); alarmFieldColumnMap.put("endTime", ModelConstants.ALARM_END_TS_PROPERTY); alarmFieldColumnMap.put("startTs", ModelConstants.ALARM_START_TS_PROPERTY); alarmFieldColumnMap.put("startTime", ModelConstants.ALARM_START_TS_PROPERTY); - alarmFieldColumnMap.put("status", ModelConstants.ALARM_STATUS_PROPERTY); + alarmFieldColumnMap.put("acknowledged", ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY); + alarmFieldColumnMap.put("cleared", ModelConstants.ALARM_CLEARED_PROPERTY); alarmFieldColumnMap.put("type", ModelConstants.ALARM_TYPE_PROPERTY); alarmFieldColumnMap.put("severity", ModelConstants.ALARM_SEVERITY_PROPERTY); alarmFieldColumnMap.put("originatorId", ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); alarmFieldColumnMap.put("originatorType", ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY); - alarmFieldColumnMap.put("originator", "originator_name"); + alarmFieldColumnMap.put(ASSIGNEE_ID_KEY, ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY); + alarmFieldColumnMap.put("originator", ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY); + alarmFieldColumnMap.put("originatorLabel", ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_FIRST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_LAST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_EMAIL_KEY, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); } - private static final String SELECT_ORIGINATOR_NAME = " COALESCE(CASE" + - " WHEN a.originator_type = " + EntityType.TENANT.ordinal() + - " THEN (select title from tenant where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.CUSTOMER.ordinal() + - " THEN (select title from customer where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.USER.ordinal() + - " THEN (select email from tb_user where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.DASHBOARD.ordinal() + - " THEN (select title from dashboard where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.ASSET.ordinal() + - " THEN (select name from asset where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.DEVICE.ordinal() + - " THEN (select name from device where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.ENTITY_VIEW.ordinal() + - " THEN (select name from entity_view where id = a.originator_id)" + - " END, 'Deleted') as originator_name"; - private static final String FIELDS_SELECTION = "select a.id as id," + " a.created_time as created_time," + " a.ack_ts as ack_ts," + " a.clear_ts as clear_ts," + + " a.assign_ts as assign_ts," + + " a.assignee_id as assignee_id," + " a.additional_info as additional_info," + " a.end_ts as end_ts," + " a.originator_id as originator_id," + @@ -100,13 +101,19 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.propagate_to_tenant as propagate_to_tenant," + " a.severity as severity," + " a.start_ts as start_ts," + - " a.status as status, " + " a.tenant_id as tenant_id, " + " a.customer_id as customer_id, " + " a.propagate_relation_types as propagate_relation_types, " + - " a.type as type," + SELECT_ORIGINATOR_NAME + ", "; + " a.type as type, " + + " a.originator_name as originator_name, " + + " a.originator_label as originator_label, " + + " a.assignee_first_name as assignee_first_name, " + + " a.assignee_last_name as assignee_last_name, " + + " a.assignee_email as assignee_email, " + + " a.cleared as cleared, " + + " a.acknowledged as acknowledged, "; - private static final String JOIN_ENTITY_ALARMS = "inner join entity_alarm ea on a.id = ea.alarm_id"; + private static final String JOIN_ENTITY_ALARMS = "inner join entity_alarm ea on a.id = ea.alarm_id "; protected final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -121,12 +128,12 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { @Override public PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { - return transactionTemplate.execute(status -> { + return transactionTemplate.execute(trStatus -> { AlarmDataPageLink pageLink = query.getPageLink(); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); ctx.addUuidListParameter("entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); StringBuilder selectPart = new StringBuilder(FIELDS_SELECTION); - StringBuilder fromPart = new StringBuilder(" from alarm a "); + StringBuilder fromPart = new StringBuilder(" from alarm_info a "); StringBuilder wherePart = new StringBuilder(" where "); StringBuilder sortPart = new StringBuilder(" order by "); StringBuilder joinPart = new StringBuilder(); @@ -140,9 +147,25 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { selectPart.append(" a.originator_id as entity_id "); } EntityDataSortOrder sortOrder = pageLink.getSortOrder(); - String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); + + List alarmFields = new ArrayList<>(); + for (EntityKey key : query.getAlarmFields()) { + if (EntityKeyType.ALARM_FIELD.equals(key.getType()) && ASSIGNEE_KEY.equalsIgnoreCase(key.getKey())) { + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_ID_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_FIRST_NAME_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_LAST_NAME_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_EMAIL_KEY)); + } else { + alarmFields.add(key); + } + } + + String textSearchQuery = buildTextSearchQuery(ctx, alarmFields, pageLink.getTextSearch()); if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { String sortOrderKey = sortOrder.getKey().getKey(); + if ("status".equalsIgnoreCase(sortOrderKey)) { + selectPart.append(", a.status as status "); + } sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) .append(" ").append(sortOrder.getDirection().name()); if (pageLink.isSearchPropagatedAlarms()) { @@ -225,14 +248,25 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { wherePart.append("a.severity in (:alarmSeverities)"); } - if (pageLink.getStatusList() != null && !pageLink.getStatusList().isEmpty()) { - Set statusSet = toStatusSet(pageLink.getStatusList()); - if (!statusSet.isEmpty()) { + AlarmStatusFilter asf = AlarmStatusFilter.fromList(pageLink.getStatusList()); + if (asf.hasAnyFilter()) { + if (asf.hasAckFilter()) { addAndIfNeeded(wherePart, addAnd); addAnd = true; - ctx.addStringListParameter("alarmStatuses", statusSet.stream().map(AlarmStatus::name).collect(Collectors.toList())); - wherePart.append(" a.status in (:alarmStatuses)"); + ctx.addBooleanParameter("ackStatus", asf.getAckFilter()); + wherePart.append(" a.acknowledged = :ackStatus"); } + if (asf.hasClearFilter()) { + addAndIfNeeded(wherePart, addAnd); + // addAnd = true; // not needed but stored as an example if someone adds new conditions + ctx.addBooleanParameter("clearStatus", asf.getClearFilter()); + wherePart.append(" a.cleared = :clearStatus"); + } + } + + if (pageLink.getAssigneeId() != null) { + ctx.addUuidParameter("assigneeId", pageLink.getAssigneeId().getId()); + wherePart.append(" a.assignee_id = :assigneeId"); } String mainQuery = String.format("%s%s", selectPart, fromPart); @@ -295,37 +329,6 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { return permissionsQuery.toString(); } - private Set toStatusSet(List statusList) { - Set result = new HashSet<>(); - for (AlarmSearchStatus searchStatus : statusList) { - switch (searchStatus) { - case ACK: - result.add(AlarmStatus.ACTIVE_ACK); - result.add(AlarmStatus.CLEARED_ACK); - break; - case UNACK: - result.add(AlarmStatus.ACTIVE_UNACK); - result.add(AlarmStatus.CLEARED_UNACK); - break; - case CLEARED: - result.add(AlarmStatus.CLEARED_ACK); - result.add(AlarmStatus.CLEARED_UNACK); - break; - case ACTIVE: - result.add(AlarmStatus.ACTIVE_ACK); - result.add(AlarmStatus.ACTIVE_UNACK); - break; - default: - break; - } - if (searchStatus == AlarmSearchStatus.ANY || result.size() == AlarmStatus.values().length) { - result.clear(); - return result; - } - } - return result; - } - private void addAndIfNeeded(StringBuilder wherePart, boolean addAnd) { if (addAnd) { wherePart.append(" and "); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 2710b84d03..c59efe69ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -127,7 +127,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentials.setEnabled(false); userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); userCredentials.setUserId(new UserId(savedUser.getUuidId())); - saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + userCredentialsDao.save(user.getTenantId(), userCredentials); } return savedUser; } @@ -157,7 +158,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { log.trace("Executing saveUserCredentials [{}]", userCredentials); userCredentialsValidator.validate(userCredentials, data -> tenantId); - return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials); + return userCredentialsDao.save(tenantId, userCredentials); } @Override @@ -175,7 +176,9 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentials.setEnabled(true); userCredentials.setActivateToken(null); userCredentials.setPassword(password); - + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } return saveUserCredentials(tenantId, userCredentials); } @@ -211,7 +214,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentialsValidator.validate(userCredentials, data -> tenantId); userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); userCredentials.setId(null); - return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials); + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + return userCredentialsDao.save(tenantId, userCredentials); } @Override @@ -347,17 +353,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic return failedLoginAttempts; } - private UserCredentials saveUserCredentialsAndPasswordHistory(TenantId tenantId, UserCredentials userCredentials) { - UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); - User user = findUserById(tenantId, userCredentials.getUserId()); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(user, userCredentials); - } - return result; - } - - private void updatePasswordHistory(User user, UserCredentials userCredentials) { - JsonNode additionalInfo = user.getAdditionalInfo(); + private void updatePasswordHistory(UserCredentials userCredentials) { + JsonNode additionalInfo = userCredentials.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } @@ -378,8 +375,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userPasswordHistoryJson = JacksonUtil.valueToTree(userPasswordHistoryMap); ((ObjectNode) additionalInfo).set(USER_PASSWORD_HISTORY, userPasswordHistoryJson); } - user.setAdditionalInfo(additionalInfo); - saveUser(user); + userCredentials.setAdditionalInfo(additionalInfo); } private final PaginatedRemover tenantAdminsRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 8f742b6b1c..fb6940e2cb 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,12 +20,20 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_alarm_tenant_status_created_time ON alarm(tenant_id, status, created_time DESC); +-- Drop index by 'status' column and replace with new one that has only active alarms; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type) WHERE cleared = false; CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_entity_alarm_created_time ON entity_alarm(tenant_id, entity_id, created_time DESC); +-- Cover index by alarm type to optimize propagated alarm queries; +CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm +USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id); + CREATE INDEX IF NOT EXISTS idx_entity_alarm_alarm_id ON entity_alarm(alarm_id); CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 373328f966..601b93f92f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -52,13 +52,16 @@ CREATE TABLE IF NOT EXISTS alarm ( propagate boolean, severity varchar(255), start_ts bigint, - status varchar(255), + assign_ts bigint, + assignee_id uuid, tenant_id uuid, customer_id uuid, propagate_relation_types varchar, type varchar(255), propagate_to_owner boolean, - propagate_to_tenant boolean + propagate_to_tenant boolean, + acknowledged boolean, + cleared boolean ); CREATE TABLE IF NOT EXISTS alarm_comment ( @@ -427,13 +430,6 @@ CREATE TABLE IF NOT EXISTS relation ( additional_info varchar, CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type) ); --- ) PARTITION BY LIST (relation_type_group); --- --- CREATE TABLE other_relations PARTITION OF relation DEFAULT; --- CREATE TABLE common_relations PARTITION OF relation FOR VALUES IN ('COMMON'); --- CREATE TABLE alarm_relations PARTITION OF relation FOR VALUES IN ('ALARM'); --- CREATE TABLE dashboard_relations PARTITION OF relation FOR VALUES IN ('DASHBOARD'); --- CREATE TABLE rule_relations PARTITION OF relation FOR VALUES IN ('RULE_CHAIN', 'RULE_NODE'); CREATE TABLE IF NOT EXISTS tb_user ( id uuid NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, @@ -488,7 +484,8 @@ CREATE TABLE IF NOT EXISTS user_credentials ( enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id uuid UNIQUE + user_id uuid UNIQUE, + additional_info varchar DEFAULT '{}' ); CREATE TABLE IF NOT EXISTS widget_type ( @@ -858,3 +855,245 @@ CREATE TABLE IF NOT EXISTS user_settings ( settings varchar(10000), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); + +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(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(label, name) from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select COALESCE(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(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))::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/service/BaseAlarmCommentServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java index 00eba295ae..688793d6c6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java @@ -58,7 +58,7 @@ public abstract class BaseAlarmCommentServiceTest extends AbstractServiceTest { alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased())) .type(TEST_ALARM) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) .startTs(System.currentTimeMillis()).build(); alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index dca45f100e..b7f5135533 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -20,20 +20,25 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -45,8 +50,9 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; -import org.thingsboard.common.util.JacksonUtil; import java.util.Arrays; import java.util.Collections; @@ -56,6 +62,11 @@ import java.util.concurrent.ExecutionException; public abstract class BaseAlarmServiceTest extends AbstractServiceTest { public static final String TEST_ALARM = "TEST_ALARM"; + + private static final String TEST_TENANT_EMAIL = "testtenant@thingsboard.org"; + private static final String TEST_TENANT_FIRST_NAME = "testtenantfirstname"; + private static final String TEST_TENANT_LAST_NAME = "testtenantlastname"; + private TenantId tenantId; @Before @@ -83,12 +94,12 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); Alarm created = result.getAlarm(); Assert.assertNotNull(created); @@ -107,7 +118,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(0L, created.getAckTs()); Assert.assertEquals(0L, created.getClearTs()); - Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + Alarm fetched = alarmService.findAlarmInfoById(tenantId, created.getId()); Assert.assertEquals(created, fetched); } @@ -121,14 +132,13 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(false) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); // Check child relation PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -139,7 +149,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -152,7 +162,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(0, alarms.getData().size()); created.setPropagate(true); - result = alarmService.createOrUpdateAlarm(created); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); created = result.getAlarm(); // Check child relation @@ -164,7 +174,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -175,10 +185,10 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); - alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -188,7 +198,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check not existing relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -200,8 +210,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), null, System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -211,7 +221,77 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + } + + @Test + public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { + + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(relationService.saveRelation(tenantId, relation)); + + long ts = System.currentTimeMillis(); + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + + AlarmInfo created = result.getAlarm(); + + User tenantUser = new User(); + tenantUser.setTenantId(tenantId); + tenantUser.setAuthority(Authority.TENANT_ADMIN); + tenantUser.setEmail(TEST_TENANT_EMAIL); + tenantUser.setFirstName(TEST_TENANT_FIRST_NAME); + tenantUser.setLastName(TEST_TENANT_LAST_NAME); + tenantUser = userService.saveUser(tenantUser); + + Assert.assertNotNull(tenantUser); + + AlarmApiCallResult assignmentResult = alarmService.assignAlarm(tenantId, created.getId(), tenantUser.getId(), ts); + created = assignmentResult.getAlarm(); + + PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .assigneeId(tenantUser.getId()) + .fetchOriginator(true) + .pageLink(new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, alarms.getData().get(0)); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setAssigneeId(tenantUser.getId()); + + PageData assignedAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(created.getOriginator())); + Assert.assertNotNull(assignedAlarms.getData()); + Assert.assertEquals(1, assignedAlarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(assignedAlarms.getData().get(0))); + + User tenantUser2 = new User(); + tenantUser2.setTenantId(tenantId); + tenantUser2.setAuthority(Authority.TENANT_ADMIN); + tenantUser2.setEmail(2 + TEST_TENANT_EMAIL); + tenantUser2.setFirstName(TEST_TENANT_FIRST_NAME); + tenantUser2.setLastName(TEST_TENANT_LAST_NAME); + tenantUser2 = userService.saveUser(tenantUser2); + + Assert.assertNotNull(tenantUser2); + pageLink.setAssigneeId(tenantUser2.getId()); + + PageData assignedToNonExistingUserAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(created.getOriginator())); + Assert.assertNotNull(assignedToNonExistingUserAlarms.getData()); + Assert.assertTrue(assignedToNonExistingUserAlarms.getData().isEmpty()); + } @Test @@ -235,23 +315,23 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { customerDevice = deviceService.saveDevice(customerDevice); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo tenantAlarm = result.getAlarm(); - Alarm deviceAlarm = Alarm.builder().tenantId(tenantId) + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(deviceAlarm); - deviceAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo deviceAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -269,7 +349,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerDevice.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(deviceAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(deviceAlarm, new AlarmInfo(customerAlarms.getData().get(0))); PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(tenantDevice.getId()) @@ -279,7 +359,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(tenantAlarm, alarms.getData().get(0)); + Assert.assertEquals(tenantAlarm, new AlarmInfo(alarms.getData().get(0))); } @Test @@ -311,23 +391,21 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { relationService.saveRelation(tenantId, relation); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type("Not Propagated") - .propagate(false) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); - Alarm customerAlarm = Alarm.builder().tenantId(tenantId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type("Propagated") - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(customerAlarm); - customerAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo customerAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -343,7 +421,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { //TEST that propagated alarms are visible on the asset level. PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerAsset.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(customerAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(customerAlarm, new AlarmInfo(customerAlarms.getData().get(0))); } @Test @@ -361,24 +439,24 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(device.getId()) .type("Propagated To Tenant") - .propagateToTenant(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagateToTenant(true).build()) + .startTs(ts).build()); + AlarmInfo tenantAlarm = result.getAlarm(); - Alarm customerAlarm = Alarm.builder().tenantId(tenantId) + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(device.getId()) .type("Propagated to Customer") - .propagate(false) - .propagateToOwner(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(customerAlarm); - customerAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagateToOwner(true).build()) + .startTs(ts).build()); + AlarmInfo customerAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -389,24 +467,24 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setStatusList(Collections.singletonList(AlarmSearchStatus.ACTIVE)); //TEST that propagated alarms are visible on the asset level. PageData tenantAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(tenantId)); Assert.assertEquals(1, tenantAlarms.getData().size()); - Assert.assertEquals(tenantAlarm, tenantAlarms.getData().get(0)); + Assert.assertEquals(tenantAlarm, new AlarmInfo(tenantAlarms.getData().get(0))); //TEST that propagated alarms are visible on the asset level. PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customer.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(customerAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(customerAlarm, new AlarmInfo(customerAlarms.getData().get(0))); } - private AlarmDataQuery toQuery(AlarmDataPageLink pageLink){ + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink) { return toQuery(pageLink, Collections.emptyList()); } - private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields){ + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields) { return new AlarmDataQuery(new DeviceTypeFilter(), pageLink, null, null, null, alarmFields); } @@ -425,45 +503,41 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { customerDevice = deviceService.saveDevice(customerDevice); // no one alarms was created - Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); - Alarm alarm1 = Alarm.builder() + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.MAJOR) - .status(AlarmStatus.ACTIVE_UNACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm(); - alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get(); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm1 = result.getAlarm(); + alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null); - Alarm alarm2 = Alarm.builder() + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.MINOR) - .status(AlarmStatus.ACTIVE_ACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm(); - alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get(); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm2 = result.getAlarm(); + alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis()); + alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null); - Alarm alarm3 = Alarm.builder() + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.CRITICAL) - .status(AlarmStatus.ACTIVE_ACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm(); - - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null)); - Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK)); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm3 = result.getAlarm(); + alarmService.acknowledgeAlarm(tenantId, alarm3.getId(), System.currentTimeMillis()); + + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null, null)); + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK, null)); } @Test @@ -479,15 +553,13 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation2).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(false) .severity(AlarmSeverity.CRITICAL) - .status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -504,7 +576,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); pageLink.setPage(0); pageLink.setPageSize(10); @@ -519,17 +591,17 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); pageLink.setSearchPropagatedAlarms(true); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check child relation created.setPropagate(true); - result = alarmService.createOrUpdateAlarm(created); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); created = result.getAlarm(); // Check child relation @@ -546,7 +618,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation pageLink.setPage(0); @@ -562,37 +634,40 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); PageData alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId2) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); pageLink.setPage(0); pageLink.setPageSize(10); @@ -607,10 +682,9 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); - alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm(); pageLink.setPage(0); pageLink.setPageSize(10); @@ -625,7 +699,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); } @Test @@ -635,17 +709,17 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); + Assert.assertTrue(relationService.saveRelation(tenantId, relation)); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -655,7 +729,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -666,7 +740,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java index ef1d8fe737..ba3fe61f8a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java @@ -76,7 +76,6 @@ public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest { alarm.setPropagate(true); alarm.setStartTs(System.currentTimeMillis()); alarm.setEndTs(System.currentTimeMillis()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); alarmDao.save(TenantId.fromUUID(tenantId), alarm); } private void saveAlarmComment(UUID id, UUID alarmId, UUID userId, AlarmCommentType type) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index 45af699f67..5fbc2fd627 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -19,21 +19,31 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Created by Valerii Sosliuk on 5/21/2017. @@ -48,28 +58,235 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void testFindLatestByOriginatorAndType() throws ExecutionException, InterruptedException, TimeoutException { log.info("Current system time in millis = {}", System.currentTimeMillis()); - UUID tenantId = UUID.fromString("d4b68f40-3e96-11e7-a884-898080180d6b"); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); UUID originator2Id = UUID.fromString("d4b68f42-3e96-11e7-a884-898080180d6b"); UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); UUID alarm2Id = UUID.fromString("d4b68f44-3e96-11e7-a884-898080180d6b"); UUID alarm3Id = UUID.fromString("d4b68f45-3e96-11e7-a884-898080180d6b"); - int alarmCountBeforeSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + // The find method does not filter by tenant. It is just using the tenantId for rate limits if any. + var alarmsBeforeSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountBeforeSave = alarmsBeforeSave.size(); + saveAlarm(alarm1Id, tenantId.getId(), originator1Id, "TEST_ALARM"); //The timestamp of the startTime should be different in order for test to always work Thread.sleep(1); - saveAlarm(alarm2Id, tenantId, originator1Id, "TEST_ALARM"); - saveAlarm(alarm3Id, tenantId, originator2Id, "TEST_ALARM"); - int alarmCountAfterSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - assertEquals(3, alarmCountAfterSave - alarmCountBeforeSave); + saveAlarm(alarm2Id, tenantId.getId(), originator1Id, "TEST_ALARM"); + saveAlarm(alarm3Id, tenantId.getId(), originator2Id, "TEST_ALARM"); + var alarmsAfterSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountAfterSave = alarmsAfterSave.size(); + int diff = alarmCountAfterSave - alarmCountBeforeSave; + if (diff != 3) { + System.out.println("test"); + } + assertEquals(3, diff); ListenableFuture future = alarmDao - .findLatestByOriginatorAndTypeAsync(TenantId.fromUUID(tenantId), new DeviceId(originator1Id), "TEST_ALARM"); + .findLatestByOriginatorAndTypeAsync(tenantId, new DeviceId(originator1Id), "TEST_ALARM"); Alarm alarm = future.get(30, TimeUnit.SECONDS); assertNotNull(alarm); assertEquals(alarm2Id, alarm.getId().getId()); } - private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { + @Test + public void createOrUpdateActiveAlarm() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + UUID newAlarmId = result.getAlarm().getUuidId(); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, result.getAlarm().getUuidId()); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE2") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + } + + @Test + public void testCantCreateAlarmIfCreateIsDisabled() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, false); + assertFalse(result.isSuccessful()); + } + + @Test + public void testAckAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long ackTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs + 1); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + } + + @Test + public void testClearAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long clearTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs, null); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCleared()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isCleared()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + } + + @Test + public void testAssignAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarmId = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + UserId userId1 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d7b")); + UserId userId2 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d8b")); + Alarm alarm = saveAlarm(alarmId, tenantId, originator1Id, "TEST_ALARM"); + long assignTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId2, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs + 1, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId2, result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); + } + + private Alarm saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { Alarm alarm = new Alarm(); alarm.setId(new AlarmId(id)); alarm.setTenantId(TenantId.fromUUID(tenantId)); @@ -78,7 +295,10 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { alarm.setPropagate(true); alarm.setStartTs(System.currentTimeMillis()); alarm.setEndTs(System.currentTimeMillis()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); - alarmDao.save(TenantId.fromUUID(tenantId), alarm); + alarm.setAcknowledged(false); + alarm.setCleared(false); + alarm.setDetails(JacksonUtil.newObjectNode().put("a", UUID.randomUUID().toString()).set("b", JacksonUtil.newObjectNode().put("a", "[}/.`1321421!@@$$(%&&$"))); + return alarmDao.save(TenantId.fromUUID(tenantId), alarm); } + } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 058d9c08fd..c2c0a6d794 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -415,6 +415,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.postForLocation(baseURL + "/api/alarm/{alarmId}/clear", null, alarmId.getId()); } + public void assignAlarm(AlarmId alarmId, UserId userId) { + restTemplate.postForLocation(baseURL + "/api/alarm/{alarmId}/assign/{userId}", null, alarmId.getId(), userId.getId()); + } + + public void unassignAlarm(AlarmId alarmId) { + restTemplate.delete(baseURL + "/api/alarm/{alarmId}/assign", alarmId.getId()); + } + public PageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { String urlSecondPart = "/api/alarm/{entityType}/{entityId}?fetchOriginator={fetchOriginator}"; Map params = new HashMap<>(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index 2620ecd069..e88411d73a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -23,13 +24,17 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.Collection; @@ -39,29 +44,65 @@ import java.util.Collection; */ public interface RuleEngineAlarmService { - Alarm createOrUpdateAlarm(Alarm alarm); + /* + * New API, since 3.5. + */ - Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + /** + * Designed to update existing alarm. Accepts only part of the alarm fields. + */ + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5", forRemoval = true) + Alarm createOrUpdateAlarm(Alarm alarm); + + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + // Other API + Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); - ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); + AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); + + default ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { + return Futures.immediateFuture(findAlarmInfoById(tenantId, alarmId)); + } ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); - AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java index b5658a3451..3641d78b7c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; import lombok.Data; +import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; @Data @AllArgsConstructor @@ -34,4 +36,8 @@ public class TbAlarmResult { this.isCleared = isCleared; this.alarm = alarm; } + + public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { + return new TbAlarmResult(result.isCreated(), result.isModified(), result.isSeverityChanged(), result.isCleared(), result.getAlarm()); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index f94b4c36e2..ee32ac1533 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -26,10 +26,10 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; @Slf4j @RuleNode( @@ -57,38 +57,29 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); - ListenableFuture alarmFuture; + Alarm alarm; if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { - alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); + alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); } else { - alarmFuture = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); } - return Futures.transformAsync(alarmFuture, a -> { - if (a != null && !a.getStatus().isCleared()) { - return clearAlarm(ctx, msg, a); - } - return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); - }, ctx.getDbCallbackExecutor()); + if (alarm != null && !alarm.getStatus().isCleared()) { + return clearAlarm(ctx, msg, alarm); + } + return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); } private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); - return Futures.transformAsync(asyncDetails, details -> { + return Futures.transform(asyncDetails, details -> { ctx.logJsEvalResponse(); - ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), details, System.currentTimeMillis()); - return Futures.transformAsync(clearFuture, cleared -> { - ListenableFuture savedAlarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); - return Futures.transformAsync(savedAlarmFuture, savedAlarm -> { - if (cleared && savedAlarm != null) { - alarm.setDetails(savedAlarm.getDetails()); - alarm.setEndTs(savedAlarm.getEndTs()); - alarm.setClearTs(savedAlarm.getClearTs()); - } - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - return Futures.immediateFuture(new TbAlarmResult(false, false, true, alarm)); - }, ctx.getDbCallbackExecutor()); - }, ctx.getDbCallbackExecutor()); + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); + if (result.isSuccessful()) { + return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); + } else { + return new TbAlarmResult(false, false, false, alarm); + } }, ctx.getDbCallbackExecutor()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index c1c53d08f7..333926864d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -30,10 +29,12 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import java.io.IOException; import java.util.List; @@ -56,16 +57,16 @@ import java.util.List; ) public class TbCreateAlarmNode extends TbAbstractAlarmNode { - private static ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = new ObjectMapper(); private List relationTypes; private AlarmSeverity notDynamicAlarmSeverity; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx, configuration); - if(!this.config.isDynamicSeverity()) { + if (!this.config.isDynamicSeverity()) { this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity()); - if(this.notDynamicAlarmSeverity == null) { + if (this.notDynamicAlarmSeverity == null) { throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity()); } } @@ -97,15 +98,12 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); - return Futures.transformAsync(latest, existingAlarm -> { - if (existingAlarm == null || existingAlarm.getStatus().isCleared()) { - return createNewAlarm(ctx, msg, msgAlarm); - } else { - return updateAlarm(ctx, msg, existingAlarm, msgAlarm); - } - }, ctx.getDbCallbackExecutor()); - + Alarm existingAlarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + if (existingAlarm == null || existingAlarm.getStatus().isCleared()) { + return createNewAlarm(ctx, msg, msgAlarm); + } else { + return updateAlarm(ctx, msg, existingAlarm, msgAlarm); + } } private Alarm getAlarmFromMessage(TbContext ctx, TbMsg msg) throws IOException { @@ -115,9 +113,6 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncAlarm = Futures.transform(asyncDetails, details -> { + ListenableFuture asyncAlarm = Futures.transform(asyncDetails, details -> { if (buildDetails) { ctx.logJsEvalResponse(); } @@ -145,9 +140,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncCreated = Futures.transform(asyncAlarm, - alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); - return Futures.transform(asyncCreated, alarm -> new TbAlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); + ListenableFuture asyncCreated = Futures.transform(asyncAlarm, + alarm -> ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm)), ctx.getDbCallbackExecutor()); + return Futures.transform(asyncCreated, TbAlarmResult::fromAlarmResult, MoreExecutors.directExecutor()); } private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { @@ -159,7 +154,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncUpdated = Futures.transform(asyncDetails, (Function) details -> { + ListenableFuture asyncUpdated = Futures.transform(asyncDetails, details -> { if (buildDetails) { ctx.logJsEvalResponse(); } @@ -183,10 +178,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode new TbAlarmResult(false, true, false, a), MoreExecutors.directExecutor()); + return Futures.transform(asyncUpdated, TbAlarmResult::fromAlarmResult, MoreExecutors.directExecutor()); } private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { @@ -194,7 +188,8 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode generate(TbContext ctx, TbMsg msg) { log.trace("generate, config {}", config); if (prevMsg == null) { - prevMsg = ctx.newMsg(null, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); + prevMsg = ctx.newMsg(config.getQueueName(), "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); } if (initialized.get()) { ctx.logJsEvalRequest(); return Futures.transformAsync(scriptEngine.executeGenerateAsync(prevMsg), generated -> { log.trace("generate process response, generated {}, config {}", generated, config); ctx.logJsEvalResponse(); - prevMsg = ctx.newMsg(null, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); + prevMsg = ctx.newMsg(config.getQueueName(), generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); return Futures.immediateFuture(prevMsg); }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java index 027f4aa708..5f12603832 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java @@ -36,6 +36,7 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration alarmClearOperationResult = ctx.getAlarmService().clearAlarmForResult( - ctx.getTenantId(), currentAlarm.getId(), createDetails(clearState), System.currentTimeMillis() + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearState) ); - DonAsynchron.withCallback(alarmClearOperationResult, - result -> { - pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); - }, - throwable -> { - throw new RuntimeException(throwable); - }); + if (result.isCleared()) { + pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); + } currentAlarm = null; } else if (AlarmEvalResult.FALSE.equals(evalResult)) { stateUpdate = clearAlarmState(stateUpdate, clearState); @@ -165,9 +159,9 @@ class AlarmState { return true; } - public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { + public void initCurrentAlarm(TbContext ctx) { if (!initialFetchDone) { - Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); + Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()); if (alarm != null && !alarm.getStatus().isCleared()) { currentAlarm = alarm; } @@ -241,21 +235,18 @@ class AlarmState { // Skip update if severity is decreased. if (severity.ordinal() <= oldSeverity.ordinal()) { currentAlarm.setDetails(createDetails(ruleState)); - if (!oldSeverity.equals(severity)) { - currentAlarm.setSeverity(severity); - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, false, true, false, currentAlarm); - } else { - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, true, false, false, currentAlarm); - } + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } else { return null; } } else { currentAlarm = new Alarm(); currentAlarm.setType(alarmDefinition.getAlarmType()); - currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); + currentAlarm.setAcknowledged(false); + currentAlarm.setCleared(false); currentAlarm.setSeverity(severity); long startTs = dataSnapshot.getTs(); if (startTs == 0L) { @@ -272,9 +263,9 @@ class AlarmState { if (alarmDefinition.getPropagateRelationTypes() != null) { currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); } - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - boolean updated = currentAlarm.getStartTs() != currentAlarm.getEndTs(); - return new TbAlarmResult(!updated, updated, false, false, currentAlarm); + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } } @@ -342,7 +333,7 @@ class AlarmState { public void processAckAlarm(Alarm alarm) { if (currentAlarm != null && currentAlarm.getId().equals(alarm.getId())) { - currentAlarm.setStatus(alarm.getStatus()); + currentAlarm.setAcknowledged(alarm.isAcknowledged()); currentAlarm.setAckTs(alarm.getAckTs()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 0b6a1d6ed9..c3a72392e1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -28,7 +28,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -36,6 +35,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import javax.script.ScriptException; import java.io.IOException; @@ -59,7 +62,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.same; import static org.mockito.Mockito.times; @@ -71,9 +73,6 @@ import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.ACTIVE_UNACK; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.CLEARED_ACK; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.CLEARED_UNACK; @RunWith(MockitoJUnitRunner.class) public class TbAlarmNodeTest { @@ -93,16 +92,16 @@ public class TbAlarmNodeTest { @Captor private ArgumentCaptor> failureCaptor; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); private ListeningExecutor dbExecutor; - private EntityId originator = new DeviceId(Uuids.timeBased()); - private EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); - private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); - private TbMsgMetaData metaData = new TbMsgMetaData(); - private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; + private final EntityId originator = new DeviceId(Uuids.timeBased()); + private final EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); + private final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + private final TbMsgMetaData metaData = new TbMsgMetaData(); + private final String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; @Before public void before() { @@ -128,11 +127,26 @@ public class TbAlarmNodeTest { initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + long ts = msg.getTs(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); - long ts = msg.getTs(); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); + node.onMsg(ctx, msg); verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -153,18 +167,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -175,7 +177,7 @@ public class TbAlarmNodeTest { TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFailedFuture(new NotImplementedException("message"))); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); node.onMsg(ctx, msg); @@ -183,10 +185,10 @@ public class TbAlarmNodeTest { verify(ctx).createScriptEngine(ScriptLanguage.JS, "DETAILS"); verify(ctx).getAlarmService(); - verify(ctx, times(3)).getDbCallbackExecutor(); + verify(ctx, times(2)).getDbCallbackExecutor(); verify(ctx).logJsEvalRequest(); verify(ctx).getTenantId(); - verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType"); + verify(alarmService).findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType"); verifyNoMoreInteractions(ctx, alarmService); } @@ -197,12 +199,27 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); - Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build(); + Alarm clearedAlarm = Alarm.builder().cleared(true).acknowledged(true).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(clearedAlarm); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -225,35 +242,36 @@ public class TbAlarmNodeTest { Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @Test - public void alarmCanBeUpdated() throws ScriptException, IOException { + public void alarmCanBeUpdated() throws IOException { initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .endTs(activeAlarm.getEndTs()) + .build(); + when(alarmService.updateAlarm(any(AlarmUpdateRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .modified(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -274,18 +292,7 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - assertTrue(activeAlarm.getEndTs() > oldEndDate); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .endTs(activeAlarm.getEndTs()) - .build(); - + assertTrue(activeAlarm.getEndTs() >= oldEndDate); assertEquals(expectedAlarm, actualAlarm); } @@ -293,17 +300,30 @@ public class TbAlarmNodeTest { public void alarmCanBeCleared() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .cleared(true) + .severity(WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())) - .thenReturn(Futures.immediateFuture( false)); - when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm)); -// doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -325,17 +345,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(CLEARED_UNACK) - .severity(WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -343,17 +352,33 @@ public class TbAlarmNodeTest { public void alarmCanBeClearedWithAlarmOriginator() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg( "USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); AlarmId id = new AlarmId(alarmOriginator.getId()); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); activeAlarm.setId(id); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .cleared(true) + .severity(WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); + expectedAlarm.setId(id); + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findAlarmByIdAsync(tenantId, id)).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); -// doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findAlarmById(tenantId, id)).thenReturn(activeAlarm); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -375,18 +400,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(CLEARED_UNACK) - .severity(WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - expectedAlarm.setId(id); - assertEquals(expectedAlarm, actualAlarm); } @@ -415,9 +428,25 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -439,18 +468,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -478,10 +495,25 @@ public class TbAlarmNodeTest { metaData.putValue("alarmSeverity", "WARNING"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -502,23 +534,11 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @Test - public void testCreateAlarmsWithPropagationToTenantWithDynamicTypes() throws Exception{ + public void testCreateAlarmsWithPropagationToTenantWithDynamicTypes() throws Exception { for (int i = 0; i < 10; i++) { var config = new TbCreateAlarmNodeConfiguration(); config.setPropagateToTenant(true); @@ -541,11 +561,26 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagateToTenant(true) + .type("SomeType" + i) + .details(null) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType" + i)).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); - long ts = msg.getTs(); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType" + i)).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); verify(ctx, atMost(10)).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -566,18 +601,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagateToTenant(true) - .type("SomeType" + i) - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java index fd10e1212e..f64661bc5b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java @@ -25,6 +25,8 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; @@ -42,6 +44,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; @@ -77,11 +80,15 @@ public class DeviceStateTest { when(ctx.getAttributesService()).thenReturn(attributesService); RuleEngineAlarmService alarmService = mock(RuleEngineAlarmService.class); - when(alarmService.findLatestByOriginatorAndType(any(), any(), any())).thenReturn(Futures.immediateFuture(null)); - when(alarmService.createOrUpdateAlarm(any())).thenAnswer(invocationOnMock -> { - Alarm alarm = invocationOnMock.getArgument(0); - alarm.setId(new AlarmId(UUID.randomUUID())); - return alarm; + when(alarmService.findLatestActiveByOriginatorAndType(any(), any(), any())).thenReturn(null); + when(alarmService.createAlarm(any())).thenAnswer(invocationOnMock -> { + AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0); + return AlarmApiCallResult.builder() + .successful(true) + .created(true) + .modified(true) + .alarm(new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID())))) + .build(); }); when(ctx.getAlarmService()).thenReturn(alarmService); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 5d9abc130e..fb3085f3f7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -34,6 +33,8 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; @@ -46,6 +47,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -62,6 +64,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; @@ -79,8 +82,10 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class TbDeviceProfileNodeTest { @@ -187,8 +192,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); @@ -205,6 +210,8 @@ public class TbDeviceProfileNodeTest { TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + registerCreateAlarmMock(alarmService.updateAlarm(any()), false); + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); @@ -274,9 +281,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(attrListListenableFuture); @@ -357,9 +364,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(Futures.immediateFuture(Collections.emptyList())); @@ -430,9 +437,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -524,9 +531,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -642,9 +649,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(optionalDurationAttribute); @@ -757,9 +764,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -867,9 +874,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(optionalDurationAttribute); @@ -969,9 +976,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -1067,9 +1074,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -1149,9 +1156,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); @@ -1246,8 +1253,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); @@ -1319,9 +1326,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -1394,10 +1401,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -1474,10 +1480,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); @@ -1485,7 +1490,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFutureWithLess); Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.anyString(), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); @@ -1560,10 +1565,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); @@ -1571,7 +1575,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFutureWithLess); Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.anyString(), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); @@ -1601,4 +1605,18 @@ public class TbDeviceProfileNodeTest { node.init(ctx, nodeConfiguration); } + private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) { + when(a).thenAnswer(invocationOnMock -> { +// AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0); + AlarmInfo alarm = new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID()))); + alarm.setSeverity(AlarmSeverity.CRITICAL); + return AlarmApiCallResult.builder() + .successful(true) + .created(created) + .modified(true) + .alarm(alarm) + .build(); + }); + } + } diff --git a/ui-ngx/src/app/core/http/alarm-comment.service.ts b/ui-ngx/src/app/core/http/alarm-comment.service.ts new file mode 100644 index 0000000000..ffb8fc4528 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm-comment.service.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { AlarmComment, AlarmCommentInfo } from '@shared/models/alarm.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmCommentService { + + constructor( + private http: HttpClient + ) { } + + public saveAlarmComment(alarmId: string, alarmComment: AlarmComment, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/comment`, alarmComment, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmComments(alarmId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/${alarmId}/comment${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmComments(alarmId: string, commentId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/comment/${commentId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index 802045b684..076f590199 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -60,6 +60,14 @@ export class AlarmService { return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); } + public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/assign/${assigneeId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public unassignAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config)); + } + public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 0a7d2054e4..9a6b70ce29 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { User } from '@shared/models/user.model'; +import { User, UserEmailInfo } from '@shared/models/user.model'; import { Observable } from 'rxjs'; import { HttpClient, HttpParams } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; @@ -84,4 +84,8 @@ export class UserService { return this.http.post(url, null, defaultHttpOptionsFromConfig(config)); } + public findUsersByQuery(pageLink: PageLink, config?: RequestConfig) : Observable> { + return this.http.get>(`/api/users/info${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index b4afeae6c3..84134f04a9 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -59,10 +59,11 @@ export class DynamicComponentFactoryService { template: string, modules?: Type[], preserveWhitespaces?: boolean, - compileAttempt = 1): Observable> { + compileAttempt = 1, + styles?: string[]): Observable> { return from(import('@angular/compiler')).pipe( mergeMap(() => { - const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles); let moduleImports: Type[] = [CommonModule]; if (modules) { moduleImports = [...moduleImports, ...modules]; @@ -91,7 +92,7 @@ export class DynamicComponentFactoryService { catchError((error) => { if (compileAttempt === 1) { ɵresetCompiledComponents(); - return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt); + return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); } else { throw error; } @@ -110,11 +111,12 @@ export class DynamicComponentFactoryService { } } - private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean): Type { + private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { // noinspection AngularMissingOrInvalidDeclarationInModule return Component({ template, - preserveWhitespaces + preserveWhitespaces, + styles })(componentType); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index ce6cf32dbd..ea4c85eb64 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -25,7 +25,7 @@ import { createLabelFromDatasource, deepClone, deleteNullProperties, - guid, + guid, hashCode, isDefined, isDefinedAndNotNull, isString, @@ -405,6 +405,13 @@ export class UtilsService { }); } + public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string { + if (str && str.length) { + let hue = hashCode(str) % 360; + return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`; + } + } + public currentPerfTime(): number { return this.window.performance && this.window.performance.now ? this.window.performance.now() : Date.now(); diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 992ddec310..4018fd491f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -304,7 +304,7 @@ export function deepClone(target: T, ignoreFields?: string[]): T { (target as any[]).forEach((v) => { cp.push(v); }); return cp.map((n: any) => deepClone(n)) as any; } - if (typeof target === 'object' && Object.keys(target).length) { + if (typeof target === 'object') { const cp = {...(target as { [key: string]: any })} as { [key: string]: any }; Object.keys(cp).forEach(k => { if (!ignoreFields || ignoreFields.indexOf(k) === -1) { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html new file mode 100644 index 0000000000..f6ba02e7a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -0,0 +1,52 @@ + + + + search + + + account_circle + alarm.unassigned + + + + +
+ + +
+
+ + + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss new file mode 100644 index 0000000000..a10848cc70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + width: 100%; + overflow: auto; + background: #fff; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15); + border-radius: 4px; +} + +::ng-deep { + mat-form-field.search-users { + padding: 8px; + height: 340px; + font-size: 14px; + background-color: #fff; + } + + .mat-form-field-appearance-outline .mdc-notched-outline__trailing{ + color: rgba(0, 0, 0, 0.12) !important; + } + + .tb-assignee-autocomplete { + &.tb-assignee-autocomplete.mat-mdc-autocomplete-panel { + position: relative; + left: -8px; + margin-top: 8px; + box-shadow: none !important; + } + .mat-mdc-option { + font-size: 14px; + border: none; + height: 52px !important; + .unassigned-icon { + color: rgba(0, 0, 0, 0.38); + font-size: 28px; + width: 28px; + height: 28px; + margin-right: 8px; + } + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + background-color: #5cb445; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: #fff; + font-size: 13px; + font-weight: 700 + } + .user-display-name { + max-width: 180px; + overflow: hidden; + span { + overflow: hidden; + text-overflow: ellipsis; + } + span + span { + color: rgba(0, 0, 0, 0.38); + } + } + .mdc-list-item__primary-text { + display: flex; + justify-content: start; + align-items: center; + line-height: normal; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts new file mode 100644 index 0000000000..bd50beef31 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Inject, + InjectionToken, OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, of, Subject } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + share, + switchMap, + takeUntil, +} from 'rxjs/operators'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { TranslateService } from '@ngx-translate/core'; +import { UserService } from '@core/http/user.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { AlarmService } from '@core/http/alarm.service'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { UtilsService } from '@core/services/utils.service'; + +export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken('AlarmAssigneePanelData'); + +export interface AlarmAssigneePanelData { + alarmId: string; + assigneeId: string; +} + +@Component({ + selector: 'tb-alarm-assignee-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'] +}) +export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { + + private dirty = false; + + alarmId: string; + + assigneeId?: string; + + selectUserFormGroup: FormGroup; + + @ViewChild('userInput', {static: true}) userInput: ElementRef; + + filteredUsers: Observable>; + + searchText = ''; + + private destroy$ = new Subject(); + + constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData, + public overlayRef: OverlayRef, + public translate: TranslateService, + private userService: UserService, + private alarmService: AlarmService, + private fb: FormBuilder, + private utilsService: UtilsService) { + this.alarmId = data.alarmId; + this.assigneeId = data.assigneeId; + this.selectUserFormGroup = this.fb.group({ + user: [null] + }); + } + + ngOnInit() { + this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges + .pipe( + debounceTime(150), + map(value => { + return value ? (typeof value === 'string' ? value : '') : '' + }), + distinctUntilChanged(), + switchMap(name => this.fetchUsers(name)), + share(), + takeUntil(this.destroy$) + ); + } + + ngAfterViewInit() { + setTimeout(() => { + this.userInput.nativeElement.focus(); + }, 0) + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + displayUserFn(user?: User): string | undefined { + return user ? user.email : undefined; + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.clear(); + const user: User = event.option.value; + if (user) { + this.assign(user); + } else { + this.unassign(); + } + } + + assign(user: User): void { + this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe( + () => this.overlayRef.dispose()); + } + + unassign(): void { + this.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe( + () => this.overlayRef.dispose()); + } + + fetchUsers(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'email', + direction: Direction.ASC + }); + return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) + .pipe( + catchError(() => of(emptyPageData())), + map(pageData => { + return pageData.data; + }) + ); + } + + onFocus(): void { + if (!this.dirty) { + this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); + this.dirty = true; + } + } + + clear() { + this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.userInput.nativeElement.blur(); + this.userInput.nativeElement.focus(); + }, 0); + } + + getUserDisplayName(entity: User) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: User): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: User): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: User) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html new file mode 100644 index 0000000000..52066ff3ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -0,0 +1,43 @@ + + +
+ + + {{ getUserInitials(alarm.assignee) }} + + + {{ getUserDisplayName(alarm.assignee) }} + + + + account_circle + alarm.unassigned + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss new file mode 100644 index 0000000000..935ac2f32d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-assignee { + cursor: pointer; + max-width: 273px; + + .assigned-container { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + } + } + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts new file mode 100644 index 0000000000..5d7f29d55b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef +} from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { AlarmAssignee, AlarmInfo } from '@shared/models/alarm.models'; +import { + ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; + +@Component({ + selector: 'tb-alarm-assignee', + templateUrl: './alarm-assignee.component.html', + styleUrls: ['./alarm-assignee.component.scss'] +}) +export class AlarmAssigneeComponent { + @Input() + alarm: AlarmInfo; + + @Output() + alarmReassigned = new EventEmitter(); + + constructor(private utilsService: UtilsService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef) { + } + + getUserDisplayName(entity: AlarmAssignee) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: AlarmAssignee): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: AlarmAssignee): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: AlarmAssignee) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, alarm: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.minWidth = '260px'; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: alarm.id.id, + assigneeId: alarm.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)).onDestroy(() => this.alarmReassigned.emit(true)); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html new file mode 100644 index 0000000000..94f8f47eb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -0,0 +1,44 @@ + +
+ +

{{ 'alarm.comments' | translate }}

+ + +
+ + +
+
+ + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts new file mode 100644 index 0000000000..026ebae8c0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { AlarmInfo } from '@shared/models/alarm.models'; + +export interface AlarmCommentDialogData { + alarmId?: string; + alarm?: AlarmInfo; + commentsHeaderEnabled: boolean; +} + +@Component({ + selector: 'tb-alarm-comment-dialog', + templateUrl: './alarm-comment-dialog.component.html', + styleUrls: [] +}) +export class AlarmCommentDialogComponent extends DialogComponent { + + alarmId: string; + + commentsHeaderEnabled: boolean = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + this.commentsHeaderEnabled = this.data.commentsHeaderEnabled + this.alarmId = this.data.alarmId; + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html new file mode 100644 index 0000000000..91b4871d86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -0,0 +1,162 @@ + + +
+
+ + {{ 'alarm-comment.comments' | translate }} + +
+ + +
+
+
+ + + +
+
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} + +
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+
+
+ {{ displayDataElement.displayName }} + + edited {{ displayDataElement.editedDateAgo }} + + + {{ displayDataElement.createdDateAgo }} + +
+ {{ displayDataElement.commentText }} +
+
+ + +
+
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+ {{ getUserInitials(userDisplayName) }} +
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss new file mode 100644 index 0000000000..3cd97767a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-alarm-comments { + padding: 16px 24px 24px 24px; + background-color: #fafafa; + max-width: 600px; + + &-header { + background-color: #fafafa; + position: sticky; + top: -25px; + z-index: 1; + margin-bottom: 10px; + + &-title { + color: rgba(0, 0, 0, 0.76); + letter-spacing: 0.25px; + font-weight: 500; + } + + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + + &-user-avatar { + width: 28px; + min-width: 28px; + height: 28px; + min-height: 28px; + border-radius: 50%; + font-weight: 700; + color: #FFFFFF; + font-size: 13px; + } + + &-user-name { + font-size: 16px; + color: rgba(0, 0, 0, 0.76); + font-weight: 500; + letter-spacing: 0.25px; + } + + &-time { + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.38); + letter-spacing: 0.2px + } + + &-system-text { + color: rgba(0, 0, 0, 0.38); + font-weight: 500; + letter-spacing: 0.25px; + } + + &-text { + white-space: pre-line; + word-break: break-word; + color: rgba(0, 0, 0, 0.54); + letter-spacing: 0.15px; + } + + &-action-buttons { + visibility: hidden; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + + .show-buttons { + visibility: visible; + } + + .green-button { + color: #00695C; + } + + .red-button { + color: #D12730; + } + + .mat-form-field { + font-size: 16px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + } + + textarea { + letter-spacing: 0.15px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts new file mode 100644 index 0000000000..6e32979a14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -0,0 +1,295 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmCommentService } from '@core/http/alarm-comment.service'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthUser, User } from '@shared/models/user.model'; +import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { map } from 'rxjs/operators'; +import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityType } from '@shared/models/entity-type.models'; + +interface AlarmCommentsDisplayData { + commentId?: string, + displayName?: string, + createdDateAgo?: string, + edit?: boolean, + isEdited?: boolean, + editedDateAgo?: string, + showActions?: boolean, + commentText?: string, + isSystemComment?: boolean, + avatarBgColor?: string +} + +@Component({ + selector: 'tb-alarm-comment', + templateUrl: './alarm-comment.component.html', + styleUrls: ['./alarm-comment.component.scss'] +}) +export class AlarmCommentComponent implements OnInit { + @Input() + alarmId: string; + + @Input() + commentsHeaderEnabled: boolean = true; + + authUser: AuthUser; + + alarmCommentFormGroup: FormGroup; + + alarmComments: Array; + + displayData: Array = new Array(); + + alarmCommentSortOrder: SortOrder = { + property: 'createdTime', + direction: Direction.DESC + }; + + editMode: boolean = false; + + userDisplayName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getUserDisplayName(user)) + ); + + currentUserDisplayName: string; + currentUserAvatarColor: string; + + constructor(protected store: Store, + private translate: TranslateService, + private alarmCommentService: AlarmCommentService, + public fb: FormBuilder, + private dialogService: DialogService, + public dateAgoPipe: DateAgoPipe, + private utilsService: UtilsService) { + + this.authUser = getCurrentAuthUser(store); + + this.alarmCommentFormGroup = this.fb.group( + { + alarmCommentEdit: [''], + alarmComment: [''] + } + ); + } + + ngOnInit() { + this.loadAlarmComments(); + this.currentUserAvatarColor = this.utilsService.stringToHslColor(this.currentUserDisplayName, + 60, 40); + } + + loadAlarmComments(): void { + this.alarmCommentService.getAlarmComments(this.alarmId, new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, + this.alarmCommentSortOrder), {ignoreLoading: true}).subscribe( + (pagedData) => { + this.alarmComments = pagedData.data; + this.displayData.length = 0; + for (let alarmComment of pagedData.data) { + let displayDataElement: AlarmCommentsDisplayData = {}; + displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); + displayDataElement.commentText = alarmComment.comment.text; + displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; + if (alarmComment.type === AlarmCommentType.OTHER) { + displayDataElement.commentId = alarmComment.id.id; + displayDataElement.displayName = this.getUserDisplayName(alarmComment); + displayDataElement.edit = false; + displayDataElement.isEdited = alarmComment.comment.edited; + displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn).toLowerCase(); + displayDataElement.showActions = false; + displayDataElement.isSystemComment = false; + displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName, + 40, 60); + } + this.displayData.push(displayDataElement); + } + } + ) + } + + changeSortDirection() { + let currentDirection = this.alarmCommentSortOrder.direction; + this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; + this.loadAlarmComments(); + } + + saveComment(): void { + const commentInputValue: string = this.getAlarmCommentFormControl().value; + if (commentInputValue) { + const comment: AlarmComment = { + alarmId: { + id: this.alarmId, + entityType: EntityType.ALARM + }, + type: AlarmCommentType.OTHER, + comment: { + text: commentInputValue + } + } + this.doSave(comment); + this.clearCommentInput(); + } + } + + saveEditedComment(commentId: string): void { + const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value; + if (commentEditInputValue) { + const editedComment: AlarmComment = this.getAlarmCommentById(commentId); + editedComment.comment.text = commentEditInputValue; + this.doSave(editedComment); + this.clearCommentEditInput(); + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + } + + private doSave(comment: AlarmComment): void { + this.alarmCommentService.saveAlarmComment(this.alarmId, comment, {ignoreLoading: true}).subscribe( + () => { + this.loadAlarmComments(); + } + ) + } + + editComment(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = true; + this.editMode = true; + this.getAlarmCommentEditFormControl().patchValue(commentDisplayData.commentText); + this.getAlarmCommentFormControl().disable({emitEvent: false}); + } + + cancelEdit(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = false; + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + + deleteComment(commentId: string): void { + const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId); + const commentText: string = alarmCommentInfo.comment.text; + this.dialogService.confirm( + this.translate.instant('alarm-comment.delete-alarm-comment'), + commentText, + this.translate.instant('action.cancel'), + this.translate.instant('action.delete')).subscribe( + (result) => { + if (result) { + this.alarmCommentService.deleteAlarmComments(this.alarmId, commentId, {ignoreLoading: true}) + .subscribe(() => { + this.loadAlarmComments(); + } + ) + } + } + ) + } + + getSortDirectionIcon() { + return this.alarmCommentSortOrder.direction === Direction.DESC ? 'arrow_downward' : 'arrow_upward' + } + + isDirectionAscending() { + return this.alarmCommentSortOrder.direction === Direction.ASC; + } + + isDirectionDescending() { + return this.alarmCommentSortOrder.direction === Direction.DESC; + } + + onCommentMouseEnter(commentId: string, displayDataIndex: number): void { + if (!this.editMode) { + const alarmUserId = this.getAlarmCommentById(commentId).userId.id; + if (this.authUser.userId === alarmUserId) { + this.displayData[displayDataIndex].showActions = true; + } + } + } + + onCommentMouseLeave(displayDataIndex: number): void { + this.displayData[displayDataIndex].showActions = false; + } + + getUserInitials(userName: string): string { + let initials = ''; + const userNameSplit = userName.split(' '); + initials += userNameSplit[0].charAt(0).toUpperCase(); + if (userNameSplit.length > 1) { + initials += userNameSplit[userNameSplit.length - 1].charAt(0).toUpperCase(); + } + return initials; + } + + getCurrentUserBgColor(userDisplayName: string) { + return this.utilsService.stringToHslColor(userDisplayName, 40, 60); + } + + private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string { + let name = ''; + if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) || + (alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) { + if (alarmCommentInfo.firstName) { + name += alarmCommentInfo.firstName; + } + if (alarmCommentInfo.lastName) { + if (name.length > 0) { + name += ' '; + } + name += alarmCommentInfo.lastName; + } + } else { + name = alarmCommentInfo.email; + } + return name; + } + + getAlarmCommentFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmComment'); + } + + getAlarmCommentEditFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmCommentEdit'); + } + + private clearCommentInput(): void { + this.getAlarmCommentFormControl().patchValue(''); + } + + private clearCommentEditInput(): void { + this.getAlarmCommentEditFormControl().patchValue(''); + } + + private getAlarmCommentById(id: string): AlarmComment { + return this.alarmComments.find(comment => comment.id.id === id); + } + + private getDataElementByCommentId(commentId: string): AlarmCommentsDisplayData { + return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 3bdb10db57..e2af9dcf24 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -28,41 +28,17 @@
-
-
+
+
- - alarm.created-time - - alarm.originator -
-
- - alarm.start-time - - - - alarm.end-time - - - -
-
- - alarm.ack-time - - - - alarm.clear-time - + + alarm.created-time + -
@@ -74,18 +50,60 @@ +
+
alarm.status + +
- - + + + + + + alarm.advanced-info + + + +
+ + alarm.start-time + + + + alarm.end-time + + + +
+
+ + alarm.ack-time + + + + alarm.clear-time + + + +
+ + +
+
+
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss index cb4a293072..2a77861e21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { +:host::ng-deep { width: 100%; height: 100%; display: block; @@ -26,6 +26,42 @@ &.invisible { visibility: hidden; } + .mat-mdc-cell { + .assignee-cell { + .assigned-container { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + } + } + .unassigned-container { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + margin-right: 8px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } + } + } + } } } span.no-data-found { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 35392e4f9e..5b233ba2d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -93,7 +93,7 @@ import { } from '@home/components/widget/lib/display-columns-panel.component'; import { AlarmDataInfo, - alarmFields, + alarmFields, AlarmInfo, AlarmSearchStatus, alarmSeverityColors, alarmSeverityTranslations, @@ -127,6 +127,14 @@ import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; +import { + ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { + AlarmCommentDialogComponent, + AlarmCommentDialogData +} from '@home/components/alarm/alarm-comment-dialog.component'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -135,15 +143,18 @@ interface AlarmsTableWidgetSettings extends TableWidgetSettings { enableSelection: boolean; enableStatusFilter?: boolean; enableFilter: boolean; + displayComments: boolean; displayDetails: boolean; allowAcknowledgment: boolean; allowClear: boolean; + allowAssign: boolean; } interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor { details?: boolean; acknowledge?: boolean; clear?: boolean; + comments?: boolean; } @Component({ @@ -186,9 +197,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private alarmsTitlePattern: string; + private displayComments = false; private displayDetails = true; public allowAcknowledgment = true; private allowClear = true; + public allowAssign = true; private defaultPageSize = 10; private defaultSortOrder = '-' + alarmFields.createdTime.value; @@ -322,9 +335,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private initializeConfig() { this.ctx.widgetActions = [this.searchAction, this.alarmFilterAction, this.columnDisplayAction]; + this.displayComments = isDefined(this.settings.displayComments) ? this.settings.displayComments : false; this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true; this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true; + this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true; if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) { this.alarmsTitlePattern = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle); @@ -418,6 +433,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (alarmField && alarmField.time) { keySettings.columnWidth = '120px'; } + if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) { + keySettings.columnWidth = '120px' + } } this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); @@ -446,6 +464,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.sortOrderProperty = sortColumn ? sortColumn.def : null; const actionCellDescriptors: AlarmWidgetActionDescriptor[] = []; + if (this.displayComments) { + actionCellDescriptors.push( + { + displayName: this.translate.instant('alarm-comment.comments'), + icon: 'comment', + comments: true + } as AlarmWidgetActionDescriptor + ); + } + if (this.displayDetails) { actionCellDescriptors.push( { @@ -791,6 +819,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.ackAlarm($event, alarm); } else if (actionDescriptor.clear) { this.clearAlarm($event, alarm); + } else if (actionDescriptor.comments) { + this.openAlarmComments($event, alarm); } else { if ($event) { $event.stopPropagation(); @@ -954,6 +984,24 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } + private openAlarmComments($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + if (alarm && alarm.id && alarm.id.id !== NULL_UUID) { + this.dialog.open + (AlarmCommentDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmId: alarm.id.id, + commentsHeaderEnabled: false + } + }).afterClosed() + } + } + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { if (isDefined(value)) { const alarmField = alarmFields[key.name]; @@ -966,7 +1014,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value; } else if (alarmField.value === alarmFields.originatorType.value) { return this.translate.instant(entityTypeTranslations.get(value).type); - } else { + } else if (alarmField.value === alarmFields.assignee.value) { + return ''; + } + else { return value; } } @@ -1013,6 +1064,84 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.cellStyleCache.length = 0; this.rowStyleCache.length = 0; } + + getUserDisplayName(entity: AlarmInfo) { + let displayName = ''; + if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) || + (entity.assignee.lastName && entity.assignee.lastName.length > 0)) { + if (entity.assignee.firstName) { + displayName += entity.assignee.firstName; + } + if (entity.assignee.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.assignee.lastName; + } + } else { + displayName = entity.assignee.email; + } + return displayName; + } + + getUserInitials(entity: AlarmInfo): string { + let initials = ''; + if (entity.assignee.firstName && entity.assignee.firstName.length || + entity.assignee.lastName && entity.assignee.lastName.length) { + if (entity.assignee.firstName) { + initials += entity.assignee.firstName.charAt(0); + } + if (entity.assignee.lastName) { + initials += entity.assignee.lastName.charAt(0); + } + } else { + initials += entity.assignee.email.charAt(0); + } + return initials.toUpperCase(); + } + + getAvatarBgColor(entity: AlarmInfo) { + return this.utils.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.minWidth = '260px'; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: entity.id.id, + assigneeId: entity.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)); + } } class AlarmsDatasource implements DataSource { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index d182c26dbe..5f4486dad2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -16,4 +16,6 @@ --> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index c71affe5c1..3f3f13626e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnInit, Type } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; @@ -25,7 +25,8 @@ import { createLabelFromPattern, flatDataWithoutOverride, formattedDataFormDatasourceData, - hashCode, isDefinedAndNotNull, + hashCode, + isDefinedAndNotNull, isNotEmptyStr, parseFunction, safeExecute @@ -53,7 +54,6 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { settings: MarkdownWidgetSettings; markdownTextFunction: MarkdownTextFunction; - @HostBinding('class') markdownClass: string; @Input() @@ -61,6 +61,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { markdownText: string; + additionalStyles: string[]; + constructor(protected store: Store, private utils: UtilsService, @@ -74,14 +76,15 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { this.settings = this.ctx.settings; this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null; - this.markdownClass = 'markdown-widget'; - const cssString = this.settings.markdownCss; + let cssString = this.settings.markdownCss; if (isNotEmptyStr(cssString)) { const cssParser = new cssjs(); - cssParser.testMode = false; - this.markdownClass += '-' + hashCode(cssString); + this.markdownClass = 'markdown-widget-' + hashCode(cssString); cssParser.cssPreviewNamespace = this.markdownClass; - cssParser.createStyleElement(this.markdownClass, cssString); + cssParser.testMode = false; + cssString = cssParser.applyNamespacing(cssString); + cssString = cssParser.getCSSForEditor(cssString); + this.additionalStyles = [cssString]; } const pageSize = isDefinedAndNotNull(this.ctx.widgetConfig.pageSize) && this.ctx.widgetConfig.pageSize > 0 ? this.ctx.widgetConfig.pageSize : 16384; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index 07dc221e6f..d0dd324e52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -66,13 +66,17 @@ :host ::ng-deep { .tb-multiple-input { - .mat-mdc-slide-toggle .mdc-form-field > label { - display: flex; - align-items: center; - - .mat-icon { - margin-left: 8px; - margin-right: 8px; + .mat-mdc-slide-toggle .mdc-form-field { + width: 100%; + & > label { + display: flex; + align-items: center; + width: 100%; + margin: 0; + .mat-icon { + margin-left: 8px; + margin-right: 8px; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index f2ce2642bd..8820225e6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -58,6 +58,9 @@
+ + {{ 'widgets.table.display-alarm-comments' | translate }} + {{ 'widgets.table.display-alarm-details' | translate }} @@ -67,6 +70,9 @@ {{ 'widgets.table.allow-alarms-clear' | translate }} + + {{ 'widgets.table.allow-alarms-assign' | translate }} +
{{ 'widgets.table.display-pagination' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts index 953bde001c..655f915a4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts @@ -51,6 +51,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent displayDetails: true, allowAcknowledgment: true, allowClear: true, + allowAssign: true, displayPagination: true, defaultPageSize: 10, defaultSortOrder: '-createdTime', @@ -72,6 +73,8 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent displayDetails: [settings.displayDetails, []], allowAcknowledgment: [settings.allowAcknowledgment, []], allowClear: [settings.allowClear, []], + allowAssign: [settings.allowAssign, []], + displayComments: [settings.displayComments, []], displayPagination: [settings.displayPagination, []], defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], defaultSortOrder: [settings.defaultSortOrder, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3ef074c91a..68e89ef8d0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -457,6 +457,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { '}\n' + '.mat-mdc-table .mat-mdc-cell {\n' + 'color: ' + mdDark + ';\n' + + 'background-color: rgba(0, 0, 0, 0);\n' + '}\n' + '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' + 'color: ' + mdDarkSecondary + ';\n' + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index 61b8345a46..4c7264ce29 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -97,6 +97,12 @@ div.tb-widget { } } + .tb-widget-title, .tb-widget-actions { + .mat-icon { + vertical-align: bottom; + } + } + .tb-widget-content { &.tb-no-interaction { pointer-events: none; diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss index 1a19de8b06..85c5dfa42e 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -18,6 +18,7 @@ .tb-toggle-icon { display: inline-block; width: 15px; + min-width: 15px; margin: auto 0 auto auto; background-size: 100% auto; transition: transform .3s ease-in-out; diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss index e0b8b24110..54cbf11c53 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -53,12 +53,19 @@ mat-icon { margin-right: 8px; margin-left: 0; + min-width: 1.125rem; + min-height: 1.125rem; } span.mdc-button__label { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + display: inline-flex; + & > span { + overflow: hidden; + text-overflow: ellipsis; + } .tb-toggle-icon { padding-top: 12px; padding-bottom: 12px; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 6d8b710b85..09949a29b3 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -89,6 +89,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; +import { UserId } from '@shared/models/id/user-id'; export interface IWidgetAction { name: string; @@ -415,8 +416,8 @@ export class WidgetContext { return new TimePageLink(pageSize, page, textSearch, sortOrder, startTime, endTime); } - alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean) { - return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator); + alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean, assigneeId: UserId) { + return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator, assigneeId); } } diff --git a/ui-ngx/src/app/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss new file mode 100644 index 0000000000..d30de48ade --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown.component.scss @@ -0,0 +1,362 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-markdown-view { + display: block; + + $headings: h1, h2, h3, h4, h5, h6; + + h1 { + font-size: 32px; + padding-right: 60px; + } + + @each $heading in $headings { + #{$heading} { + color: #0F161D; + line-height: normal; + font-weight: 500; + border-bottom: none; + margin: 0; + } + & > #{$heading} { + padding: 30px 32px 10px; + } + } + + p { + font-size: 16px; + font-weight: 400; + line-height: 1.25em; + margin: 0; + } + + p + p { + margin-top: 10px; + } + + p, div { + color: rgba(15, 22, 29, 0.8); + line-height: 1.5em; + } + + & > p, & > div { + padding-right: 32px; + padding-left: 32px; + } + + ul { + padding-left: 62px; + padding-right: 32px; + color: rgba(15, 22, 29, 0.8); + margin-top: 16px; + margin-bottom: 16px; + } + + ul { + @each $heading in $headings { + & + #{$heading} { + padding-top: 14px; + } + } + } + + li { + padding-bottom: .75em; + line-height: 1.5em; + + ul { + margin-bottom: 0; + } + + p { + padding-left: 0; + } + } + + a { + font-weight: 500; + color: #2a7dec; + text-decoration: none; + border: none; + + &:hover { + color: #2a7dec; + text-decoration: underline; + border: none; + } + } + + & > table { + margin-left: 32px; + width: calc(100% - 64px); + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + border-collapse: unset; + border-spacing: 0; + margin-top: 30px; + margin-bottom: 30px; + overflow: hidden; + table-layout: fixed; + + &.auto { + table-layout: auto; + } + + & > thead { + background-color: #f9fbff; + color: rgba(33, 37, 41, .6); + + & > tr { + & > th { + border-bottom: 1px solid rgba(42, 125, 236, .2); + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + & > tbody { + & > tr:not(:last-child) { + & > td { + border-bottom: 1px solid rgba(42, 125, 236, .2); + } + } + + & > tr { + & > td { + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + color: rgba(15, 22, 29, 0.8); + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + th, td { + font-size: .85em; + padding: 8px; + margin: 0; + text-align: left; + } + + td[align=center], th[align=center] { + text-align: center; + } + + td[align=right], th[align=right] { + text-align: right; + } + + tr td div { + padding-left: 0; + padding-right: 0; + } + } + + + div.divider { + padding-top: 32px; + border-bottom: 1px solid rgba(15, 22, 29, 0.1); + } + + ul + div.divider { + padding-top: 16px; + } + + img { + max-width: 100%; + } + + button.tb-button { + cursor: pointer; + display: inline-block; + border-radius: 4px; + border: none; + padding: 10px 20px; + line-height: 24px; + color: #fff; + background-color: #305680; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + text-decoration: none; + font-size: 16px; + font-weight: 500; + transition: background-color .4s cubic-bezier(.25, .8, .25, 1); + + &:hover { + background-color: #264363; + color: #fff; + text-decoration: none; + } + } + + #video { + width: 100%; + margin: 0; + position: relative; + + #video_wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + padding-left: 0; + padding-right: 0; + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + } + + @media screen and (min-width: 750px) { + #video { + width: 100%; + display: block; + } + } + + @media screen and (min-width: 1025px) { + #video { + width: 50%; + position: relative; + } + } + + code:not([class*=language-]) { + color: #eb5757; + font-family: monospace; + font-size: 16px; + } + + div.code-wrapper { + position: relative; + + button.clipboard-btn { + pointer-events: none; + outline: none; + position: absolute; + width: 206px; + height: 42px; + top: 0; + right: 32px; + background: 0 0; + border: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.multiline { + right: 44px; + } + + p { + padding: 8px; + top: 1px; + transition: .2s; + color: #2a7dec; + background: rgba(255, 255, 255, .85); + backdrop-filter: blur(4px); + opacity: 0; + font-weight: 500; + right: 32px; + position: absolute; + } + + div { + background-color: #fff; + position: absolute; + width: 38px; + height: 38px; + top: 3px; + right: 3px; + padding: 10px; + + img { + position: initial; + width: 18px; + height: 18px; + filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); + } + } + } + + &:hover { + cursor: pointer; + + pre[class*="language-"] { + border: solid 1px #2a7dec; + } + + button.clipboard-btn { + p { + opacity: 1; + } + + div { + img { + filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); + } + } + } + } + } + + th, td { + div.code-wrapper { + display: inline-block; + width: 100%; + + button.clipboard-btn { + top: -10px; + right: 0; + padding: 0 3px; + } + } + } + + pre[class*="language-"] { + font-size: 16px; + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + background: 0 0; + padding: 8px 16px; + color: #212529; + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 95c0ccd23b..24226573e6 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -18,14 +18,17 @@ import { ChangeDetectorRef, Component, ComponentFactory, - ComponentRef, ElementRef, + ComponentRef, + ElementRef, EventEmitter, Inject, Injector, - Input, OnChanges, + Input, + OnChanges, Output, SimpleChanges, - Type, ViewChild, + Type, + ViewChild, ViewContainerRef } from '@angular/core'; import { HelpService } from '@core/services/help.service'; @@ -33,12 +36,13 @@ import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; -import { isDefinedAndNotNull } from '@core/utils'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; @Component({ selector: 'tb-markdown', - templateUrl: './markdown.component.html' + templateUrl: './markdown.component.html', + styleUrls: ['./markdown.component.scss'] }) export class TbMarkdownComponent implements OnChanges { @@ -53,8 +57,12 @@ export class TbMarkdownComponent implements OnChanges { @Input() markdownClass: string | undefined; + @Input() containerClass: string | undefined; + @Input() style: { [klass: string]: any } = {}; + @Input() additionalStyles: string[]; + @Input() get lineNumbers(): boolean { return this.lineNumbersValue; } set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } @@ -95,6 +103,9 @@ export class TbMarkdownComponent implements OnChanges { markdownClass += ` ${this.markdownClass}`; } template = `
${template}
`; + if (this.containerClass) { + template = `
${template}
`; + } this.markdownContainer.clear(); const parent = this; let readyObservable: Observable; @@ -102,6 +113,11 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalCompileModules) { compileModules = compileModules.concat(this.additionalCompileModules); } + let styles: string[] = deepClone(TbMarkdownComponent['ɵcmp'].styles); + styles[0] = styles[0].replace(/\[_nghost\-%COMP%\]/g, '').replace(/\[_ngcontent\-%COMP%\]/g, ''); + if (this.additionalStyles) { + styles = styles.concat(this.additionalStyles); + } this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { ngOnDestroy(): void { @@ -110,7 +126,7 @@ export class TbMarkdownComponent implements OnChanges { }, template, compileModules, - true + true, 1, styles ).subscribe((factory) => { this.tbMarkdownInstanceComponentFactory = factory; const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index ed962b2fc1..5f0f7f37ef 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -23,6 +23,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { CustomerId } from '@shared/models/id/customer-id'; import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; +import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; +import { UserId } from '@shared/models/id/user-id'; export enum AlarmSeverity { CRITICAL = 'CRITICAL', @@ -89,6 +91,7 @@ export const alarmSeverityColors = new Map( export interface Alarm extends BaseData { tenantId: TenantId; customerId: CustomerId; + assigneeId: UserId; type: string; originator: EntityId; severity: AlarmSeverity; @@ -97,12 +100,43 @@ export interface Alarm extends BaseData { endTs: number; ackTs: number; clearTs: number; + assignTs: number; propagate: boolean; details?: any; } +export enum AlarmCommentType { + SYSTEM = 'SYSTEM', + OTHER = 'OTHER' +} + +export interface AlarmComment extends BaseData { + alarmId: AlarmId; + userId?: UserId; + type: AlarmCommentType; + comment: { + text: string; + edited?: boolean; + editedOn?: number; + } +} + +export interface AlarmCommentInfo extends AlarmComment { + firstName?: string; + lastName?: string; + email?: string; +} + export interface AlarmInfo extends Alarm { originatorName: string; + originatorLabel: string; + assignee: AlarmAssignee; +} + +export interface AlarmAssignee { + firstName: string; + lastName: string; + email: string; } export interface AlarmDataInfo extends AlarmInfo { @@ -115,12 +149,20 @@ export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), customerId: new CustomerId(NULL_UUID), + assigneeId: new UserId(NULL_UUID), createdTime: new Date().getTime(), startTs: new Date().getTime(), endTs: 0, ackTs: 0, clearTs: 0, + assignTs: 0, originatorName: 'Simulated', + originatorLabel: 'Simulated', + assignee: { + firstName: "", + lastName: "", + email: "test@example.com", + }, originator: { entityType: EntityType.DEVICE, id: '1' @@ -172,11 +214,22 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { name: 'alarm.clear-time', time: true }, + assignTime: { + keyName: 'assignTime', + value: 'assignTs', + name: 'alarm.assign-time', + time: true + }, originator: { keyName: 'originator', value: 'originatorName', name: 'alarm.originator' }, + originatorLabel: { + keyName: 'originatorLabel', + value: 'originatorLabel', + name: 'alarm.originator-label' + }, originatorType: { keyName: 'originatorType', value: 'originator.entityType', @@ -196,6 +249,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'status', value: 'status', name: 'alarm.status' + }, + assignee: { + keyName: 'assignee', + value: 'assignee', + name: 'alarm.assignee' } }; @@ -206,15 +264,17 @@ export class AlarmQuery { searchStatus: AlarmSearchStatus; status: AlarmStatus; fetchOriginator: boolean; + assigneeId?: UserId; constructor(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, - fetchOriginator: boolean) { + fetchOriginator: boolean, assigneeId?: UserId) { this.affectedEntityId = entityId; this.pageLink = pageLink; this.searchStatus = searchStatus; this.status = status; this.fetchOriginator = fetchOriginator; + this.assigneeId = assigneeId; } public toQuery(): string { @@ -228,6 +288,9 @@ export class AlarmQuery { if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { query += `&fetchOriginator=${this.fetchOriginator}`; } + if (typeof this.assigneeId !== 'undefined' && this.assigneeId !== null) { + query += `&assigneeId=${this.assigneeId.id}`; + } return query; } diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 096e13a47c..8f4e26171e 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -47,6 +47,8 @@ export enum ActionType { RELATIONS_DELETED = 'RELATIONS_DELETED', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ALARM_ASSIGN = 'ALARM_ASSIGN', + ALARM_UNASSIGN = 'ALARM_UNASSIGN', ADDED_COMMENT = 'ADDED_COMMENT', UPDATED_COMMENT = 'UPDATED_COMMENT', DELETED_COMMENT = 'DELETED_COMMENT', @@ -88,6 +90,8 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_ASSIGN, 'audit-log.type-alarm-assign'], + [ActionType.ALARM_UNASSIGN, 'audit-log.type-alarm-unassign'], [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], [ActionType.UPDATED_COMMENT, 'audit-log.type-updated-comment'], [ActionType.DELETED_COMMENT, 'audit-log.type-deleted-comment'], diff --git a/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts new file mode 100644 index 0000000000..c161e3095c --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class AlarmCommentId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts index 7bc44e8915..02a8c657f9 100644 --- a/ui-ngx/src/app/shared/models/user.model.ts +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -55,3 +55,10 @@ export interface AuthUser { isPublic: boolean; authority: Authority; } + +export interface UserEmailInfo { + id: UserId; + email: string; + firstName: string; + lastName: string; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index e4be6d7a67..6108fdbeaf 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -190,6 +190,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) FileSizePipe, DateAgoPipe, SafePipe, + DateAgoPipe, { provide: FlowInjectionToken, useValue: Flow @@ -321,7 +322,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) BranchAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, - NotificationComponent + NotificationComponent, + DateAgoPipe ], imports: [ CommonModule, @@ -532,7 +534,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) BranchAutocompleteComponent, PhoneInputComponent, TbScriptLangComponent, - NotificationComponent + NotificationComponent, + DateAgoPipe ] }) export class SharedModule { } 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 f076d64196..8fdb965dd6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -443,9 +443,19 @@ "originator": "Originator", "originator-type": "Originator type", "details": "Details", + "originator-label": "Originator label", + "assign": "Assign", + "assignments": "Assignments", + "assignee": "Assignee", + "assignee-id": "Assignee id", + "assignee-first-name": "Assignee first name", + "assignee-last-name": "Assignee last name", + "assignee-email": "Assignee email", + "unassigned": "Unassigned", "status": "Status", "alarm-details": "Alarm details", "start-time": "Start time", + "assign-time": "Assign time", "end-time": "End time", "ack-time": "Acknowledged time", "clear-time": "Cleared time", @@ -482,7 +492,17 @@ "fetch-size-error-min": "Minimum value is 10.", "alarm-type-list": "Alarm type list", "any-type": "Any type", - "search-propagated-alarms": "Search propagated alarms" + "search-propagated-alarms": "Search propagated alarms", + "comments": "Alarm comments", + "advanced-info": "Advanced info" + }, + "alarm-comment": { + "add": "Add a comment...", + "alarm-comment": "Alarm comment", + "comments": "Comments", + "delete-alarm-comment": "Do you want to delete this comment?", + "refresh": "Refresh", + "sort-direction": "Sort direction" }, "alias": { "add": "Add alias", @@ -736,6 +756,8 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-alarm-assign": "Assigned", + "type-alarm-unassign": "Unassigned", "type-added-comment": "Added comment", "type-updated-comment": "Updated comment", "type-deleted-comment": "Deleted comment", @@ -4934,7 +4956,9 @@ "enable-alarm-filter": "Enable alarm filter", "display-alarm-details": "Display alarm details", "allow-alarms-ack": "Allow alarms acknowledgment", - "allow-alarms-clear": "Allow alarms clear" + "allow-alarms-clear": "Allow alarms clear", + "display-alarm-comments": "Display alarm comments", + "allow-alarms-assign": "Allow alarms assignment" }, "value-source": { "value-source": "Value source", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 42fa7ec965..2704c7d522 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -424,345 +424,24 @@ mat-label { } } -.tb-default .tb-markdown-view, .tb-dark .tb-markdown-view, .tb-markdown-view { - display: block; - - $headings: h1, h2, h3, h4, h5, h6; - - h1 { - font-size: 32px; - padding-right: 60px; - } - - @each $heading in $headings { - #{$heading} { - color: #0F161D; - line-height: normal; - font-weight: 500; - border-bottom: none; - margin: 0; - } - & > #{$heading} { - padding: 30px 32px 10px; - } - } - - p { - font-size: 16px; - font-weight: 400; - line-height: 1.25em; - margin: 0; - } - - p + p { - margin-top: 10px; - } - - p, div { - color: rgba(15, 22, 29, 0.8); - line-height: 1.5em; - } - - & > p, & > div { - padding-right: 32px; - padding-left: 32px; - } - - ul { - padding-left: 62px; - padding-right: 32px; - color: rgba(15, 22, 29, 0.8); - margin-top: 16px; - margin-bottom: 16px; - } - - ul { - @each $heading in $headings { - & + #{$heading} { - padding-top: 14px; - } - } - } - - li { - padding-bottom: .75em; - line-height: 1.5em; - ul { - margin-bottom: 0; - } - p { - padding-left: 0; - } - } - - a { - font-weight: 500; - color: #2a7dec; - text-decoration: none; - border: none; - &:hover { - color: #2a7dec; - text-decoration: underline; - border: none; - } - } - - & > table { - margin-left: 32px; - width: calc(100% - 64px); - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - border-collapse: unset; - border-spacing: 0; - margin-top: 30px; - margin-bottom: 30px; - overflow: hidden; - table-layout: fixed; - &.auto { - table-layout: auto; - } - - & > thead { - background-color: #f9fbff; - color: rgba(33,37,41,.6); - - & > tr { - & > th { - border-bottom: 1px solid rgba(42,125,236,.2); - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - - & > tbody { - & > tr:not(:last-child) { - & > td { - border-bottom: 1px solid rgba(42,125,236,.2); - } - } - & > tr { - & > td { - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - color: rgba(15, 22, 29, 0.8); - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - th, td { - font-size: .85em; - padding: 8px; - margin: 0; - text-align: left; - } - td[align=center], th[align=center] { - text-align: center; - } - - td[align=right], th[align=right] { - text-align: right; - } - - tr td div { - padding-left: 0; - padding-right: 0; - } - } - - - div.divider { - padding-top: 32px; - border-bottom: 1px solid rgba(15, 22, 29, 0.1); - } - - ul + div.divider { - padding-top: 16px; - } - - img { - max-width: 100%; - } - - button.tb-button { - cursor: pointer; - display: inline-block; - border-radius: 4px; - border: none; - padding: 10px 20px; - line-height: 24px; - color: #fff; - background-color: #305680; - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); - text-decoration: none; - font-size: 16px; - font-weight: 500; - transition: background-color .4s cubic-bezier(.25,.8,.25,1); - } - - button.tb-button:hover { - background-color: #264363; - color: #fff; - text-decoration: none; - } - - #video { - width: 100%; - margin: 0; - position: relative; - } - - - #video #video_wrapper { - position: relative; - width: 100%; - padding-bottom: 56.25%; - padding-left: 0; - padding-right: 0; - } - - #video #video_wrapper iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - @media screen and (min-width: 750px) { - #video { - width: 100%; - display: block; - } - } - - @media screen and (min-width: 1025px) { - #video { - width: 50%; - position: relative; - } - } - - code:not([class*=language-]) { - color: #eb5757; - font-family: monospace; - font-size: 16px; - } - - div.code-wrapper { - position: relative; - button.clipboard-btn { - pointer-events: none; - outline: none; - position: absolute; - width: 206px; - height: 42px; - top: 0; - right: 32px; - background: 0 0; - border: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - &.multiline { - right: 44px; - } - p { - padding: 8px; - top: 1px; - transition: .2s; - color: #2a7dec; - background: rgba(255,255,255,.85); - backdrop-filter: blur(4px); - opacity: 0; - font-weight: 500; - right: 32px; - position: absolute; - } - div { - background-color: #fff; - position: absolute; - width: 38px; - height: 38px; - top: 3px; - right: 3px; - padding: 10px; - img { - position: initial; - width: 18px; - height: 18px; - filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); - } - } - } - - &:hover { - cursor: pointer; - pre[class*="language-"] { - border: solid 1px #2a7dec; - } - button.clipboard-btn { - p { - opacity: 1; - } - div { - img { - filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); - } - } - } - } - } - - th, td { - div.code-wrapper { - display: inline-block; - width: 100%; - - button.clipboard-btn { - top: -10px; - right: 0; - padding: 0 3px; - } - } - } - +.tb-markdown-view { pre[class*="language-"] { - font-size: 16px; - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - background: 0 0; - padding: 8px 16px; - color: #212529; + .token.atrule, .token.attr-value, .token.keyword { color: #2a7dec; } + .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #eb5757; } + .token.punctuation { color: #212529; } + &.line-numbers { padding-left: 66px; + & > code { span.line-numbers-rows { top: -12px; @@ -773,14 +452,17 @@ mat-label { padding: 8px 12px 8px 18px; text-align: right; background: #f9fbff; + & > span:before { - color: rgba(33,37,41,.6); + color: rgba(33, 37, 41, .6); padding-right: 0; } } } + &.no-line-numbers { padding-left: 16px; + & > code { span.line-numbers-rows { display: none;