Browse Source

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

pull/2664/head
Adsumus 7 years ago
parent
commit
76a8a60d76
  1. 6
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 123
      application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
  3. 125
      application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql
  4. 1
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  5. 8
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  6. 11
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  7. 35
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  8. 24
      application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java
  9. 66
      application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
  10. 54
      application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
  11. 89
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractTimeseriesCleanUpService.java
  12. 41
      application/src/main/java/org/thingsboard/server/service/ttl/PsqlTimeseriesCleanUpService.java
  13. 35
      application/src/main/java/org/thingsboard/server/service/ttl/TimescaleTimeseriesCleanUpService.java
  14. 4
      application/src/main/resources/thingsboard.yml
  15. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  16. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java
  17. 120
      dao/src/main/resources/sql/schema-timescale.sql
  18. 271
      dao/src/main/resources/sql/schema-ts-psql.sql
  19. 8
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  20. 4
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  21. 4
      tools/src/main/shell/client.keygen.sh
  22. 8
      tools/src/main/shell/server.keygen.sh
  23. 37
      ui-ngx/package-lock.json
  24. 2
      ui-ngx/package.json
  25. 18
      ui-ngx/src/app/app.module.ts
  26. 5
      ui-ngx/src/app/core/auth/auth.service.ts
  27. 7
      ui-ngx/src/app/core/core.module.ts
  28. 5
      ui-ngx/src/app/core/http/entity.service.ts
  29. 76
      ui-ngx/src/app/core/translate/translate-default-parser.ts
  30. 7
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  31. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  32. 4
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html
  33. 12
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss
  34. 5
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  35. 5
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts
  36. 4
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  37. 82
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  38. 43
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  39. 6
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  40. 6
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html
  41. 10
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  42. 2
      ui-ngx/src/app/modules/home/components/contact.component.html
  43. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  44. 12
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  45. 3
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss
  46. 54
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts
  47. 7
      ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts
  48. 69
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  49. 28
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  50. 178
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  51. 6
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html
  52. 91
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  53. 2
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  54. 9
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  55. 21
      ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts
  56. 31
      ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts
  57. 42
      ui-ngx/src/app/modules/home/components/entity/entity.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  59. 4
      ui-ngx/src/app/modules/home/components/event/event-table-header.component.html
  60. 12
      ui-ngx/src/app/modules/home/components/event/event-table-header.component.scss
  61. 12
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  62. 53
      ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html
  63. 1
      ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss
  64. 8
      ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html
  65. 18
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  66. 45
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  67. 10
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html
  68. 25
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts
  69. 50
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  70. 37
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  71. 36
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw
  72. 646
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw
  73. 562
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw
  74. 262
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html
  75. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss
  76. 408
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts
  77. 374
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts
  78. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  79. 17
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  80. 1
      ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts
  81. 11
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  82. 28
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  83. 8
      ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html
  84. 21
      ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts
  85. 10
      ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html
  86. 21
      ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts
  87. 4
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts
  88. 3
      ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts
  89. 11
      ui-ngx/src/app/modules/home/home.component.html
  90. 12
      ui-ngx/src/app/modules/home/home.component.ts
  91. 10
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  92. 14
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  93. 30
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss
  94. 43
      ui-ngx/src/app/modules/home/menu/side-menu.component.scss
  95. 12
      ui-ngx/src/app/modules/home/models/datasource/entity-datasource.ts
  96. 70
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  97. 44
      ui-ngx/src/app/modules/home/models/services.map.ts
  98. 6
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  99. 6
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  100. 5
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts

6
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -13,13 +13,13 @@
"sizeX": 8, "sizeX": 8,
"sizeY": 6.5, "sizeY": 6.5,
"resources": [], "resources": [],
"templateHtml": "<tb-gateway-form\n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-gateway-form>\n", "templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.formId = \"form-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n self.ctx.$scope.$broadcast('gateway-form-resize', self.ctx.$scope.formId);\n}\n", "controllerScript": "self.onInit = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
} }
} }
] ]
} }

123
application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql

@ -0,0 +1,123 @@
--
-- Copyright © 2016-2020 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.
--
CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_month varchar;
partition_day varchar;
partition_year varchar;
partition varchar;
partition_to_delete varchar;
BEGIN
SELECT max(attribute_kv.long_v)
FROM tenant
INNER JOIN attribute_kv ON tenant.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_tenant_ttl;
SELECT max(attribute_kv.long_v)
FROM customer
INNER JOIN attribute_kv ON customer.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_customer_ttl;
max_ttl := GREATEST(system_ttl, max_customer_ttl, max_tenant_ttl);
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - (max_ttl / 1000));
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 3)::integer < partition_year::integer THEN
partition_to_delete := partition;
ELSE
IF partition_month IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 4)::integer < partition_month::integer THEN
partition_to_delete := partition;
ELSE
IF partition_day IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 5)::integer < partition_day::integer THEN
partition_to_delete := partition;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
IF partition_to_delete IS NOT NULL THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete;
EXECUTE format('DROP TABLE %I', partition_to_delete);
deleted := deleted + 1;
END IF;
END LOOP;
END IF;
END IF;
END
$$;
CREATE OR REPLACE FUNCTION get_partition_by_max_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
WHEN partition_type = 'INDEFINITE' THEN
partition := NULL;
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;

125
application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql

@ -0,0 +1,125 @@
--
-- Copyright © 2016-2020 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.
--
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

1
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -107,7 +107,6 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return filter; return filter;
} }
@Bean
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,

8
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -120,7 +120,9 @@ public class AlarmController extends BaseController {
try { try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, System.currentTimeMillis()).get(); long ackTs = System.currentTimeMillis();
alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get();
alarm.setAckTs(ackTs);
logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null); logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
@ -135,7 +137,9 @@ public class AlarmController extends BaseController {
try { try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, System.currentTimeMillis()).get(); long clearTs = System.currentTimeMillis();
alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get();
alarm.setClearTs(clearTs);
logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null); logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);

11
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -199,6 +199,7 @@ public class AuthController extends BaseController {
@ResponseBody @ResponseBody
public JsonNode activateUser( public JsonNode activateUser(
@RequestBody JsonNode activateRequest, @RequestBody JsonNode activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
String activateToken = activateRequest.get("activateToken").asText(); String activateToken = activateRequest.get("activateToken").asText();
@ -213,10 +214,12 @@ public class AuthController extends BaseController {
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();
try { if (sendActivationMail) {
mailService.sendAccountActivatedEmail(loginUrl, email); try {
} catch (Exception e) { mailService.sendAccountActivatedEmail(loginUrl, email);
log.info("Unable to send account activation email [{}]", e.getMessage()); } catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
} }
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);

35
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -24,13 +24,11 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
@ -38,6 +36,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@RestController @RestController
@ -167,7 +166,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findByFrom(getTenantId(), entityId, typeGroup)); return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup)));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -185,7 +184,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get()); return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -205,7 +204,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup)); return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup)));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -223,7 +222,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findByTo(getTenantId(), entityId, typeGroup)); return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup)));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -241,7 +240,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get()); return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -261,7 +260,7 @@ public class EntityRelationController extends BaseController {
checkEntityId(entityId, Operation.READ); checkEntityId(entityId, Operation.READ);
RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
try { try {
return checkNotNull(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup)); return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup)));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -276,7 +275,7 @@ public class EntityRelationController extends BaseController {
checkNotNull(query.getFilters()); checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ); checkEntityId(query.getParameters().getEntityId(), Operation.READ);
try { try {
return checkNotNull(relationService.findByQuery(getTenantId(), query).get()); return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
@ -291,12 +290,28 @@ public class EntityRelationController extends BaseController {
checkNotNull(query.getFilters()); checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ); checkEntityId(query.getParameters().getEntityId(), Operation.READ);
try { try {
return checkNotNull(relationService.findInfoByQuery(getTenantId(), query).get()); return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByQuery(getTenantId(), query).get()));
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }
} }
private <T extends EntityRelation> List<T> filterRelationsByReadPermission(List<T> relationsByQuery) {
return relationsByQuery.stream().filter(relationByQuery -> {
try {
checkEntityId(relationByQuery.getTo(), Operation.READ);
} catch (ThingsboardException e) {
return false;
}
try {
checkEntityId(relationByQuery.getFrom(), Operation.READ);
} catch (ThingsboardException e) {
return false;
}
return true;
}).collect(Collectors.toList());
}
private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) { private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
RelationTypeGroup result = defaultValue; RelationTypeGroup result = defaultValue;
if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length() > 0) { if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length() > 0) {

24
application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java

@ -47,7 +47,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
@Autowired @Autowired
protected InstallScripts installScripts; protected InstallScripts installScripts;
protected abstract void loadSql(Connection conn); protected abstract void loadSql(Connection conn, String fileName);
protected void loadFunctions(Path sqlFile, Connection conn) throws Exception { protected void loadFunctions(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8); String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8);
@ -70,6 +70,26 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
return versionValid; return versionValid;
} }
protected boolean isOldSchema(Connection conn, long fromVersion) {
boolean isOldSchema = true;
try {
Statement statement = conn.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings ( schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version));");
Thread.sleep(1000);
ResultSet resultSet = statement.executeQuery("SELECT schema_version FROM tb_schema_settings;");
if (resultSet.next()) {
isOldSchema = resultSet.getLong(1) <= fromVersion;
} else {
resultSet.close();
statement.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")");
}
statement.close();
} catch (InterruptedException | SQLException e) {
log.info("Failed to check current PostgreSQL schema due to: {}", e.getMessage());
}
return isOldSchema;
}
protected void executeQuery(Connection conn, String query) { protected void executeQuery(Connection conn, String query) {
try { try {
Statement statement = conn.createStatement(); Statement statement = conn.createStatement();
@ -83,7 +103,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
nextWarning = nextWarning.getNextWarning(); nextWarning = nextWarning.getNextWarning();
} }
} }
Thread.sleep(5000); Thread.sleep(2000);
log.info("Successfully executed query: {}", query); log.info("Successfully executed query: {}", query);
} catch (InterruptedException | SQLException e) { } catch (InterruptedException | SQLException e) {
log.info("Failed to execute query: {} due to: {}", query, e.getMessage()); log.info("Failed to execute query: {} due to: {}", query, e.getMessage());

66
application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java

@ -34,6 +34,8 @@ import java.sql.DriverManager;
public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService { public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
private static final String LOAD_FUNCTIONS_SQL = "schema_update_psql_ts.sql"; private static final String LOAD_FUNCTIONS_SQL = "schema_update_psql_ts.sql";
private static final String LOAD_TTL_FUNCTIONS_SQL = "schema_update_ttl.sql";
private static final String LOAD_DROP_PARTITIONS_FUNCTIONS_SQL = "schema_update_psql_drop_partitions.sql";
private static final String TS_KV_OLD = "ts_kv_old;"; private static final String TS_KV_OLD = "ts_kv_old;";
private static final String TS_KV_LATEST_OLD = "ts_kv_latest_old;"; private static final String TS_KV_LATEST_OLD = "ts_kv_latest_old;";
@ -76,30 +78,39 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!"); throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!");
} else { } else {
log.info("PostgreSQL version is valid!"); log.info("PostgreSQL version is valid!");
log.info("Load upgrade functions ..."); if (isOldSchema(conn, 2004003)) {
loadSql(conn); log.info("Load upgrade functions ...");
log.info("Updating timeseries schema ..."); loadSql(conn, LOAD_FUNCTIONS_SQL);
executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE); log.info("Updating timeseries schema ...");
executeQuery(conn, CALL_CREATE_PARTITIONS); executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, CALL_CREATE_PARTITIONS);
executeQuery(conn, CALL_INSERT_INTO_DICTIONARY); executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, CALL_INSERT_INTO_TS_KV); executeQuery(conn, CALL_INSERT_INTO_DICTIONARY);
executeQuery(conn, CALL_CREATE_NEW_TS_KV_LATEST_TABLE); executeQuery(conn, CALL_INSERT_INTO_TS_KV);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, CALL_CREATE_NEW_TS_KV_LATEST_TABLE);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, DROP_TABLE_TS_KV_OLD);
executeQuery(conn, DROP_TABLE_TS_KV_LATEST_OLD); executeQuery(conn, DROP_TABLE_TS_KV_OLD);
executeQuery(conn, DROP_TABLE_TS_KV_LATEST_OLD);
executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITIONS); executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITIONS);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY);
executeQuery(conn, DROP_PROCEDURE_CREATE_NEW_TS_KV_LATEST_TABLE); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, DROP_PROCEDURE_CREATE_NEW_TS_KV_LATEST_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN IF NOT EXISTS json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN IF NOT EXISTS json_v json;");
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
log.info("Load Drop Partitions functions ...");
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL);
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
log.info("schema timeseries updated!"); log.info("schema timeseries updated!");
} }
@ -110,11 +121,12 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
} }
} }
protected void loadSql(Connection conn) { @Override
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", LOAD_FUNCTIONS_SQL); protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
try { try {
loadFunctions(schemaUpdateFile, conn); loadFunctions(schemaUpdateFile, conn);
log.info("Upgrade functions successfully loaded!"); log.info("Functions successfully loaded!");
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
} }

54
application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java

@ -39,6 +39,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
private long chunkTimeInterval; private long chunkTimeInterval;
private static final String LOAD_FUNCTIONS_SQL = "schema_update_timescale_ts.sql"; private static final String LOAD_FUNCTIONS_SQL = "schema_update_timescale_ts.sql";
private static final String LOAD_TTL_FUNCTIONS_SQL = "schema_update_ttl.sql";
private static final String TENANT_TS_KV_OLD_TABLE = "tenant_ts_kv_old;"; private static final String TENANT_TS_KV_OLD_TABLE = "tenant_ts_kv_old;";
@ -79,31 +80,37 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!"); throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!");
} else { } else {
log.info("PostgreSQL version is valid!"); log.info("PostgreSQL version is valid!");
log.info("Load upgrade functions ..."); if (isOldSchema(conn, 2004003)) {
loadSql(conn); log.info("Load upgrade functions ...");
log.info("Updating timescale schema ..."); loadSql(conn, LOAD_FUNCTIONS_SQL);
executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE); log.info("Updating timescale schema ...");
executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE); executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE);
executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE);
executeQuery(conn, "SELECT create_hypertable('ts_kv', 'ts', chunk_time_interval => " + chunkTimeInterval + ", if_not_exists => true);"); executeQuery(conn, "SELECT create_hypertable('ts_kv', 'ts', chunk_time_interval => " + chunkTimeInterval + ", if_not_exists => true);");
executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, CALL_INSERT_INTO_DICTIONARY); executeQuery(conn, CALL_INSERT_INTO_DICTIONARY);
executeQuery(conn, CALL_INSERT_INTO_TS_KV); executeQuery(conn, CALL_INSERT_INTO_TS_KV);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, DROP_OLD_TENANT_TS_KV_TABLE); executeQuery(conn, DROP_OLD_TENANT_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_LATEST_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_LATEST_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TENANT_TS_KV_TABLE_COPY); executeQuery(conn, DROP_PROCEDURE_CREATE_TENANT_TS_KV_TABLE_COPY);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TENANT_TS_KV); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TENANT_TS_KV);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN IF NOT EXISTS json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN IF NOT EXISTS json_v json;");
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
log.info("schema timescale updated!"); log.info("schema timescale updated!");
} }
} }
@ -113,13 +120,14 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
} }
} }
protected void loadSql(Connection conn) { @Override
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", LOAD_FUNCTIONS_SQL); protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
try { try {
loadFunctions(schemaUpdateFile, conn); loadFunctions(schemaUpdateFile, conn);
log.info("Upgrade functions successfully loaded!"); log.info("Functions successfully loaded!");
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to load Timescale upgrade functions due to: {}", e.getMessage()); log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
} }
} }
} }

89
application/src/main/java/org/thingsboard/server/service/ttl/AbstractTimeseriesCleanUpService.java

@ -0,0 +1,89 @@
/**
* Copyright © 2016-2020 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.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.thingsboard.server.dao.util.PsqlTsAnyDao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
@PsqlTsAnyDao
@Slf4j
public abstract class AbstractTimeseriesCleanUpService {
@Value("${sql.ttl.ts_key_value_ttl}")
protected long systemTtl;
@Value("${sql.ttl.enabled}")
private boolean ttlTaskExecutionEnabled;
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String dbUserName;
@Value("${spring.datasource.password}")
private String dbPassword;
@Scheduled(initialDelayString = "${sql.ttl.execution_interval_ms}", fixedDelayString = "${sql.ttl.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
doCleanUp(conn);
} catch (SQLException e) {
log.error("SQLException occurred during TTL task execution ", e);
}
}
}
protected abstract void doCleanUp(Connection connection);
protected long executeQuery(Connection conn, String query) {
long removed = 0L;
try {
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery(query);
getWarnings(statement);
resultSet.next();
removed = resultSet.getLong(1);
log.debug("Successfully executed query: {}", query);
} catch (SQLException e) {
log.debug("Failed to execute query: {} due to: {}", query, e.getMessage());
}
return removed;
}
private void getWarnings(Statement statement) throws SQLException {
SQLWarning warnings = statement.getWarnings();
if (warnings != null) {
log.debug("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
while (nextWarning != null) {
log.debug("{}", nextWarning.getMessage());
nextWarning = nextWarning.getNextWarning();
}
}
}
}

41
application/src/main/java/org/thingsboard/server/service/ttl/PsqlTimeseriesCleanUpService.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 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.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.PsqlTsDao;
import java.sql.Connection;
@PsqlTsDao
@Service
@Slf4j
public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Value("${sql.postgres.ts_key_value_partitioning}")
private String partitionType;
@Override
protected void doCleanUp(Connection connection) {
long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);");
log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved);
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

35
application/src/main/java/org/thingsboard/server/service/ttl/TimescaleTimeseriesCleanUpService.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2020 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.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.sql.Connection;
@TimescaleDBTsDao
@Service
@Slf4j
public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Override
protected void doCleanUp(Connection connection) {
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

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

@ -214,6 +214,10 @@ sql:
timescale: timescale:
# Specify Interval size for new data chunks storage. # Specify Interval size for new data chunks storage.
chunk_time_interval: "${SQL_TIMESCALE_CHUNK_TIME_INTERVAL:604800000}" chunk_time_interval: "${SQL_TIMESCALE_CHUNK_TIME_INTERVAL:604800000}"
ttl:
enabled: "${SQL_TTL_ENABLED:true}"
execution_interval_ms: "${SQL_TTL_EXECUTION_INTERVAL:86400000}" # Number of miliseconds
ts_key_value_ttl: "${SQL_TTL_TS_KEY_VALUE_TTL:0}" # Number of seconds
# Actor system parameters # Actor system parameters
actors: actors:

2
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -39,7 +39,7 @@ public interface AlarmService {
ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long ackTs); ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId);

21
common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2020 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.util;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@ConditionalOnExpression("'${database.ts.type}'=='sql' && '${spring.jpa.database-platform}'=='org.hibernate.dialect.PostgreSQLDialect'")
public @interface PsqlTsDao { }

120
dao/src/main/resources/sql/schema-timescale.sql

@ -44,4 +44,122 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );
CREATE TABLE IF NOT EXISTS tb_schema_settings
(
schema_version bigint NOT NULL,
CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version)
);
INSERT INTO tb_schema_settings (schema_version) VALUES (2005000);
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

271
dao/src/main/resources/sql/schema-ts-psql.sql

@ -14,31 +14,260 @@
-- limitations under the License. -- limitations under the License.
-- --
CREATE TABLE IF NOT EXISTS ts_kv ( CREATE TABLE IF NOT EXISTS ts_kv
entity_id uuid NOT NULL, (
key int NOT NULL, entity_id uuid NOT NULL,
ts bigint NOT NULL, key int NOT NULL,
bool_v boolean, ts bigint NOT NULL,
str_v varchar(10000000), bool_v boolean,
long_v bigint, str_v varchar(10000000),
dbl_v double precision, long_v bigint,
json_v json dbl_v double precision,
json_v json
) PARTITION BY RANGE (ts); ) PARTITION BY RANGE (ts);
CREATE TABLE IF NOT EXISTS ts_kv_latest ( CREATE TABLE IF NOT EXISTS ts_kv_latest
entity_id uuid NOT NULL, (
key int NOT NULL, entity_id uuid NOT NULL,
ts bigint NOT NULL, key int NOT NULL,
bool_v boolean, ts bigint NOT NULL,
str_v varchar(10000000), bool_v boolean,
long_v bigint, str_v varchar(10000000),
dbl_v double precision, long_v bigint,
json_v json, dbl_v double precision,
json_v json,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );
CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( CREATE TABLE IF NOT EXISTS ts_kv_dictionary
key varchar(255) NOT NULL, (
key varchar(255) NOT NULL,
key_id serial UNIQUE, key_id serial UNIQUE,
CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) CONSTRAINT ts_key_id_pkey PRIMARY KEY (key)
); );
CREATE TABLE IF NOT EXISTS tb_schema_settings
(
schema_version bigint NOT NULL,
CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version)
);
INSERT INTO tb_schema_settings (schema_version) VALUES (2005000);
CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_month varchar;
partition_day varchar;
partition_year varchar;
partition varchar;
partition_to_delete varchar;
BEGIN
SELECT max(attribute_kv.long_v)
FROM tenant
INNER JOIN attribute_kv ON tenant.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_tenant_ttl;
SELECT max(attribute_kv.long_v)
FROM customer
INNER JOIN attribute_kv ON customer.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_customer_ttl;
max_ttl := GREATEST(system_ttl, max_customer_ttl, max_tenant_ttl);
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - (max_ttl / 1000));
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 3)::integer < partition_year::integer THEN
partition_to_delete := partition;
ELSE
IF partition_month IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 4)::integer < partition_month::integer THEN
partition_to_delete := partition;
ELSE
IF partition_day IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 5)::integer < partition_day::integer THEN
partition_to_delete := partition;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
IF partition_to_delete IS NOT NULL THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete;
EXECUTE format('DROP TABLE %I', partition_to_delete);
deleted := deleted + 1;
END IF;
END LOOP;
END IF;
END IF;
END
$$;
CREATE OR REPLACE FUNCTION get_partition_by_max_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
WHEN partition_type = 'INDEFINITE' THEN
partition := NULL;
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

8
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -407,8 +407,12 @@ final class MqttClientImpl implements MqttClient {
} }
private MqttMessageIdVariableHeader getNewMessageId() { private MqttMessageIdVariableHeader getNewMessageId() {
this.nextMessageId.compareAndSet(0xffff, 1); int messageId;
return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement()); synchronized (this.nextMessageId) {
this.nextMessageId.compareAndSet(0xffff, 1);
messageId = this.nextMessageId.getAndIncrement();
}
return MqttMessageIdVariableHeader.from(messageId);
} }
private Future<Void> createSubscription(String topic, MqttHandler handler, boolean once, MqttQoS qos) { private Future<Void> createSubscription(String topic, MqttHandler handler, boolean once, MqttQoS qos) {

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

File diff suppressed because one or more lines are too long

4
tools/src/main/shell/client.keygen.sh

@ -20,7 +20,7 @@ usage() {
echo "and imports server public key to client keystore" echo "and imports server public key to client keystore"
echo "usage: ./client.keygen.sh [-p file]" echo "usage: ./client.keygen.sh [-p file]"
echo " -p | --props | --properties file Properties file. default value is ./keygen.properties" echo " -p | --props | --properties file Properties file. default value is ./keygen.properties"
echo " -h | --help | ? Show this message" echo " -h | --help | ? Show this message"
} }
PROPERTIES_FILE=keygen.properties PROPERTIES_FILE=keygen.properties
@ -55,7 +55,7 @@ while :
echo "Done" echo "Done"
exit 0 exit 0
;; ;;
[yY]|[yY][eE]|[yY][eE]|[sS]|[yY]|"") [yY]|[yY][eE]|[yY][eE][sS]|"")
echo "Cleaning up files" echo "Cleaning up files"
rm -rf $CLIENT_FILE_PREFIX.jks rm -rf $CLIENT_FILE_PREFIX.jks
rm -rf $CLIENT_FILE_PREFIX.pub.pem rm -rf $CLIENT_FILE_PREFIX.pub.pem

8
tools/src/main/shell/server.keygen.sh

@ -23,7 +23,7 @@ usage() {
echo " -d | --dir directory Server keystore directory, where the generated keystore file will be copied. If specified, overrides the value from the properties file" echo " -d | --dir directory Server keystore directory, where the generated keystore file will be copied. If specified, overrides the value from the properties file"
echo " Default value is SERVER_KEYSTORE_DIR property from properties file" echo " Default value is SERVER_KEYSTORE_DIR property from properties file"
echo " -p | --props | --properties file Properties file. default value is ./keygen.properties" echo " -p | --props | --properties file Properties file. default value is ./keygen.properties"
echo " -h | --help | ? Show this message" echo " -h | --help | ? Show this message"
} }
COPY=true; COPY=true;
@ -71,7 +71,7 @@ while :
echo "Done" echo "Done"
exit 0 exit 0
;; ;;
[yY]|[yY][eE]|[yY][eE]|[sS]|[yY]|"") [yY]|[yY][eE]|[yY][eE][sS]|"")
echo "Cleaning up files" echo "Cleaning up files"
rm -rf $SERVER_FILE_PREFIX.jks rm -rf $SERVER_FILE_PREFIX.jks
rm -rf $SERVER_FILE_PREFIX.pub.pem rm -rf $SERVER_FILE_PREFIX.pub.pem
@ -129,7 +129,7 @@ if [[ $COPY = true ]]; then
[nN]|[nN][oO]) [nN]|[nN][oO])
break break
;; ;;
[yY]|[yY][eE]|[yY][eE]|[sS]|[yY]|"") [yY]|[yY][eE]|[yY][eE][sS]|"")
read -p "(Default: $SERVER_KEYSTORE_DIR): " dir read -p "(Default: $SERVER_KEYSTORE_DIR): " dir
if [[ ! -z $dir ]]; then if [[ ! -z $dir ]]; then
DESTINATION=$dir; DESTINATION=$dir;
@ -155,4 +155,4 @@ if [[ $COPY = true ]]; then
fi fi
fi fi
fi fi
echo "Done." echo "Done."

37
ui-ngx/package-lock.json

@ -1657,6 +1657,15 @@
"@types/jquery": "*" "@types/jquery": "*"
} }
}, },
"@types/jszip": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.1.7.tgz",
"integrity": "sha512-+XQKNI5zpxutK05hO67huUTw/2imXCuJWjnFdU63tRES/xXSX1yVR9cv/QAdO6Rii2y2tTHbzjQ4i2apLfuK0Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/leaflet": { "@types/leaflet": {
"version": "1.5.12", "version": "1.5.12",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz",
@ -3701,8 +3710,7 @@
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
"dev": true
}, },
"cosmiconfig": { "cosmiconfig": {
"version": "5.2.1", "version": "5.2.1",
@ -6017,8 +6025,7 @@
"immediate": { "immediate": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
"dev": true
}, },
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
@ -6503,8 +6510,7 @@
"isarray": { "isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
"dev": true
}, },
"isbinaryfile": { "isbinaryfile": {
"version": "3.0.3", "version": "3.0.3",
@ -7001,7 +7007,6 @@
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
"integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==", "integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
"dev": true,
"requires": { "requires": {
"lie": "~3.3.0", "lie": "~3.3.0",
"pako": "~1.0.2", "pako": "~1.0.2",
@ -7236,7 +7241,6 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": { "requires": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
@ -8748,8 +8752,7 @@
"pako": { "pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
"dev": true
}, },
"parallel-transform": { "parallel-transform": {
"version": "1.2.0", "version": "1.2.0",
@ -9598,8 +9601,7 @@
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
"dev": true
}, },
"promise": { "promise": {
"version": "7.3.1", "version": "7.3.1",
@ -10238,7 +10240,6 @@
"version": "2.3.7", "version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": { "requires": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@ -10569,8 +10570,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"dev": true
}, },
"safe-regex": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",
@ -10898,8 +10898,7 @@
"set-immediate-shim": { "set-immediate-shim": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
"dev": true
}, },
"set-value": { "set-value": {
"version": "2.0.1", "version": "2.0.1",
@ -11725,7 +11724,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": { "requires": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@ -12623,8 +12621,7 @@
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"dev": true
}, },
"util-promisify": { "util-promisify": {
"version": "2.1.0", "version": "2.1.0",

2
ui-ngx/package.json

@ -56,6 +56,7 @@
"json-schema-defaults": "^0.4.0", "json-schema-defaults": "^0.4.0",
"jstree": "^3.3.9", "jstree": "^3.3.9",
"jstree-bootstrap-theme": "^1.0.1", "jstree-bootstrap-theme": "^1.0.1",
"jszip": "^3.2.2",
"leaflet": "^1.6.0", "leaflet": "^1.6.0",
"leaflet-geometryutil": "^0.9.3", "leaflet-geometryutil": "^0.9.3",
"leaflet-polylinedecorator": "^1.6.0", "leaflet-polylinedecorator": "^1.6.0",
@ -105,6 +106,7 @@
"@types/jquery": "^3.3.32", "@types/jquery": "^3.3.32",
"@types/js-beautify": "^1.8.1", "@types/js-beautify": "^1.8.1",
"@types/jstree": "^3.3.39", "@types/jstree": "^3.3.39",
"@types/jszip": "^3.1.7",
"@types/leaflet": "^1.5.9", "@types/leaflet": "^1.5.9",
"@types/leaflet-polylinedecorator": "^1.6.0", "@types/leaflet-polylinedecorator": "^1.6.0",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",

18
ui-ngx/src/app/app.module.ts

@ -25,6 +25,21 @@ import { HomeModule } from '@home/home.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module'; import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: '**',
redirectTo: 'home'
}
];
@NgModule({
imports: [
RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PageNotFoundRoutingModule { }
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -36,8 +51,9 @@ import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.mod
AppRoutingModule, AppRoutingModule,
CoreModule, CoreModule,
LoginModule, LoginModule,
HomeModule,
DashboardRoutingModule, DashboardRoutingModule,
HomeModule PageNotFoundRoutingModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

5
ui-ngx/src/app/core/auth/auth.service.ts

@ -127,8 +127,9 @@ export class AuthService {
{email}, defaultHttpOptions()); {email}, defaultHttpOptions());
} }
public activate(activateToken: string, password: string): Observable<LoginResponse> { public activate(activateToken: string, password: string, sendActivationMail: boolean): Observable<LoginResponse> {
return this.http.post<LoginResponse>('/api/noauth/activate', {activateToken, password}, defaultHttpOptions()).pipe( return this.http.post<LoginResponse>(`/api/noauth/activate?sendActivationMail=${sendActivationMail}`,
{activateToken, password}, defaultHttpOptions()).pipe(
tap((loginResponse: LoginResponse) => { tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true);
} }

7
ui-ngx/src/app/core/core.module.ts

@ -28,7 +28,7 @@ import {
MissingTranslationHandler, MissingTranslationHandler,
TranslateCompiler, TranslateCompiler,
TranslateLoader, TranslateLoader,
TranslateModule TranslateModule, TranslateParser
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
@ -39,6 +39,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler';
import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { WINDOW_PROVIDERS } from '@core/services/window.service';
import { HotkeyModule } from 'angular2-hotkeys'; import { HotkeyModule } from 'angular2-hotkeys';
import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
export function HttpLoaderFactory(http: HttpClient) { export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
@ -67,6 +68,10 @@ export function HttpLoaderFactory(http: HttpClient) {
compiler: { compiler: {
provide: TranslateCompiler, provide: TranslateCompiler,
useClass: TranslateDefaultCompiler useClass: TranslateDefaultCompiler
},
parser: {
provide: TranslateParser,
useClass: TranslateDefaultParser
} }
}), }),
HotkeyModule.forRoot(), HotkeyModule.forRoot(),

5
ui-ngx/src/app/core/http/entity.service.ts

@ -480,6 +480,7 @@ export class EntityService {
entityTypes.push(EntityType.DASHBOARD); entityTypes.push(EntityType.DASHBOARD);
if (useAliasEntityTypes) { if (useAliasEntityTypes) {
entityTypes.push(AliasEntityType.CURRENT_CUSTOMER); entityTypes.push(AliasEntityType.CURRENT_CUSTOMER);
entityTypes.push(AliasEntityType.CURRENT_TENANT);
} }
break; break;
case Authority.CUSTOMER_USER: case Authority.CUSTOMER_USER:
@ -1065,6 +1066,10 @@ export class EntityService {
if (authUser.authority === Authority.CUSTOMER_USER) { if (authUser.authority === Authority.CUSTOMER_USER) {
entityId.id = authUser.customerId; entityId.id = authUser.customerId;
} }
} else if (entityType === AliasEntityType.CURRENT_TENANT){
const authUser = getCurrentAuthUser(this.store);
entityId.entityType = EntityType.TENANT;
entityId.id = authUser.tenantId;
} }
return entityId; return entityId;
} }

76
ui-ngx/src/app/core/translate/translate-default-parser.ts

@ -0,0 +1,76 @@
///
/// Copyright © 2016-2020 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 { TranslateParser } from '@ngx-translate/core';
import { isDefinedAndNotNull } from '@core/utils';
@Injectable({ providedIn: 'root' })
export class TranslateDefaultParser extends TranslateParser {
templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g;
// tslint:disable-next-line:ban-types
public interpolate(expr: string | Function, params?: any): string {
let result: string;
if (typeof expr === 'string') {
result = this.interpolateString(expr, params);
} else if (typeof expr === 'function') {
result = this.interpolateFunction(expr, params);
if (typeof result === 'string') {
result = this.interpolateString(result, params);
}
} else {
result = expr as string;
}
return result;
}
getValue(target: any, key: string): any {
const keys = typeof key === 'string' ? key.split('.') : [key];
key = '';
do {
key += keys.shift();
if (isDefinedAndNotNull(target) && isDefinedAndNotNull(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
target = target[key];
key = '';
} else if (!keys.length) {
target = undefined;
} else {
key += '.';
}
} while (keys.length);
return target;
}
// tslint:disable-next-line:ban-types
private interpolateFunction(fn: Function, params?: any) {
return fn(params);
}
private interpolateString(expr: string, params?: any) {
if (!params) {
return expr;
}
return expr.replace(this.templateMatcher, (substring: string, b: string) => {
const r = this.getValue(params, b);
return isDefinedAndNotNull(r) ? r : substring;
});
}
}

7
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -16,10 +16,10 @@
--> -->
<form [formGroup]="alarmFormGroup" style="min-width: 600px;"> <form [formGroup]="alarmFormGroup" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2>{{ 'alarm.alarm-details' | translate }}</h2> <h2>{{ 'alarm.alarm-details' | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-icon-button
(click)="close()" (click)="close()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -91,7 +91,6 @@
<div fxLayout="row" *ngIf="alarm$ | async; let alarm;"> <div fxLayout="row" *ngIf="alarm$ | async; let alarm;">
<button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK || <button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||
alarm.status === alarmStatuses.CLEARED_UNACK)" alarm.status === alarmStatuses.CLEARED_UNACK)"
mat-button
mat-raised-button mat-raised-button
color="primary" color="primary"
type="button" type="button"
@ -102,7 +101,6 @@
</button> </button>
<button *ngIf="allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK || <button *ngIf="allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK ||
alarm.status === alarmStatuses.ACTIVE_UNACK)" alarm.status === alarmStatuses.ACTIVE_UNACK)"
mat-button
mat-raised-button mat-raised-button
color="primary" color="primary"
type="button" type="button"
@ -113,7 +111,6 @@
</div> </div>
<span fxFlex></span> <span fxFlex></span>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial> (click)="close()" cdkFocusInitial>

6
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -39,10 +39,6 @@ import {
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component'; import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component';
import {
AuditLogDetailsDialogComponent,
AuditLogDetailsDialogData
} from '@home/components/audit-log/audit-log-details-dialog.component';
import { import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AlarmDetailsDialogData AlarmDetailsDialogData
@ -72,7 +68,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.entityType = EntityType.ALARM; this.entityType = EntityType.ALARM;
this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM);
this.entityResources = { this.entityResources = {
} as EntityTypeResource; } as EntityTypeResource<AlarmInfo>;
this.searchStatus = defaultSearchStatus; this.searchStatus = defaultSearchStatus;
this.headerComponent = AlarmTableHeaderComponent; this.headerComponent = AlarmTableHeaderComponent;

4
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field class="mat-block" style="width: 200px;"> <mat-form-field class="mat-block">
<mat-label translate>alarm.alarm-status</mat-label> <mat-label translate>alarm.alarm-status</mat-label>
<mat-select matInput [ngModel]="alarmTableConfig.searchStatus" <mat-select [ngModel]="alarmTableConfig.searchStatus"
(ngModelChange)="searchStatusChanged($event)"> (ngModelChange)="searchStatusChanged($event)">
<mat-option *ngFor="let status of alarmSearchStatusTypes" [value]="status"> <mat-option *ngFor="let status of alarmSearchStatusTypes" [value]="status">
{{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }} {{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }}

12
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss

@ -13,16 +13,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants';
:host { :host {
/* flex: 1; /* flex: 1;
display: flex; display: flex;
justify-content: flex-start; */ justify-content: flex-start; */
padding-right: 8px; padding-right: 8px;
min-width: 160px;
} }
:host ::ng-deep { :host ::ng-deep {
mat-form-field { mat-form-field {
font-size: 16px; font-size: 16px;
width: 200px;
.mat-form-field-wrapper { .mat-form-field-wrapper {
padding-bottom: 0; padding-bottom: 0;
@ -31,5 +35,13 @@
.mat-form-field-underline { .mat-form-field-underline {
bottom: 0; bottom: 0;
} }
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
} }
} }

5
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts

@ -20,8 +20,6 @@ import { DatePipe } from '@angular/common';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
@ -70,8 +68,7 @@ export class AlarmTableComponent implements OnInit {
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog) {
private store: Store<AppState>) {
} }
ngOnInit() { ngOnInit() {

5
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts

@ -14,11 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, InjectionToken, OnInit } from '@angular/core'; import { Component, Inject, InjectionToken } from '@angular/core';
import { Timewindow } from '@shared/models/time/time.models';
import { AliasInfo, IAliasController } from '@core/api/widget-api.models'; import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
import { PageComponent } from '@shared/components/page.component';
import { TIMEWINDOW_PANEL_DATA, TimewindowPanelData } from '@shared/components/time/timewindow-panel.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
export const ALIASES_ENTITY_SELECT_PANEL_DATA = new InjectionToken<any>('AliasesEntitySelectPanelData'); export const ALIASES_ENTITY_SELECT_PANEL_DATA = new InjectionToken<any>('AliasesEntitySelectPanelData');

4
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<form #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 600px;"> <form #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="min-width: 550px;">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2> <h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-button mat-icon-button

82
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -19,35 +19,37 @@
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<mat-form-field class="mat-block tb-attribute-scope" style="width: 200px;" *ngIf="!disableAttributeScopeSelection"> <span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
<mat-label translate>attribute.attributes-scope</mat-label> <mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection">
<mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly" <mat-label translate>attribute.attributes-scope</mat-label>
matInput [ngModel]="attributeScope" <mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly"
(ngModelChange)="attributeScopeChanged($event)"> [ngModel]="attributeScope"
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> (ngModelChange)="attributeScopeChanged($event)">
{{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} <mat-option *ngFor="let scope of attributeScopes" [value]="scope">
</mat-option> {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }}
</mat-select> </mat-option>
</mat-form-field> </mat-select>
</mat-form-field>
</div>
<span fxFlex></span> <span fxFlex></span>
<button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)" <button mat-icon-button
mat-button mat-icon-button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="addAttribute($event)" (click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}" matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)" <button mat-icon-button
mat-button mat-icon-button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="reloadAttributes()" (click)="reloadAttributes()"
matTooltip="{{ 'action.refresh' | translate }}" matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
<button mat-button mat-icon-button <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="enterFilterMode()" (click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}" matTooltip="{{ 'action.search' | translate }}"
@ -58,7 +60,7 @@
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<button mat-button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.search' | translate }}" matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
@ -69,7 +71,7 @@
[(ngModel)]="pageLink.textSearch" [(ngModel)]="pageLink.textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/> placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field> </mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()" <button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}" matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -77,23 +79,22 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" fxLayout.xs="row wrap">
<span> <span class="tb-entity-table-info">
{{ translate.get( {{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ?
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY 'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
? 'attribute.selected-telemetry' : 'attribute.selected-attributes', </span>
{count: dataSource.selection.selected.length}) | async }}
</span>
<span fxFlex></span> <span fxFlex></span>
<button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)" <button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"
mat-button mat-icon-button [disabled]="isLoading$ | async" class="button-widget-action"
mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}" matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteAttributes($event)"> (click)="deleteAttributes($event)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button mat-button mat-raised-button <button mat-raised-button color="accent"
color="accent" class="button-widget-action"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.show-on-widget' | translate }}" matTooltip="{{ 'attribute.show-on-widget' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -104,7 +105,7 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'widget'"> <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'widget'">
<div class="mat-toolbar-tools" fxLayoutGap="8px"> <div class="mat-toolbar-tools" fxLayoutGap="8px" fxLayout.xs="row wrap">
<div fxFlex fxLayout="row" fxLayoutAlign="start"> <div fxFlex fxLayout="row" fxLayoutAlign="start">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span> <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
<tb-widgets-bundle-select fxFlexOffset="5" <tb-widgets-bundle-select fxFlexOffset="5"
@ -115,8 +116,9 @@
(ngModelChange)="onWidgetsBundleChanged()"> (ngModelChange)="onWidgetsBundleChanged()">
</tb-widgets-bundle-select> </tb-widgets-bundle-select>
</div> </div>
<button mat-button mat-raised-button [fxShow]="widgetsList.length > 0" <button mat-raised-button [fxShow]="widgetsList.length > 0"
color="accent" color="accent"
class="button-widget-action"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.add-to-dashboard' | translate }}" matTooltip="{{ 'attribute.add-to-dashboard' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -124,7 +126,7 @@
<mat-icon>dashboard</mat-icon> <mat-icon>dashboard</mat-icon>
<span translate>attribute.add-to-dashboard</span> <span translate>attribute.add-to-dashboard</span>
</button> </button>
<button mat-button mat-icon-button <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltip="{{ 'action.close' | translate }}" matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -134,7 +136,7 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<div fxFlex class="table-container" [fxShow]="mode !== 'widget'"> <div fxFlex class="table-container" [fxShow]="mode !== 'widget'">
<mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;"> <mat-header-cell *matHeaderCellDef style="width: 30px;">
@ -167,7 +169,7 @@
<mat-cell *matCellDef="let attribute" <mat-cell *matCellDef="let attribute"
class="tb-value-cell" class="tb-value-cell"
(click)="editAttribute($event, attribute)"> (click)="editAttribute($event, attribute)">
<div fxLayout="row"> <div fxLayout="row" style="align-items: center">
<span fxFlex>{{attribute.value | tbJson}}</span> <span fxFlex>{{attribute.value | tbJson}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"> <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
@ -179,7 +181,7 @@
<mat-row [ngClass]="{'mat-row-select': true, <mat-row [ngClass]="{'mat-row-select': true,
'mat-selected': dataSource.selection.isSelected(attribute)}" 'mat-selected': dataSource.selection.isSelected(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row> *matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row>
</mat-table> </table>
<span [fxShow]="dataSource.isEmpty() | async" <span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center" fxLayoutAlign="center center"
class="no-data-found" translate>{{ class="no-data-found" translate>{{
@ -218,14 +220,16 @@
</section> </section>
<ng-template #carouselPrev> <ng-template #carouselPrev>
<button mat-button mat-icon-button *ngIf="widgetsCarouselIndex > 0" <button mat-icon-button
*ngIf="widgetsCarouselIndex > 0"
matTooltip="{{ 'attribute.prev-widget' | translate }}" matTooltip="{{ 'attribute.prev-widget' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>keyboard_arrow_left</mat-icon> <mat-icon>keyboard_arrow_left</mat-icon>
</button> </button>
</ng-template> </ng-template>
<ng-template #carouselNext> <ng-template #carouselNext>
<button mat-button mat-icon-button *ngIf="widgetsCarouselIndex < widgetsList.length - 1" <button mat-icon-button
*ngIf="widgetsCarouselIndex < widgetsList.length - 1"
matTooltip="{{ 'attribute.next-widget' | translate }}" matTooltip="{{ 'attribute.next-widget' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>keyboard_arrow_right</mat-icon> <mat-icon>keyboard_arrow_right</mat-icon>
@ -241,13 +245,11 @@
<span translate *ngIf="mode === 'widget' && widgetsLoaded && <span translate *ngIf="mode === 'widget' && widgetsLoaded &&
widgetsList.length === 0 && widgetsList.length === 0 &&
widgetsBundle" widgetsBundle"
fxLayoutAlign="center center" fxFlex fxLayoutAlign="center center"
fxFlex
style="text-transform: uppercase; display: flex;" style="text-transform: uppercase; display: flex;"
class="mat-headline">widgets-bundle.empty</span> class="mat-headline">widgets-bundle.empty</span>
<span translate *ngIf="mode === 'widget' && !widgetsBundle" <span translate *ngIf="mode === 'widget' && !widgetsBundle"
fxLayoutAlign="center center" fxFlex fxLayoutAlign="center center"
fxFlex
style="text-transform: uppercase; display: flex;" style="text-transform: uppercase; display: flex;"
class="mat-headline">widget.select-widgets-bundle</span> class="mat-headline">widget.select-widgets-bundle</span>
</div> </div>

43
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -22,6 +24,14 @@
height: 100%; height: 100%;
background: #fff; background: #fff;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-entity-table-title { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; white-space: nowrap;
@ -32,6 +42,30 @@
.table-container { .table-container {
overflow: auto; overflow: auto;
} }
.tb-entity-table-info{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.button-widget-action{
margin-left: auto;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-entity-table-title{
padding-bottom: 5px;
width: 100%;
}
} }
} }
} }
@ -42,6 +76,7 @@
} }
mat-form-field.tb-attribute-scope { mat-form-field.tb-attribute-scope {
font-size: 16px; font-size: 16px;
width: 200px;
.mat-form-field-wrapper { .mat-form-field-wrapper {
padding-bottom: 0; padding-bottom: 0;
@ -50,6 +85,14 @@
.mat-form-field-underline { .mat-form-field-underline {
bottom: 0; bottom: 0;
} }
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
} }
mat-cell.tb-value-cell { mat-cell.tb-value-cell {
cursor: pointer; cursor: pointer;

6
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -45,7 +45,8 @@ import {
isClientSideTelemetryType, isClientSideTelemetryType,
LatestTelemetry, LatestTelemetry,
TelemetryType, TelemetryType,
telemetryTypeTranslations, toTelemetryType telemetryTypeTranslations,
toTelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { AttributeService } from '@app/core/http/attribute.service'; import { AttributeService } from '@app/core/http/attribute.service';
@ -297,6 +298,9 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) {
return;
}
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';

6
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2 translate>audit-log.audit-log-details</h2> <h2 translate>audit-log.audit-log-details</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-button mat-icon-button
@ -33,10 +33,8 @@
<div #failureDetailsEditor [fxShow]="displayFailureDetails" class="tb-audit-log-failure-details"> <div #failureDetailsEditor [fxShow]="displayFailureDetails" class="tb-audit-log-failure-details">
</div> </div>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayoutAlign="end center">
<span fxFlex></span>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial> [mat-dialog-close]="false" cdkFocusInitial>

10
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -25,17 +25,13 @@ import {
AuditLog, AuditLog,
AuditLogMode AuditLogMode
} from '@shared/models/audit-log.models'; } from '@shared/models/audit-log.models';
import { import { EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models';
EntityTypeResource,
EntityTypeTranslation,
entityTypeTranslations
} from '@shared/models/entity-type.models';
import { AuditLogService } from '@core/http/audit-log.service'; import { AuditLogService } from '@core/http/audit-log.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { PageLink, TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
@ -72,7 +68,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
search: 'audit-log.search' search: 'audit-log.search'
}; };
this.entityResources = { this.entityResources = {
} as EntityTypeResource; } as EntityTypeResource<AuditLog>;
this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink); this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink);

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

@ -18,7 +18,7 @@
<section [formGroup]="parentForm"> <section [formGroup]="parentForm">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>contact.country</mat-label> <mat-label translate>contact.country</mat-label>
<mat-select matInput formControlName="country"> <mat-select formControlName="country">
<mat-option *ngFor="let country of countries" [value]="country"> <mat-option *ngFor="let country of countries" [value]="country">
{{ country }} {{ country }}
</mat-option> </mat-option>

2
ui-ngx/src/app/modules/home/components/details-panel.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';

12
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -16,11 +16,11 @@
--> -->
<form (ngSubmit)="add()" style="min-width: 400px;" [ngStyle]="entitiesTableConfig.addDialogStyle"> <form (ngSubmit)="add()" style="min-width: 400px;" [ngStyle]="entitiesTableConfig.addDialogStyle">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2 translate>{{ translations.add }}</h2> <h2 translate>{{ translations.add }}</h2>
<span fxFlex></span> <span fxFlex></span>
<div [tb-help]="resources.helpLinkId"></div> <div [tb-help]="helpLinkId()"></div>
<button mat-button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -32,15 +32,13 @@
<div mat-dialog-content> <div mat-dialog-content>
<tb-anchor #entityDetailsForm></tb-anchor> <tb-anchor #entityDetailsForm></tb-anchor>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayoutAlign="end center">
<span fxFlex></span> <button mat-raised-button color="primary"
<button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"> [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
cdkFocusInitial cdkFocusInitial
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"

3
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.scss

@ -13,5 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { :host ::ng-deep .mat-padding{
padding: 0;
} }

54
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.ts

@ -14,22 +14,22 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {Component, ComponentFactoryResolver, Inject, OnInit, SkipSelf, ViewChild} from '@angular/core'; import { Component, ComponentFactoryResolver, Inject, Injector, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {PageComponent} from '@shared/components/page.component'; import { Store } from '@ngrx/store';
import {Store} from '@ngrx/store'; import { AppState } from '@core/core.state';
import {AppState} from '@core/core.state'; import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
import {EntityTypeResource, EntityTypeTranslation} from '@shared/models/entity-type.models'; import { BaseData, HasId } from '@shared/models/base-data';
import {BaseData, HasId} from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id';
import {EntityId} from '@shared/models/id/entity-id'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import {TbAnchorComponent} from '@shared/components/tb-anchor.component'; import { EntityComponent } from './entity.component';
import {EntityComponent} from './entity.component'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import {EntityTableConfig} from '@home/models/entity/entities-table-config.models'; import { AddEntityDialogData } from '@home/models/entity/entity-component.models';
import {AddEntityDialogData} from '@home/models/entity/entity-component.models';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'tb-add-entity-dialog', selector: 'tb-add-entity-dialog',
@ -45,7 +45,7 @@ export class AddEntityDialogComponent extends
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
translations: EntityTypeTranslation; translations: EntityTypeTranslation;
resources: EntityTypeResource; resources: EntityTypeResource<BaseData<HasId>>;
entity: BaseData<EntityId>; entity: BaseData<EntityId>;
submitted = false; submitted = false;
@ -57,6 +57,7 @@ export class AddEntityDialogComponent extends
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>, @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>,
public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>, public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
@ -69,14 +70,35 @@ export class AddEntityDialogComponent extends
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent);
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef;
viewContainerRef.clear(); viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory); const injector: Injector = Injector.create(
{
providers: [
{
provide: 'entity',
useValue: this.entity
},
{
provide: 'entitiesTableConfig',
useValue: this.entitiesTableConfig
}
],
parent: this.injector
}
);
const componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
this.entityComponent = componentRef.instance; this.entityComponent = componentRef.instance;
this.entityComponent.isEdit = true; this.entityComponent.isEdit = true;
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig;
this.entityComponent.entity = this.entity;
this.detailsForm = this.entityComponent.entityNgForm; this.detailsForm = this.entityComponent.entityNgForm;
} }
helpLinkId(): string {
if (this.resources.helpLinkIdForEntity && this.entityComponent.entityForm) {
return this.resources.helpLinkIdForEntity(this.entityComponent.entityForm.getRawValue());
} else {
return this.resources.helpLinkId;
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted); const customErrorState = !!(control && control.invalid && this.submitted);

7
ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts

@ -22,12 +22,15 @@ import { AfterViewInit } from '@angular/core';
import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models';
import { HasId } from '@shared/models/base-data'; import { HasId } from '@shared/models/base-data';
import {EntityComponent} from './entity.component'; import {EntityComponent} from './entity.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit { export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
protected constructor(protected store: Store<AppState>, protected constructor(protected store: Store<AppState>,
protected fb: FormBuilder) { protected fb: FormBuilder,
super(store); protected entityValue: T,
protected entitiesTableConfigValue: EntityTableConfig<T>) {
super(store, fb, entityValue, entitiesTableConfigValue);
} }
buildForm(entity: T): FormGroup { buildForm(entity: T): FormGroup {

69
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -25,7 +25,7 @@
<tb-entity-details-panel <tb-entity-details-panel
[entitiesTableConfig]="entitiesTableConfig" [entitiesTableConfig]="entitiesTableConfig"
[entityId]="dataSource.currentEntity?.id" [entityId]="dataSource.currentEntity?.id"
(closeEntityDetails)="isDetailsOpen = false" (closeEntityDetails)="isDetailsOpen = false; detailsPanelOpened.emit(isDetailsOpen);"
(entityUpdated)="onEntityUpdated($event)" (entityUpdated)="onEntityUpdated($event)"
(entityAction)="onEntityAction($event)" (entityAction)="onEntityAction($event)"
> >
@ -33,18 +33,19 @@
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<tb-anchor #entityTableHeader></tb-anchor> <span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span>
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" <tb-anchor #entityTableHeader></tb-anchor>
(ngModelChange)="onTimewindowChange()" <tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
asButton historyOnly></tb-timewindow> (ngModelChange)="onTimewindowChange()"
<span fxFlex *ngIf="!entitiesTableConfig.headerComponent || asButton historyOnly></tb-timewindow>
entitiesTableConfig.useTimePageLink"></span> </div>
<span fxFlex></span>
<div [fxShow]="addEnabled()"> <div [fxShow]="addEnabled()">
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
*ngIf="!entitiesTableConfig.addActionDescriptors.length; else addActions" *ngIf="!entitiesTableConfig.addActionDescriptors.length; else addActions"
(click)="addEntity($event)" (click)="addEntity($event)"
matTooltip="{{ translations.add | translate }}" matTooltip="{{ translations.add | translate }}"
@ -52,7 +53,7 @@
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<ng-template #addActions> <ng-template #addActions>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
*ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else addActionsMenu" *ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else addActionsMenu"
[fxShow]="entitiesTableConfig.addActionDescriptors[0].isEnabled()" [fxShow]="entitiesTableConfig.addActionDescriptors[0].isEnabled()"
(click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)" (click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)"
@ -61,7 +62,7 @@
<mat-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon> <mat-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon>
</button> </button>
<ng-template #addActionsMenu> <ng-template #addActionsMenu>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ translations.add | translate }}" matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
[matMenuTriggerFor]="addActionsMenu"> [matMenuTriggerFor]="addActionsMenu">
@ -79,20 +80,20 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
</div> </div>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors" [fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors"
matTooltip="{{ actionDescriptor.name }}" matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)"> (click)="actionDescriptor.onAction($event)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon> <mat-icon>{{actionDescriptor.icon}}</mat-icon>
</button> </button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()" <button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}" matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
<button *ngIf="entitiesTableConfig.searchEnabled" <button *ngIf="entitiesTableConfig.searchEnabled"
mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ translations.search | translate }}" matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
@ -101,7 +102,7 @@
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<button mat-button mat-icon-button <button mat-icon-button
matTooltip="{{ translations.search | translate }}" matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
@ -112,7 +113,7 @@
[(ngModel)]="pageLink.textSearch" [(ngModel)]="pageLink.textSearch"
placeholder="{{ translations.search | translate }}"/> placeholder="{{ translations.search | translate }}"/>
</mat-form-field> </mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()" <button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}" matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -121,11 +122,11 @@
</mat-toolbar> </mat-toolbar>
<mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()"> <mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span> <span class="tb-entity-table-info">
{{ translate.get(translations.selectedEntities, {count: dataSource.selection.selected.length}) | async }} {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }}
</span> </span>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors" [fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors"
matTooltip="{{ actionDescriptor.name }}" matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -135,8 +136,8 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId" <table mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="pageLink.sortOrder?.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;"> <mat-header-cell *matHeaderCellDef style="width: 30px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null" <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
@ -154,8 +155,11 @@
</ng-container> </ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;"> <ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;">
<mat-header-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}" <mat-header-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
*matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell> [fxHide.lt-lg]="column.mobileHide"
*matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable">
{{ column.ignoreTranslate ? column.title : (column.title | translate) }} </mat-header-cell>
<mat-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}" <mat-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
[fxHide.lt-lg]="column.mobileHide"
*matCellDef="let entity; let row = index" *matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)" [matTooltip]="cellTooltip(entity, column, row)"
matTooltipPosition="above" matTooltipPosition="above"
@ -167,7 +171,7 @@
<mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index" <mat-cell *matCellDef="let entity; let row = index"
[ngStyle]="cellStyle(entity, column, row)"> [ngStyle]="cellStyle(entity, column, row)">
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="column.actionDescriptor.isEnabled(entity)" [fxShow]="column.actionDescriptor.isEnabled(entity)"
matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}" matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -187,8 +191,8 @@
maxWidth: (cellActionDescriptors.length * 40) + 'px', maxWidth: (cellActionDescriptors.length * 40) + 'px',
width: (cellActionDescriptors.length * 40) + 'px' }"> width: (cellActionDescriptors.length * 40) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
[fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors" *ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)"> (click)="actionDescriptor.onAction($event, entity)">
@ -197,7 +201,7 @@
</button> </button>
</div> </div>
<div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length"> <div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
<button mat-button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
@ -220,16 +224,17 @@
'mat-selected': dataSource.selection.isSelected(entity), 'mat-selected': dataSource.selection.isSelected(entity),
'tb-current-entity': dataSource.isCurrentEntity(entity)}" 'tb-current-entity': dataSource.isCurrentEntity(entity)}"
*matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row> *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row>
</mat-table> </table>
<span [fxShow]="!(isLoading$ | async) && (dataSource.isEmpty() | async)" <span [fxShow]="!(isLoading$ | async) && (dataSource.isEmpty() | async)"
fxLayoutAlign="center center" fxLayoutAlign="center center"
class="no-data-found" translate>{{ translations.noEntities }}</span> class="no-data-found" translate>{{ translations.noEntities }}</span>
</div> </div>
<mat-divider></mat-divider> <mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator [length]="dataSource.total() | async" <mat-paginator *ngIf="displayPagination"
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [pageSizeOptions]="pageSizeOptions"></mat-paginator>
</div> </div>
</div> </div>
</mat-drawer-content> </mat-drawer-content>

28
ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -22,6 +24,14 @@
height: 100%; height: 100%;
background: #fff; background: #fff;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-entity-table-title { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; white-space: nowrap;
@ -32,6 +42,24 @@
.table-container { .table-container {
overflow: auto; overflow: auto;
} }
.tb-entity-table-info{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-entity-table-title{
padding-bottom: 5px;
width: 100%;
}
} }
} }
} }

178
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -16,25 +16,25 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy, ChangeDetectorRef,
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ElementRef, ElementRef, EventEmitter,
Input, Input, OnChanges,
OnInit, OnInit, SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { PageLink, TimePageLink } from '@shared/models/page/page-link'; import { MAX_SAFE_PAGE_SIZE, PageLink, TimePageLink } from '@shared/models/page/page-link';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@ -51,7 +51,7 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogComponent } from './add-entity-dialog.component';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { historyInterval, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DAY, historyInterval, HistoryWindowType, Timewindow } from '@shared/models/time/time.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
@ -62,7 +62,7 @@ import { isDefined, isUndefined } from '@core/utils';
styleUrls: ['./entities-table.component.scss'], styleUrls: ['./entities-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit, OnChanges {
@Input() @Input()
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
@ -87,12 +87,16 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
selectionEnabled; selectionEnabled;
defaultPageSize = 10;
displayPagination = true;
pageSizeOptions;
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
timewindow: Timewindow; timewindow: Timewindow;
dataSource: EntitiesDataSource<BaseData<HasId>>; dataSource: EntitiesDataSource<BaseData<HasId>>;
isDetailsOpen = false; isDetailsOpen = false;
detailsPanelOpened = new EventEmitter<boolean>();
@ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent;
@ -101,18 +105,43 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
private sortSubscription: Subscription;
private updateDataSubscription: Subscription;
private viewInited = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, public route: ActivatedRoute,
public translate: TranslateService, public translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog,
private dialogService: DialogService, private dialogService: DialogService,
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver) { private componentFactoryResolver: ComponentFactoryResolver) {
super(store); super(store);
} }
ngOnInit() { ngOnInit() {
this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig; if (this.entitiesTableConfig) {
this.init(this.entitiesTableConfig);
} else {
this.init(this.route.snapshot.data.entitiesTableConfig);
}
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'entitiesTableConfig' && change.currentValue) {
this.init(change.currentValue);
}
}
}
}
private init(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) {
this.isDetailsOpen = false;
this.entitiesTableConfig = entitiesTableConfig;
if (this.entitiesTableConfig.headerComponent) { if (this.entitiesTableConfig.headerComponent) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent);
const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef; const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef;
@ -138,17 +167,16 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
onAction: ($event, entity) => this.deleteEntity($event, entity) onAction: ($event, entity) => this.deleteEntity($event, entity)
} }
); );
this.groupActionDescriptors.push(
{
name: this.translate.instant('action.delete'),
icon: 'delete',
isEnabled: true,
onAction: ($event, entities) => this.deleteEntities($event, entities)
}
);
} }
this.groupActionDescriptors.push(
{
name: this.translate.instant('action.delete'),
icon: 'delete',
isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled,
onAction: ($event, entities) => this.deleteEntities($event, entities)
}
);
const enabledGroupActionDescriptors = const enabledGroupActionDescriptors =
this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled); this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled);
@ -156,30 +184,39 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
this.columnsUpdated(); this.columnsUpdated();
const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property, let sortOrder: SortOrder = null;
direction: this.entitiesTableConfig.defaultSortOrder.direction }; if (this.entitiesTableConfig.defaultSortOrder) {
sortOrder = {
property: this.entitiesTableConfig.defaultSortOrder.property,
direction: this.entitiesTableConfig.defaultSortOrder.direction
};
}
this.displayPagination = this.entitiesTableConfig.displayPagination;
this.defaultPageSize = this.entitiesTableConfig.defaultPageSize;
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = historyInterval(24 * 60 * 60 * 1000); this.timewindow = historyInterval(DAY);
const currentTime = Date.now(); const currentTime = Date.now();
this.pageLink = new TimePageLink(10, 0, null, sortOrder, this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime); currentTime - this.timewindow.history.timewindowMs, currentTime);
} else { } else {
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
} }
this.dataSource = new EntitiesDataSource<BaseData<HasId>>( this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE;
this.entitiesTableConfig.entitiesFetchFunction, this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this));
this.entitiesTableConfig.entitySelectionEnabled,
() => {
this.dataLoaded();
}
);
if (this.entitiesTableConfig.onLoadAction) { if (this.entitiesTableConfig.onLoadAction) {
this.entitiesTableConfig.onLoadAction(this.route); this.entitiesTableConfig.onLoadAction(this.route);
} }
if (this.entitiesTableConfig.loadDataOnInit) { if (this.entitiesTableConfig.loadDataOnInit) {
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
} }
if (this.viewInited) {
setTimeout(() => {
this.updatePaginationSubscriptions();
}, 0);
}
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -189,15 +226,31 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
debounceTime(150), debounceTime(150),
distinctUntilChanged(), distinctUntilChanged(),
tap(() => { tap(() => {
this.paginator.pageIndex = 0; if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData(); this.updateData();
}) })
) )
.subscribe(); .subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); this.updatePaginationSubscriptions();
this.viewInited = true;
}
merge(this.sort.sortChange, this.paginator.page) private updatePaginationSubscriptions() {
if (this.sortSubscription) {
this.sortSubscription.unsubscribe();
this.sortSubscription = null;
}
if (this.updateDataSubscription) {
this.updateDataSubscription.unsubscribe();
this.updateDataSubscription = null;
}
if (this.displayPagination) {
this.sortSubscription = this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
}
this.updateDataSubscription = (this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange)
.pipe( .pipe(
tap(() => this.updateData()) tap(() => this.updateData())
) )
@ -208,14 +261,29 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
return this.entitiesTableConfig.addEnabled; return this.entitiesTableConfig.addEnabled;
} }
clearSelection() {
this.dataSource.selection.clear();
this.cd.detectChanges();
}
updateData(closeDetails: boolean = true) { updateData(closeDetails: boolean = true) {
if (closeDetails) { if (closeDetails) {
this.isDetailsOpen = false; this.isDetailsOpen = false;
} }
this.pageLink.page = this.paginator.pageIndex; if (this.displayPagination) {
this.pageLink.pageSize = this.paginator.pageSize; this.pageLink.page = this.paginator.pageIndex;
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; } else {
this.pageLink.page = 0;
}
if (this.sort.active) {
this.pageLink.sortOrder = {
property: this.sort.active,
direction: Direction[this.sort.direction.toUpperCase()]
};
} else {
this.pageLink.sortOrder = null;
}
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
const timePageLink = this.pageLink as TimePageLink; const timePageLink = this.pageLink as TimePageLink;
if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) {
@ -230,14 +298,24 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
} }
private dataLoaded() { private dataLoaded(col?: number, row?: number) {
this.headerCellStyleCache.length = 0; if (isFinite(col) && isFinite(row)) {
this.cellContentCache.length = 0; this.clearCellCache(col, row);
this.cellTooltipCache.length = 0; } else {
this.cellStyleCache.length = 0; this.headerCellStyleCache.length = 0;
this.cellContentCache.length = 0;
this.cellTooltipCache.length = 0;
this.cellStyleCache.length = 0;
}
} }
onRowClick($event: Event, entity) { onRowClick($event: Event, entity) {
if (!this.entitiesTableConfig.handleRowClick($event, entity)) {
this.toggleEntityDetails($event, entity);
}
}
toggleEntityDetails($event: Event, entity) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -246,6 +324,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
} else { } else {
this.isDetailsOpen = !this.isDetailsOpen; this.isDetailsOpen = !this.isDetailsOpen;
} }
this.detailsPanelOpened.emit(this.isDetailsOpen);
} }
addEntity($event: Event) { addEntity($event: Event) {
@ -345,16 +424,20 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
exitFilterMode() { exitFilterMode() {
this.textSearchMode = false; this.textSearchMode = false;
this.pageLink.textSearch = null; this.pageLink.textSearch = null;
this.paginator.pageIndex = 0; if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData(); this.updateData();
} }
resetSortAndFilter(update: boolean = true) { resetSortAndFilter(update: boolean = true) {
this.pageLink.textSearch = null; this.pageLink.textSearch = null;
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = historyInterval(24 * 60 * 60 * 1000); this.timewindow = historyInterval(DAY);
}
if (this.displayPagination) {
this.paginator.pageIndex = 0;
} }
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property);
this.sort.active = sortable.id; this.sort.active = sortable.id;
this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc';
@ -410,6 +493,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
return res; return res;
} }
clearCellCache(col: number, row: number) {
const index = row * this.entitiesTableConfig.columns.length + col;
this.cellContentCache[index] = undefined;
this.cellTooltipCache[index] = undefined;
this.cellStyleCache[index] = undefined;
}
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) {
if (column instanceof EntityTableColumn) { if (column instanceof EntityTableColumn) {
const col = this.entitiesTableConfig.columns.indexOf(column); const col = this.entitiesTableConfig.columns.indexOf(column);

6
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html

@ -16,7 +16,7 @@
--> -->
<tb-details-panel fxFlex <tb-details-panel fxFlex
[headerTitle]="entity?.name" [headerTitle]="entitiesTableConfig.entityTitle(entity)"
headerSubtitle="{{ translations.details | translate }}" headerSubtitle="{{ translations.details | translate }}"
[isReadOnly]="entitiesTableConfig.detailsReadonly(entity)" [isReadOnly]="entitiesTableConfig.detailsReadonly(entity)"
[isEdit]="isEditValue" [isEdit]="isEditValue"
@ -25,9 +25,9 @@
(applyDetails)="saveEntity()" (applyDetails)="saveEntity()"
[theForm]="detailsForm"> [theForm]="detailsForm">
<div class="details-buttons"> <div class="details-buttons">
<div [tb-help]="resources.helpLinkId"></div> <div [tb-help]="helpLinkId()"></div>
</div> </div>
<mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': isEditValue}" fxFlex [(selectedIndex)]="selectedTab"> <mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': hideDetailsTabs()}" fxFlex [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'details.details' | translate }}"> <mat-tab label="{{ 'details.details' | translate }}">
<tb-anchor #entityDetailsForm></tb-anchor> <tb-anchor #entityDetailsForm></tb-anchor>
</mat-tab> </mat-tab>

91
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -15,36 +15,33 @@
/// ///
import { import {
ChangeDetectionStrategy, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectorRef,
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
EventEmitter, EventEmitter, Injector,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
ViewChild,
ViewChildren,
QueryList, QueryList,
ContentChildren, AfterViewInit ViewChild,
ViewChildren
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { import { EntityType, EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
EntityType,
EntityTypeResource,
EntityTypeTranslation
} from '@shared/models/entity-type.models';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { EntityComponent } from './entity.component'; import { EntityComponent } from './entity.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatTabGroup, MatTab } from '@angular/material/tabs'; import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
import { deepClone } from '@core/utils';
@Component({ @Component({
selector: 'tb-entity-details-panel', selector: 'tb-entity-details-panel',
@ -54,8 +51,6 @@ import { EntityTabsComponent } from '@home/components/entity/entity-tabs.compone
}) })
export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
@Output() @Output()
closeEntityDetails = new EventEmitter<void>(); closeEntityDetails = new EventEmitter<void>();
@ -69,6 +64,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
entityTabsComponent: EntityTabsComponent<BaseData<HasId>>; entityTabsComponent: EntityTabsComponent<BaseData<HasId>>;
detailsForm: NgForm; detailsForm: NgForm;
entitiesTableConfigValue: EntityTableConfig<BaseData<HasId>>;
isEditValue = false; isEditValue = false;
selectedTab = 0; selectedTab = 0;
@ -83,13 +79,16 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
@ViewChildren(MatTab) inclusiveTabs: QueryList<MatTab>; @ViewChildren(MatTab) inclusiveTabs: QueryList<MatTab>;
translations: EntityTypeTranslation; translations: EntityTypeTranslation;
resources: EntityTypeResource; resources: EntityTypeResource<BaseData<HasId>>;
entity: BaseData<HasId>; entity: BaseData<HasId>;
editingEntity: BaseData<HasId>;
private currentEntityId: HasId; private currentEntityId: HasId;
private entityActionSubscription: Subscription; private entityActionSubscription: Subscription;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private injector: Injector,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver) { private componentFactoryResolver: ComponentFactoryResolver) {
super(store); super(store);
} }
@ -102,9 +101,26 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
} }
} }
@Input()
set entitiesTableConfig(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) {
this.entitiesTableConfigValue = entitiesTableConfig;
if (this.entityComponent) {
this.entityComponent.entitiesTableConfig = entitiesTableConfig;
}
if (this.entityTabsComponent) {
this.entityTabsComponent.entitiesTableConfig = entitiesTableConfig;
}
}
get entitiesTableConfig(): EntityTableConfig<BaseData<HasId>> {
return this.entitiesTableConfigValue;
}
set isEdit(val: boolean) { set isEdit(val: boolean) {
this.isEditValue = val; this.isEditValue = val;
this.entityComponent.isEdit = val; if (this.entityComponent) {
this.entityComponent.isEdit = val;
}
if (this.entityTabsComponent) { if (this.entityTabsComponent) {
this.entityTabsComponent.isEdit = val; this.entityTabsComponent.isEdit = val;
} }
@ -131,15 +147,32 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent);
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef;
viewContainerRef.clear(); viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory); const injector: Injector = Injector.create(
{
providers: [
{
provide: 'entity',
useValue: this.entity
},
{
provide: 'entitiesTableConfig',
useValue: this.entitiesTableConfig
}
],
parent: this.injector
}
);
const componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
this.entityComponent = componentRef.instance; this.entityComponent = componentRef.instance;
this.entityComponent.isEdit = this.isEdit; this.entityComponent.isEdit = this.isEdit;
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig;
this.detailsForm = this.entityComponent.entityNgForm; this.detailsForm = this.entityComponent.entityNgForm;
this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => { this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => {
this.entityAction.emit(action); this.entityAction.emit(action);
}); });
this.buildEntityTabsComponent(); this.buildEntityTabsComponent();
this.entityComponent.entityForm.valueChanges.subscribe(() => {
this.cd.detectChanges();
});
} }
buildEntityTabsComponent() { buildEntityTabsComponent() {
@ -151,9 +184,14 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
this.entityTabsComponent = componentTabsRef.instance; this.entityTabsComponent = componentTabsRef.instance;
this.entityTabsComponent.isEdit = this.isEdit; this.entityTabsComponent.isEdit = this.isEdit;
this.entityTabsComponent.entitiesTableConfig = this.entitiesTableConfig; this.entityTabsComponent.entitiesTableConfig = this.entitiesTableConfig;
this.entityTabsComponent.detailsForm = this.detailsForm;
} }
} }
hideDetailsTabs(): boolean {
return this.isEditValue && this.entitiesTableConfig.hideDetailsTabsOnEdit;
}
reload(): void { reload(): void {
this.isEdit = false; this.isEdit = false;
this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe(
@ -179,13 +217,28 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
this.entityTabsComponent.entity = this.entity; this.entityTabsComponent.entity = this.entity;
} }
} else { } else {
this.selectedTab = 0; this.editingEntity = deepClone(this.entity);
this.entityComponent.entity = this.editingEntity;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = this.editingEntity;
}
if (this.entitiesTableConfig.hideDetailsTabsOnEdit) {
this.selectedTab = 0;
}
}
}
helpLinkId(): string {
if (this.resources.helpLinkIdForEntity && this.entityComponent.entityForm) {
return this.resources.helpLinkIdForEntity(this.entityComponent.entityForm.getRawValue());
} else {
return this.resources.helpLinkId;
} }
} }
saveEntity() { saveEntity() {
if (this.detailsForm.valid) { if (this.detailsForm.valid) {
const editingEntity = {...this.entity, ...this.entityComponent.entityFormValue()}; const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()};
this.entitiesTableConfig.saveEntity(editingEntity).subscribe( this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
(entity) => { (entity) => {
this.entity = entity; this.entity = entity;

2
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -18,7 +18,7 @@
<div fxLayout="column" [formGroup]="entityFilterFormGroup" class="tb-entity-filter"> <div fxLayout="column" [formGroup]="entityFilterFormGroup" class="tb-entity-filter">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>alias.filter-type</mat-label> <mat-label translate>alias.filter-type</mat-label>
<mat-select required matInput formControlName="type"> <mat-select required formControlName="type">
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type"> <mat-option *ngFor="let type of aliasFilterTypes" [value]="type">
{{aliasFilterTypeTranslations.get(type) | translate}} {{aliasFilterTypeTranslations.get(type) | translate}}
</mat-option> </mat-option>

9
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -15,14 +15,7 @@
/// ///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
ControlValueAccessor,
FormBuilder, FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
ValidatorFn,
Validators
} from '@angular/forms';
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';

21
ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts

@ -20,12 +20,25 @@ import { Input, OnInit, Directive } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { PageLink } from '@shared/models/page/page-link';
@Directive() @Directive()
export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>,
P extends PageLink = PageLink,
L extends BaseData<HasId> = T,
C extends EntityTableConfig<T, P, L> = EntityTableConfig<T, P, L>>
extends PageComponent implements OnInit {
entitiesTableConfigValue: C;
@Input() @Input()
entitiesTableConfig: EntityTableConfig<T>; set entitiesTableConfig(entitiesTableConfig: C) {
this.setEntitiesTableConfig(entitiesTableConfig);
}
get entitiesTableConfig(): C {
return this.entitiesTableConfigValue;
}
protected constructor(protected store: Store<AppState>) { protected constructor(protected store: Store<AppState>) {
super(store); super(store);
@ -34,4 +47,8 @@ export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> exte
ngOnInit() { ngOnInit() {
} }
protected setEntitiesTableConfig(entitiesTableConfig: C) {
this.entitiesTableConfigValue = entitiesTableConfig;
}
} }

31
ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts

@ -31,9 +31,15 @@ import { AuditLogMode } from '@shared/models/audit-log.models';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, EventType } from '@shared/models/event.models';
import { AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { NgForm } from '@angular/forms';
import { PageLink } from '@shared/models/page/page-link';
@Directive() @Directive()
export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit, AfterViewInit { export abstract class EntityTabsComponent<T extends BaseData<HasId>,
P extends PageLink = PageLink,
L extends BaseData<HasId> = T,
C extends EntityTableConfig<T, P, L> = EntityTableConfig<T, P, L>>
extends PageComponent implements OnInit, AfterViewInit {
attributeScopes = AttributeScope; attributeScopes = AttributeScope;
latestTelemetryTypes = LatestTelemetry; latestTelemetryTypes = LatestTelemetry;
@ -54,6 +60,8 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends Pag
entityValue: T; entityValue: T;
entitiesTableConfigValue: C;
@ViewChildren(MatTab) entityTabs: QueryList<MatTab>; @ViewChildren(MatTab) entityTabs: QueryList<MatTab>;
isEditValue: boolean; isEditValue: boolean;
@ -69,7 +77,7 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends Pag
@Input() @Input()
set entity(entity: T) { set entity(entity: T) {
this.entityValue = entity; this.setEntity(entity);
} }
get entity(): T { get entity(): T {
@ -77,7 +85,16 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends Pag
} }
@Input() @Input()
entitiesTableConfig: EntityTableConfig<T>; set entitiesTableConfig(entitiesTableConfig: C) {
this.setEntitiesTableConfig(entitiesTableConfig);
}
get entitiesTableConfig(): C {
return this.entitiesTableConfigValue;
}
@Input()
detailsForm: NgForm;
private entityTabsSubject = new BehaviorSubject<Array<MatTab>>(null); private entityTabsSubject = new BehaviorSubject<Array<MatTab>>(null);
@ -100,4 +117,12 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends Pag
); );
} }
protected setEntity(entity: T) {
this.entityValue = entity;
}
protected setEntitiesTableConfig(entitiesTableConfig: C) {
this.entitiesTableConfigValue = entitiesTableConfig;
}
} }

42
ui-ngx/src/app/modules/home/components/entity/entity.component.ts

@ -15,24 +15,39 @@
/// ///
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { FormGroup, NgForm } from '@angular/forms'; import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { EventEmitter, Input, OnInit, Output, ViewChild, Directive } from '@angular/core'; import { Directive, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { PageLink } from '@shared/models/page/page-link';
// @dynamic
@Directive() @Directive()
export abstract class EntityComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { // tslint:disable-next-line:directive-class-suffix
export abstract class EntityComponent<T extends BaseData<HasId>,
P extends PageLink = PageLink,
L extends BaseData<HasId> = T,
C extends EntityTableConfig<T, P, L> = EntityTableConfig<T, P, L>>
extends PageComponent implements OnInit {
entityValue: T;
entityForm: FormGroup; entityForm: FormGroup;
@ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm; @ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm;
isEditValue: boolean; isEditValue: boolean;
@Input()
set entitiesTableConfig(entitiesTableConfig: C) {
this.setEntitiesTableConfig(entitiesTableConfig);
}
get entitiesTableConfig(): C {
return this.entitiesTableConfigValue;
}
@Input() @Input()
set isEdit(isEdit: boolean) { set isEdit(isEdit: boolean) {
this.isEditValue = isEdit; this.isEditValue = isEdit;
@ -51,7 +66,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>> extends PageCom
set entity(entity: T) { set entity(entity: T) {
this.entityValue = entity; this.entityValue = entity;
if (this.entityForm) { if (this.entityForm) {
this.entityForm.reset(); this.entityForm.reset(undefined, {emitEvent: false});
this.entityForm.markAsPristine();
this.updateForm(entity); this.updateForm(entity);
} }
} }
@ -60,18 +76,18 @@ export abstract class EntityComponent<T extends BaseData<HasId>> extends PageCom
return this.entityValue; return this.entityValue;
} }
@Input()
entitiesTableConfig: EntityTableConfig<T>;
@Output() @Output()
entityAction = new EventEmitter<EntityAction<T>>(); entityAction = new EventEmitter<EntityAction<T>>();
protected constructor(protected store: Store<AppState>) { protected constructor(protected store: Store<AppState>,
protected fb: FormBuilder,
protected entityValue: T,
protected entitiesTableConfigValue: C) {
super(store); super(store);
this.entityForm = this.buildForm(this.entityValue);
} }
ngOnInit() { ngOnInit() {
this.entityForm = this.buildForm(this.entityValue);
} }
onEntityAction($event: Event, action: string) { onEntityAction($event: Event, action: string) {
@ -96,7 +112,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>> extends PageCom
} }
entityFormValue() { entityFormValue() {
const formValue = this.entityForm ? {...this.entityForm.value} : {}; const formValue = this.entityForm ? {...this.entityForm.getRawValue()} : {};
return this.prepareFormValue(formValue); return this.prepareFormValue(formValue);
} }
@ -104,6 +120,10 @@ export abstract class EntityComponent<T extends BaseData<HasId>> extends PageCom
return formValue; return formValue;
} }
protected setEntitiesTableConfig(entitiesTableConfig: C) {
this.entitiesTableConfigValue = entitiesTableConfig;
}
abstract buildForm(entity: T): FormGroup; abstract buildForm(entity: T): FormGroup;
abstract updateForm(entity: T); abstract updateForm(entity: T);

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

@ -94,7 +94,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
noEntities: 'event.no-events-prompt' noEntities: 'event.no-events-prompt'
}; };
this.entityResources = { this.entityResources = {
} as EntityTypeResource; } as EntityTypeResource<Event>;
this.entitiesFetchFunction = pageLink => this.fetchEvents(pageLink); this.entitiesFetchFunction = pageLink => this.fetchEvents(pageLink);
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};

4
ui-ngx/src/app/modules/home/components/event/event-table-header.component.html

@ -15,9 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field class="mat-block" style="width: 200px;"> <mat-form-field class="mat-block">
<mat-label translate>event.event-type</mat-label> <mat-label translate>event.event-type</mat-label>
<mat-select matInput [ngModel]="eventTableConfig.eventType" <mat-select [ngModel]="eventTableConfig.eventType"
(ngModelChange)="eventTypeChanged($event)"> (ngModelChange)="eventTypeChanged($event)">
<mat-option *ngFor="let type of eventTableConfig.eventTypes" [value]="type"> <mat-option *ngFor="let type of eventTableConfig.eventTypes" [value]="type">
{{ eventTypeTranslationsMap.get(type) | translate }} {{ eventTypeTranslationsMap.get(type) | translate }}

12
ui-ngx/src/app/modules/home/components/event/event-table-header.component.scss

@ -13,16 +13,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants';
:host { :host {
/* flex: 1; /* flex: 1;
display: flex; display: flex;
justify-content: flex-start; */ justify-content: flex-start; */
padding-right: 8px; padding-right: 8px;
min-width: 160px;
} }
:host ::ng-deep { :host ::ng-deep {
mat-form-field { mat-form-field {
font-size: 16px; font-size: 16px;
width: 200px;
.mat-form-field-wrapper { .mat-form-field-wrapper {
padding-bottom: 0; padding-bottom: 0;
@ -31,5 +35,13 @@
.mat-form-field-underline { .mat-form-field-underline {
bottom: 0; bottom: 0;
} }
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
} }
} }

12
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -15,20 +15,11 @@
/// ///
import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { AuditLogService } from '@core/http/audit-log.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AuditLogMode } from '@shared/models/audit-log.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { UserId } from '@shared/models/id/user-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { AuditLogTableConfig } from '@home/components/audit-log/audit-log-table-config';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { EventTableConfig } from './event-table-config'; import { EventTableConfig } from './event-table-config';
import { EventService } from '@core/http/event.service'; import { EventService } from '@core/http/event.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -89,8 +80,7 @@ export class EventTableComponent implements OnInit {
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog) {
private store: Store<AppState>) {
} }
ngOnInit() { ngOnInit() {

53
ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html

@ -44,13 +44,11 @@
</tb-file-input> </tb-file-input>
</fieldset> </fieldset>
</form> </form>
<div fxLayout="row"> <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<span fxFlex></span>
<button mat-button <button mat-button
style="margin-right: 20px;"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button> (click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-button mat-raised-button <button mat-raised-button
[disabled]="(isLoading$ | async) || selectFileFormGroup.invalid || !selectFileFormGroup.dirty" [disabled]="(isLoading$ | async) || selectFileFormGroup.invalid || !selectFileFormGroup.dirty"
color="primary" color="primary"
(click)="nextStep(2)">{{ 'action.continue' | translate }}</button> (click)="nextStep(2)">{{ 'action.continue' | translate }}</button>
@ -62,7 +60,7 @@
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> <fieldset [disabled]="isLoading$ | async" fxLayout="column">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>import.csv-delimiter</mat-label> <mat-label translate>import.csv-delimiter</mat-label>
<mat-select required matInput formControlName="delim"> <mat-select required formControlName="delim">
<mat-option *ngFor="let delimiter of delimiters" [value]="delimiter.key"> <mat-option *ngFor="let delimiter of delimiters" [value]="delimiter.key">
{{ delimiter.value }} {{ delimiter.value }}
</mat-option> </mat-option>
@ -78,19 +76,19 @@
</div> </div>
</fieldset> </fieldset>
</form> </form>
<div fxLayout="row"> <div fxLayout="row wrap" fxLayoutAlign="space-between center">
<button mat-button <button mat-button
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button> (click)="previousStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span> <div fxLayout="row wrap" fxLayoutGap="20px">
<button mat-button <button mat-button
style="margin-right: 20px;" [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
(click)="cancel()">{{ 'action.cancel' | translate }}</button> <button mat-raised-button
<button mat-button mat-raised-button [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" color="primary"
color="primary" (click)="nextStep(3)">{{ 'action.continue' | translate }}</button>
(click)="nextStep(3)">{{ 'action.continue' | translate }}</button> </div>
</div> </div>
</mat-step> </mat-step>
<mat-step [stepControl]="columnTypesFormGroup"> <mat-step [stepControl]="columnTypesFormGroup">
@ -98,19 +96,19 @@
<ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template> <ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template>
<tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment> <tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment>
</form> </form>
<div fxLayout="row"> <div fxLayout="row wrap" fxLayoutAlign="space-between center">
<button mat-button <button mat-button
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button> (click)="previousStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span> <div fxLayout="row wrap" fxLayoutGap="20px">
<button mat-button <button mat-button
style="margin-right: 20px;" [disabled]="(isLoading$ | async)"
[disabled]="(isLoading$ | async)" (click)="cancel()">{{ 'action.cancel' | translate }}</button>
(click)="cancel()">{{ 'action.cancel' | translate }}</button> <button mat-raised-button
<button mat-button mat-raised-button [disabled]="(isLoading$ | async) || columnTypesFormGroup.invalid || !columnTypesFormGroup.dirty"
[disabled]="(isLoading$ | async) || columnTypesFormGroup.invalid || !columnTypesFormGroup.dirty" color="primary"
color="primary" (click)="nextStep(4)">{{ 'action.continue' | translate }}</button>
(click)="nextStep(4)">{{ 'action.continue' | translate }}</button> </div>
</div> </div>
</mat-step> </mat-step>
<mat-step> <mat-step>
@ -131,9 +129,8 @@
{{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }} {{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }}
</p> </p>
</div> </div>
<div fxLayout="row"> <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<span fxFlex></span> <button mat-raised-button
<button mat-button mat-raised-button
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
color="primary" color="primary"
(click)="nextStep(6)">{{ 'action.ok' | translate }}</button> (click)="nextStep(6)">{{ 'action.ok' | translate }}</button>

1
ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.scss

@ -17,7 +17,6 @@
.mat-dialog-content { .mat-dialog-content {
.mat-vertical-content { .mat-vertical-content {
form { form {
overflow: hidden !important;
padding-bottom: 12px; padding-bottom: 12px;
} }
} }

8
ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html

@ -16,7 +16,7 @@
--> -->
<form #importForm="ngForm" [formGroup]="importFormGroup" (ngSubmit)="importFromJson()"> <form #importForm="ngForm" [formGroup]="importFormGroup" (ngSubmit)="importFromJson()">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2 translate>{{ importTitle }}</h2> <h2 translate>{{ importTitle }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-button mat-icon-button
@ -42,15 +42,13 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayoutAlign="end center">
<span fxFlex></span> <button mat-raised-button color="primary"
<button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty"> [disabled]="(isLoading$ | async) || importFormGroup.invalid || !importFormGroup.dirty">
{{ 'action.import' | translate }} {{ 'action.import' | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>

18
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -17,9 +17,6 @@
import { Widget, WidgetType } from '@app/shared/models/widget.models'; import { Widget, WidgetType } from '@app/shared/models/widget.models';
import { DashboardLayoutId } from '@shared/models/dashboard.models'; import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { ActionType } from '@shared/models/audit-log.models';
import { RuleChain, RuleChainMetaData } from '@shared/models/rule-chain.models';
export interface ImportWidgetResult { export interface ImportWidgetResult {
widget: Widget; widget: Widget;
@ -80,6 +77,21 @@ export interface CsvColumnParam {
sampleData: any; sampleData: any;
} }
export interface FileType {
mimeType: string;
extension: string;
}
export const JSON_TYPE: FileType = {
mimeType: 'text/json',
extension: 'json'
};
export const ZIP_TYPE: FileType = {
mimeType: 'application/zip',
extension: 'zip'
};
export function convertCSVToJson(csvdata: string, config: CsvToJsonConfig, export function convertCSVToJson(csvdata: string, config: CsvToJsonConfig,
onError: (messageId: string, params?: any) => void): CsvToJsonResult | number { onError: (messageId: string, params?: any) => void): CsvToJsonResult | number {
config = config || {}; config = config || {};

45
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -44,7 +44,7 @@ import {
EntityAliasesDialogData EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component'; } from '@home/components/alias/entity-aliases-dialog.component';
import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
import { ImportWidgetResult, WidgetsBundleItem } from './import-export.models'; import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE } from './import-export.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
@ -54,6 +54,7 @@ import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entit
import { RequestConfig } from '@core/http/http-utils'; import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models'; import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import * as JSZip from 'jszip';
// @dynamic // @dynamic
@Injectable() @Injectable()
@ -79,7 +80,7 @@ export class ImportExportService {
(dashboard) => { (dashboard) => {
let name = dashboard.title; let name = dashboard.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json'); this.exportToPc(this.prepareDashboardExport(dashboard), name);
}, },
(e) => { (e) => {
this.handleExportError(e, 'dashboard.export-failed-error'); this.handleExportError(e, 'dashboard.export-failed-error');
@ -136,7 +137,7 @@ export class ImportExportService {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
let name = widgetItem.widget.config.title; let name = widgetItem.widget.config.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetItem), name + '.json'); this.exportToPc(this.prepareExport(widgetItem), name);
} }
public importWidget(dashboard: Dashboard, targetState: string, public importWidget(dashboard: Dashboard, targetState: string,
@ -235,7 +236,7 @@ export class ImportExportService {
} }
let name = widgetType.name; let name = widgetType.name;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetType), name + '.json'); this.exportToPc(this.prepareExport(widgetType), name);
}, },
(e) => { (e) => {
this.handleExportError(e, 'widget-type.export-failed-error'); this.handleExportError(e, 'widget-type.export-failed-error');
@ -281,7 +282,7 @@ export class ImportExportService {
} }
let name = widgetsBundle.title; let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name + '.json'); this.exportToPc(widgetsBundleItem, name);
}, },
(e) => { (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error'); this.handleExportError(e, 'widgets-bundle.export-failed-error');
@ -383,7 +384,7 @@ export class ImportExportService {
).subscribe((ruleChainExport) => { ).subscribe((ruleChainExport) => {
let name = ruleChainExport.ruleChain.name; let name = ruleChainExport.ruleChain.name;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(ruleChainExport, name + '.json'); this.exportToPc(ruleChainExport, name);
}, },
(e) => { (e) => {
this.handleExportError(e, 'rulechain.export-failed-error'); this.handleExportError(e, 'rulechain.export-failed-error');
@ -414,6 +415,19 @@ export class ImportExportService {
); );
} }
public exportJSZip(data: object, filename: string) {
const jsZip: JSZip = new JSZip();
for (const keyName in data) {
if (data.hasOwnProperty(keyName)) {
const valueData = data[keyName];
jsZip.file(keyName, valueData);
}
}
jsZip.generateAsync({type: 'blob'}).then(content => {
this.downloadFile(content, filename, ZIP_TYPE);
});
}
private prepareRuleChain(ruleChain: RuleChain): RuleChain { private prepareRuleChain(ruleChain: RuleChain): RuleChain {
ruleChain = this.prepareExport(ruleChain); ruleChain = this.prepareExport(ruleChain);
if (ruleChain.firstRuleNodeId) { if (ruleChain.firstRuleNodeId) {
@ -678,13 +692,22 @@ export class ImportExportService {
console.error('No data'); console.error('No data');
return; return;
} }
if (!filename) { this.exportJson(data, filename);
filename = 'download.json'; }
}
private exportJson(data: any, filename: string) {
if (isObject(data)) { if (isObject(data)) {
data = JSON.stringify(data, null, 2); data = JSON.stringify(data, null, 2);
} }
const blob = new Blob([data], {type: 'text/json'}); this.downloadFile(data, filename, JSON_TYPE);
}
private downloadFile(data: any, filename: string, fileType: FileType) {
if (!filename) {
filename = 'download';
}
filename += '.' + fileType.extension;
const blob = new Blob([data], {type: fileType.mimeType});
if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
this.window.navigator.msSaveOrOpenBlob(blob, filename); this.window.navigator.msSaveOrOpenBlob(blob, filename);
} else { } else {
@ -692,7 +715,7 @@ export class ImportExportService {
const a = this.document.createElement('a'); const a = this.document.createElement('a');
a.download = filename; a.download = filename;
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.dataset.downloadurl = ['text/json', a.download, a.href].join(':'); a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':');
// @ts-ignore // @ts-ignore
e.initEvent('click', true, false, this.window, e.initEvent('click', true, false, this.window,
0, 0, 0, 0, 0, false, false, false, false, 0, null); 0, 0, 0, 0, 0, false, false, false, false, 0, null);

10
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-table [dataSource]="dataSource"> <table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="order"> <ng-container matColumnDef="order">
<mat-header-cell *matHeaderCellDef style="min-width: 40px; max-width: 40px; width: 40px"></mat-header-cell> <mat-header-cell *matHeaderCellDef style="min-width: 40px; max-width: 40px; width: 40px"></mat-header-cell>
<mat-cell *matCellDef="let column; let i = index"> <mat-cell *matCellDef="let column; let i = index">
@ -23,13 +23,13 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="sampleData"> <ng-container matColumnDef="sampleData">
<mat-header-cell *matHeaderCellDef style="width: 30%"> {{ 'import.column-example' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="flex: 0 0 30%;"> {{ 'import.column-example' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
{{column.sampleData}} {{column.sampleData}}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef style="width: 40%"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="flex: 0 0 40%"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
<mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()"> <mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
<mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled"> <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled">
@ -39,7 +39,7 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef style="width: 30%"> {{ 'import.column-key' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="flex: 0 0 30%"> {{ 'import.column-key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let column"> <mat-cell *matCellDef="let column">
<mat-form-field floatLabel="always" hideRequiredMarker <mat-form-field floatLabel="always" hideRequiredMarker
*ngIf="isColumnTypeDiffers(column.type)"> *ngIf="isColumnTypeDiffers(column.type)">
@ -52,5 +52,5 @@
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let column; columns: displayedColumns;"></mat-row> <mat-row *matRowDef="let column; columns: displayedColumns;"></mat-row>
</mat-table> </table>
<mat-divider></mat-divider> <mat-divider></mat-divider>

25
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts

@ -19,8 +19,12 @@ import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Va
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { CsvColumnParam, ImportEntityColumnType, importEntityColumnTypeTranslations, import {
importEntityObjectColumns } from '@home/components/import-export/import-export.models'; CsvColumnParam,
ImportEntityColumnType,
importEntityColumnTypeTranslations,
importEntityObjectColumns
} from '@home/components/import-export/import-export.models';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
@ -51,8 +55,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
displayedColumns = ['order', 'sampleData', 'type', 'key']; displayedColumns = ['order', 'sampleData', 'type', 'key'];
importEntityColumnType = ImportEntityColumnType;
columnTypes: AssignmentColumnType[] = []; columnTypes: AssignmentColumnType[] = [];
columnTypesTranslations = importEntityColumnTypeTranslations; columnTypesTranslations = importEntityColumnTypeTranslations;
@ -123,9 +125,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.name).disabled = isSelectName; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.name).disabled = isSelectName;
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.type).disabled = isSelectType; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.type).disabled = isSelectType;
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel;
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway).disabled = isSelectGateway;
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription; this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription;
const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);
if (isGatewayColumnType) {
isGatewayColumnType.disabled = isSelectGateway;
}
const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken); const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken);
if (accessTokenColumnType) { if (accessTokenColumnType) {
accessTokenColumnType.disabled = isSelectCredentials; accessTokenColumnType.disabled = isSelectCredentials;
@ -138,12 +143,10 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
} }
public isColumnTypeDiffers(columnType: ImportEntityColumnType): boolean { public isColumnTypeDiffers(columnType: ImportEntityColumnType): boolean {
return columnType !== ImportEntityColumnType.name && return columnType === ImportEntityColumnType.clientAttribute ||
columnType !== ImportEntityColumnType.type && columnType === ImportEntityColumnType.sharedAttribute ||
columnType !== ImportEntityColumnType.label && columnType === ImportEntityColumnType.serverAttribute ||
columnType !== ImportEntityColumnType.accessToken && columnType === ImportEntityColumnType.timeseries;
columnType !== ImportEntityColumnType.isGateway &&
columnType !== ImportEntityColumnType.description;
} }
private columnValid(column: CsvColumnParam): boolean { private columnValid(column: CsvColumnParam): boolean {

50
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -19,30 +19,33 @@
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span class="tb-entity-table-title">{{(direction == directions.FROM ? <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
'relation.from-relations' : 'relation.to-relations') | translate}}</span> <span class="tb-entity-table-title">
<mat-form-field class="mat-block tb-relation-direction" style="width: 200px;"> {{(direction == directions.FROM ? 'relation.from-relations' : 'relation.to-relations') | translate}}
<mat-label translate>relation.direction</mat-label> </span>
<mat-select matInput [ngModel]="direction" <mat-form-field class="mat-block tb-relation-direction">
(ngModelChange)="directionChanged($event)"> <mat-label translate>relation.direction</mat-label>
<mat-option *ngFor="let type of directionTypes" [value]="type"> <mat-select [ngModel]="direction"
{{ directionTypeTranslations.get(directions[type]) | translate }} (ngModelChange)="directionChanged($event)">
</mat-option> <mat-option *ngFor="let type of directionTypes" [value]="type">
</mat-select> {{ directionTypeTranslations.get(directions[type]) | translate }}
</mat-form-field> </mat-option>
</mat-select>
</mat-form-field>
</div>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
(click)="addRelation($event)" (click)="addRelation($event)"
matTooltip="{{ 'action.add' | translate }}" matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="reloadRelations()" <button mat-icon-button [disabled]="isLoading$ | async" (click)="reloadRelations()"
matTooltip="{{ 'action.refresh' | translate }}" matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" <button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}" matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
@ -51,7 +54,7 @@
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<button mat-button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.search' | translate }}" matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>search</mat-icon> <mat-icon>search</mat-icon>
@ -62,7 +65,7 @@
[(ngModel)]="pageLink.textSearch" [(ngModel)]="pageLink.textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/> placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field> </mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()" <button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}" matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -70,12 +73,11 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()"> <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" fxLayoutAlign="space-between center">
<span> <span class="tb-entity-table-info">
{{ translate.get('relation.selected-relations', {count: dataSource.selection.selected.length}) | async }} {{ 'relation.selected-relations' | translate:{count: dataSource.selection.selected.length} }}
</span> </span>
<span fxFlex></span> <button mat-icon-button [disabled]="isLoading$ | async"
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}" matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteRelations($event)"> (click)="deleteRelations($event)">
@ -135,13 +137,13 @@
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let relation"> <mat-cell *matCellDef="let relation">
<div fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'relation.edit' | translate }}" matTooltip="{{ 'relation.edit' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="editRelation($event, relation)"> (click)="editRelation($event, relation)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'relation.delete' | translate }}" matTooltip="{{ 'relation.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="deleteRelation($event, relation)"> (click)="deleteRelation($event, relation)">

37
ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -22,6 +24,14 @@
height: 100%; height: 100%;
background: #fff; background: #fff;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-entity-table-title { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; white-space: nowrap;
@ -32,6 +42,24 @@
.table-container { .table-container {
overflow: auto; overflow: auto;
} }
.tb-entity-table-info{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-entity-table-title{
padding-bottom: 5px;
width: 100%;
}
} }
} }
} }
@ -42,6 +70,7 @@
} }
mat-form-field.tb-relation-direction { mat-form-field.tb-relation-direction {
font-size: 16px; font-size: 16px;
width: 200px;
.mat-form-field-wrapper { .mat-form-field-wrapper {
padding-bottom: 0; padding-bottom: 0;
@ -50,5 +79,13 @@
.mat-form-field-underline { .mat-form-field-underline {
bottom: 0; bottom: 0;
} }
@media #{$mat-xs} {
width: 100%;
.mat-form-field-infix {
width: auto !important;
}
}
} }
} }

36
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw

@ -1,21 +1,9 @@
/*================================================================================*/
/*======================= New TB 3.0 / Angular 8 Example =======================*/
/*================================================================================*/
/*
.edit-entity-form mat-form-field {
padding-right: 10px;
}
*/
/*=======================================================================*/ /*=======================================================================*/
/*========== There are two examples: for edit and add entity ==========*/ /*========== There are two examples: for edit and add entity ==========*/
/*=======================================================================*/ /*=======================================================================*/
/*======================== Edit entity example ========================*/ /*======================== Edit entity example ========================*/
/*=======================================================================*/ /*=======================================================================*/
/* /*
.edit-entity-form md-input-container {
padding-right: 10px;
}
.edit-entity-form .boolean-value-input { .edit-entity-form .boolean-value-input {
padding-left: 5px; padding-left: 5px;
} }
@ -41,6 +29,10 @@
white-space: nowrap; white-space: nowrap;
} }
.relations-list .mat-form-field-infix {
width: auto !important;
}
.relations-list .body { .relations-list .body {
padding-right: 5px; padding-right: 5px;
padding-bottom: 15px; padding-bottom: 15px;
@ -56,26 +48,14 @@
padding-left: 5px; padding-left: 5px;
} }
.relations-list .body md-autocomplete-wrap md-input-container {
height: 30px;
}
.relations-list .body .md-button { .relations-list .body .md-button {
margin: 0; margin: 0;
} }
.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {
display: none;
}
*/ */
/*========================================================================*/ /*========================================================================*/
/*========================= Add entity example =========================*/ /*========================= Add entity example =========================*/
/*========================================================================*/ /*========================================================================*/
/* /*
.add-entity-form md-input-container {
padding-right: 10px;
}
.add-entity-form .boolean-value-input { .add-entity-form .boolean-value-input {
padding-left: 5px; padding-left: 5px;
} }
@ -101,6 +81,10 @@
white-space: nowrap; white-space: nowrap;
} }
.relations-list .mat-form-field-infix {
width: auto !important;
}
.relations-list .body { .relations-list .body {
padding-right: 5px; padding-right: 5px;
padding-bottom: 15px; padding-bottom: 15px;
@ -116,10 +100,6 @@
padding-left: 5px; padding-left: 5px;
} }
.relations-list .body md-autocomplete-wrap md-input-container {
height: 30px;
}
.relations-list .body .md-button { .relations-list .body .md-button {
margin: 0; margin: 0;
} }

646
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw

@ -1,349 +1,355 @@
<!--=======================================================================--> <!--=======================================================================-->
<!--=================== New TB 3.0 / Angular 8 Example =================-->
<!--=======================================================================-->
<!--<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"-->
<!-- class="edit-entity-form"-->
<!-- (ngSubmit)="save()" style="width: 600px;">-->
<!-- <mat-toolbar fxLayout="row" color="primary">-->
<!-- <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>-->
<!-- <span fxFlex></span>-->
<!-- <button mat-button mat-icon-button-->
<!-- (click)="cancel()"-->
<!-- type="button">-->
<!-- <mat-icon class="material-icons">close</mat-icon>-->
<!-- </button>-->
<!-- </mat-toolbar>-->
<!-- <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">-->
<!-- </mat-progress-bar>-->
<!-- <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>-->
<!-- <div mat-dialog-content>-->
<!-- <div class="mat-padding" fxLayout="column">-->
<!-- <mat-form-field class="mat-block">-->
<!-- <mat-label>Entity name</mat-label>-->
<!-- <input matInput formControlName="entityName" required>-->
<!-- <mat-error *ngIf="editEntityFormGroup.get('entityName').hasError('required')">-->
<!-- Entity name required-->
<!-- </mat-error>-->
<!-- </mat-form-field>-->
<!-- </div> -->
<!-- </div>-->
<!-- <div mat-dialog-actions fxLayout="row">-->
<!-- <span fxFlex></span>-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">-->
<!-- {{ 'action.save' | translate }}-->
<!-- </button>-->
<!-- <button mat-button color="primary"-->
<!-- style="margin-right: 20px;"-->
<!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>-->
<!-- {{ 'action.cancel' | translate }}-->
<!-- </button>-->
<!-- </div>-->
<!--</form>-->
<!--=======================================================================-->
<!--===== There are two example templates: for edit and add entity =====--> <!--===== There are two example templates: for edit and add entity =====-->
<!--=======================================================================--> <!--=======================================================================-->
<!--======================== Edit entity example ========================--> <!--======================== Edit entity example ========================-->
<!--=======================================================================--> <!--=======================================================================-->
<!-- --> <!-- -->
<!--<md-dialog aria-label="Edit entity">--> <!--<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"-->
<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">--> <!-- (ngSubmit)="save()" class="edit-entity-form">-->
<!-- <md-toolbar>--> <!-- <mat-toolbar fxLayout="row" color="primary">-->
<!-- <div class="md-toolbar-tools">--> <!-- <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>-->
<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>--> <!-- <span fxFlex></span>-->
<!-- <span flex></span>--> <!-- <button mat-icon-button (click)="cancel()" type="button">-->
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> <!-- <mat-icon class="material-icons">close</mat-icon>-->
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> <!-- </button>-->
<!-- </md-button>--> <!-- </mat-toolbar>-->
<!-- <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">-->
<!-- </mat-progress-bar>-->
<!-- <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>-->
<!-- <div mat-dialog-content fxLayout="column">-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Entity Name</mat-label>-->
<!-- <input matInput formControlName="entityName" required readonly="">-->
<!-- </mat-form-field>-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Entity Label</mat-label>-->
<!-- <input matInput formControlName="entityLabel">-->
<!-- </mat-form-field>-->
<!-- </div>-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Entity Type</mat-label>-->
<!-- <input matInput formControlName="entityType" readonly>-->
<!-- </mat-form-field>-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Type</mat-label>-->
<!-- <input matInput formControlName="type" readonly>-->
<!-- </mat-form-field>-->
<!-- </div>-->
<!-- <div formGroupName="attributes" fxLayout="column">-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Latitude</mat-label>-->
<!-- <input type="number" step="any" matInput formControlName="latitude">-->
<!-- </mat-form-field>-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Longitude</mat-label>-->
<!-- <input type="number" step="any" matInput formControlName="longitude">-->
<!-- </mat-form-field>-->
<!-- </div>--> <!-- </div>-->
<!-- </md-toolbar>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <md-dialog-content>--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <div class="md-dialog-content">--> <!-- <mat-label>Address</mat-label>-->
<!-- <div layout="row">--> <!-- <input matInput formControlName="address">-->
<!-- <md-input-container flex class="md-block">--> <!-- </mat-form-field>-->
<!-- <label>Entity Name</label>--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <input ng-model="vm.entityName" readonly>--> <!-- <mat-label>Owner</mat-label>-->
<!-- </md-input-container>--> <!-- <input matInput formControlName="owner">-->
<!-- <md-input-container flex class="md-block">--> <!-- </mat-form-field>-->
<!-- <label>Entity Type</label>--> <!-- </div>-->
<!-- <input ng-model="vm.entityType" readonly>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- </md-input-container>--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <md-input-container flex class="md-block">--> <!-- <mat-label>Integer Value</mat-label>-->
<!-- <label>Type</label>--> <!-- <input type="number" step="1" matInput formControlName="number">-->
<!-- <input ng-model="vm.type" readonly>--> <!-- <mat-error *ngIf="editEntityFormGroup.get('attributes').get('number').hasError('pattern')">-->
<!-- </md-input-container>--> <!-- Invalid integer value.-->
<!-- </div>--> <!-- </mat-error>-->
<!-- <div layout="row">--> <!-- </mat-form-field>-->
<!-- <md-input-container flex class="md-block">--> <!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>-->
<!-- <label>Latitude</label>--> <!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> <!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">-->
<!-- </md-input-container>--> <!-- {{ (editEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- <md-input-container flex class="md-block">--> <!-- </mat-checkbox>-->
<!-- <label>Longitude</label>-->
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">-->
<!-- </md-input-container>-->
<!-- </div>-->
<!-- <div layout="row">-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Address</label>-->
<!-- <input ng-model="vm.attributes.address">-->
<!-- </md-input-container>-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Owner</label>-->
<!-- <input ng-model="vm.attributes.owner">-->
<!-- </md-input-container>-->
<!-- </div>-->
<!-- <div layout="row">-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Integer Value</label>-->
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">-->
<!-- <div ng-messages="editEntityForm.integerNumber.$error">-->
<!-- <div ng-message="pattern">Invalid integer value.</div>-->
<!-- </div>-->
<!-- </md-input-container>-->
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>-->
<!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">-->
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}-->
<!-- </md-checkbox>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div class="relations-list old-relations">--> <!-- </div>-->
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> <!-- </div>-->
<!-- <div class="body" ng-show="vm.relations.length">--> <!-- <div class="relations-list old-relations">-->
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> <!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>-->
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> <!-- <div class="body" [fxShow]="oldRelations().length">-->
<!-- <div flex layout="column">--> <!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations" -->
<!-- <div layout="row">--> <!-- *ngFor="let relation of oldRelations().controls; let i = index;">-->
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> <!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">-->
<!-- <label>Direction</label>--> <!-- <div fxFlex fxLayout="column">-->
<!-- <md-select ng-disabled="true" required ng-model="relation.direction">--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> <!-- <mat-form-field class="mat-block" style="min-width: 100px;">-->
<!-- {{ ("relation.search-direction." + direction) | translate}}--> <!-- <mat-label>Direction</mat-label>-->
<!-- </md-option>--> <!-- <mat-select formControlName="direction" name="direction">-->
<!-- </md-select>--> <!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">-->
<!-- </md-input-container>--> <!-- {{ ("relation.search-direction." + direction.value) | translate}}-->
<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"--> <!-- </mat-option>-->
<!-- the-form="editEntityForm"--> <!-- </mat-select>-->
<!-- ng-model="relation.relationType"--> <!-- <mat-error *ngIf="relation.get('direction').hasError('required')">-->
<!-- tb-required="true">--> <!-- Relation direction is required.-->
<!-- </tb-relation-type-autocomplete>--> <!-- </mat-error>-->
<!-- </div>--> <!-- </mat-form-field>-->
<!-- <div layout="row">--> <!-- <tb-relation-type-autocomplete-->
<!-- <tb-entity-select flex class="md-block"--> <!-- fxFlex class="mat-block"-->
<!-- the-form="editEntityForm"--> <!-- formControlName="relationType"-->
<!-- ng-disabled="true"--> <!-- required="true">-->
<!-- tb-required="true"--> <!-- </tb-relation-type-autocomplete>-->
<!-- ng-model="relation.relatedEntity">-->
<!-- </tb-entity-select>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div layout="column" layout-align="center center">-->
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">-->
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
<!-- <md-icon aria-label="Remove" class="material-icons">-->
<!-- close-->
<!-- </md-icon>-->
<!-- </md-button>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div fxLayout="row" fxLayout.xs="column">-->
<!-- <tb-entity-select-->
<!-- fxFlex class="mat-block"-->
<!-- required="true"-->
<!-- formControlName="relatedEntity">-->
<!-- </tb-entity-select>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div fxLayout="column" fxLayoutAlign="center center">-->
<!-- <button mat-icon-button color="primary"-->
<!-- aria-label="Remove"-->
<!-- type="button"-->
<!-- (click)="removeOldRelation(i)"-->
<!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">-->
<!-- <mat-icon>close</mat-icon>-->
<!-- </button>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div class="relations-list">--> <!-- </div>-->
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> <!-- </div>-->
<!-- <div class="body" ng-show="vm.newRelations.length">--> <!-- <div class="relations-list">-->
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">--> <!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>-->
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> <!-- <div class="body" [fxShow]="relations().length">-->
<!-- <div flex layout="column">--> <!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">-->
<!-- <div layout="row">--> <!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">-->
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> <!-- <div fxFlex fxLayout="column">-->
<!-- <label>Direction</label>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <md-select name="direction" required ng-model="relation.direction">--> <!-- <mat-form-field class="mat-block" style="min-width: 100px;">-->
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> <!-- <mat-label>Direction</mat-label>-->
<!-- {{ ("relation.search-direction." + direction) | translate}}--> <!-- <mat-select formControlName="direction" name="direction">-->
<!-- </md-option>--> <!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">-->
<!-- </md-select>--> <!-- {{ ("relation.search-direction." + direction.value) | translate}}-->
<!-- <div ng-messages="editEntityForm.direction.$error">--> <!-- </mat-option>-->
<!-- <div ng-message="required">Relation direction is required.</div>--> <!-- </mat-select>-->
<!-- </div>--> <!-- <mat-error *ngIf="relation.get('direction').hasError('required')">-->
<!-- </md-input-container>--> <!-- Relation direction is required.-->
<!-- <tb-relation-type-autocomplete flex class="md-block"--> <!-- </mat-error>-->
<!-- the-form="editEntityForm"--> <!-- </mat-form-field>-->
<!-- ng-model="relation.relationType"--> <!-- <tb-relation-type-autocomplete-->
<!-- tb-required="true">--> <!-- fxFlex class="mat-block"-->
<!-- </tb-relation-type-autocomplete>--> <!-- formControlName="relationType"-->
<!-- </div>--> <!-- [required]="true">-->
<!-- <div layout="row">--> <!-- </tb-relation-type-autocomplete>-->
<!-- <tb-entity-select flex class="md-block"--> <!-- </div>-->
<!-- the-form="editEntityForm"--> <!-- <div fxLayout="row" fxLayout.xs="column">-->
<!-- tb-required="true"--> <!-- <tb-entity-select-->
<!-- ng-model="relation.relatedEntity">--> <!-- fxFlex class="mat-block"-->
<!-- </tb-entity-select>--> <!-- [required]="true"-->
<!-- </div>--> <!-- formControlName="relatedEntity">-->
<!-- </div>--> <!-- </tb-entity-select>-->
<!-- <div layout="column" layout-align="center center">-->
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">-->
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
<!-- <md-icon aria-label="Remove" class="material-icons">-->
<!-- close-->
<!-- </md-icon>-->
<!-- </md-button>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div fxLayout="column" fxLayoutAlign="center center">-->
<!-- <button mat-icon-button color="primary"-->
<!-- aria-label="Remove"-->
<!-- type="button"-->
<!-- (click)="removeRelation(i)"-->
<!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">-->
<!-- <mat-icon>close</mat-icon>-->
<!-- </button>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div>-->
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">-->
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>-->
<!-- Add-->
<!-- </md-button>-->
<!-- </div> -->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- </md-dialog-content>--> <!-- <div>-->
<!-- <md-dialog-actions>--> <!-- <button mat-raised-button color="primary"-->
<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>--> <!-- type="button"-->
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> <!-- (click)="addRelation()"-->
<!-- </md-dialog-actions>--> <!-- matTooltip="Add Relation"-->
<!-- </form>--> <!-- matTooltipPosition="above">-->
<!--</md-dialog>--> <!-- Add-->
<!-- </button>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">-->
<!-- Save-->
<!-- </button>-->
<!-- <button mat-button color="primary"-->
<!-- style="margin-right: 20px;"-->
<!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>-->
<!-- Cancel-->
<!-- </button>-->
<!-- </div>-->
<!--</form>-->
<!----> <!---->
<!--========================================================================--> <!--========================================================================-->
<!--========================= Add entity example =========================--> <!--========================= Add entity example =========================-->
<!--========================================================================--> <!--========================================================================-->
<!----> <!---->
<!--<md-dialog aria-label="Add entity">--> <!--<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"-->
<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">--> <!-- (ngSubmit)="save()" class="add-entity-form">-->
<!-- <md-toolbar>--> <!-- <mat-toolbar fxLayout="row" color="primary">-->
<!-- <div class="md-toolbar-tools">--> <!-- <h2>Add entity</h2>-->
<!-- <h2>Add entity</h2>--> <!-- <span fxFlex></span>-->
<!-- <span flex></span>--> <!-- <button mat-icon-button (click)="cancel()" type="button">-->
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> <!-- <mat-icon class="material-icons">close</mat-icon>-->
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> <!-- </button>-->
<!-- </md-button>--> <!-- </mat-toolbar>-->
<!-- <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">-->
<!-- </mat-progress-bar>-->
<!-- <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>-->
<!-- <div mat-dialog-content fxLayout="column">-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Entity Name</mat-label>-->
<!-- <input matInput formControlName="entityName" required>-->
<!-- <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">-->
<!-- Entity name is required.-->
<!-- </mat-error>-->
<!-- </mat-form-field>-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Entity Label</mat-label>-->
<!-- <input matInput formControlName="entityLabel" >-->
<!-- </mat-form-field>-->
<!-- </div>-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <tb-entity-type-select-->
<!-- class="mat-block"-->
<!-- formControlName="entityType"-->
<!-- [showLabel]="true"-->
<!-- [allowedEntityTypes]="allowedEntityTypes"-->
<!-- ></tb-entity-type-select>-->
<!-- <tb-entity-subtype-autocomplete-->
<!-- fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"-->
<!-- class="mat-block"-->
<!-- formControlName="type"-->
<!-- [required]="true"-->
<!-- [entityType]="'ASSET'"-->
<!-- ></tb-entity-subtype-autocomplete>-->
<!-- <tb-entity-subtype-autocomplete-->
<!-- fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"-->
<!-- class="mat-block"-->
<!-- formControlName="type"-->
<!-- [required]="true"-->
<!-- [entityType]="'DEVICE'"-->
<!-- ></tb-entity-subtype-autocomplete>-->
<!-- </div>-->
<!-- <div formGroupName="attributes" fxLayout="column">-->
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Latitude</mat-label>-->
<!-- <input type="number" step="any" matInput formControlName="latitude">-->
<!-- </mat-form-field>-->
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Longitude</mat-label>-->
<!-- <input type="number" step="any" matInput formControlName="longitude">-->
<!-- </mat-form-field>-->
<!-- </div>--> <!-- </div>-->
<!-- </md-toolbar>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <md-dialog-content>--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <div class="md-dialog-content">--> <!-- <mat-label>Address</mat-label>-->
<!-- <div layout="row">--> <!-- <input matInput formControlName="address">-->
<!-- <md-input-container flex class="md-block">--> <!-- </mat-form-field>-->
<!-- <label>Entity Name</label>--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <input ng-model="vm.entityName" name=entityName required>--> <!-- <mat-label>Owner</mat-label>-->
<!-- <div ng-messages="addEntityForm.entityName.$error">--> <!-- <input matInput formControlName="owner">-->
<!-- <div ng-message="required">Entity name is required.</div>--> <!-- </mat-form-field>-->
<!-- </div>--> <!-- </div>-->
<!-- </md-input-container>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"--> <!-- <mat-form-field fxFlex class="mat-block">-->
<!-- the-form="addEntityForm"--> <!-- <mat-label>Integer Value</mat-label>-->
<!-- tb-required="true"--> <!-- <input type="number" step="1" matInput formControlName="number">-->
<!-- allowed-entity-types="vm.allowedEntityTypes"--> <!-- <mat-error *ngIf="addEntityFormGroup.get('attributes').get('number').hasError('pattern')">-->
<!-- ng-model="vm.entityType">--> <!-- Invalid integer value.-->
<!-- </tb-entity-type-select>--> <!-- </mat-error>-->
<!-- <md-input-container flex class="md-block">--> <!-- </mat-form-field>-->
<!-- <label>Entity Subtype</label>--> <!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>-->
<!-- <input ng-model="vm.type" name=type required>--> <!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <div ng-messages="addEntityForm.type.$error">--> <!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">-->
<!-- <div ng-message="required">Entity subtype is required.</div>--> <!-- {{ (addEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- </div>--> <!-- </mat-checkbox>-->
<!-- </md-input-container>-->
<!-- </div>-->
<!-- <div layout="row">-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Latitude</label>-->
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">-->
<!-- </md-input-container>-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Longitude</label>-->
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">-->
<!-- </md-input-container>-->
<!-- </div>-->
<!-- <div layout="row">-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Address</label>-->
<!-- <input ng-model="vm.attributes.address">-->
<!-- </md-input-container>-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Owner</label>-->
<!-- <input ng-model="vm.attributes.owner">-->
<!-- </md-input-container>-->
<!-- </div>-->
<!-- <div layout="row">-->
<!-- <md-input-container flex class="md-block">-->
<!-- <label>Integer Value</label>-->
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">-->
<!-- <div ng-messages="addEntityForm.integerNumber.$error">-->
<!-- <div ng-message="pattern">Invalid integer value.</div>-->
<!-- </div>-->
<!-- </md-input-container>-->
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>-->
<!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">-->
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}-->
<!-- </md-checkbox>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div class="relations-list">--> <!-- </div>-->
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> <!-- </div>-->
<!-- <div class="body" ng-show="vm.relations.length">--> <!-- <div class="relations-list">-->
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> <!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>-->
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">--> <!-- <div class="body" [fxShow]="relations().length">-->
<!-- <div flex layout="column">--> <!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">-->
<!-- <div layout="row">--> <!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">-->
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> <!-- <div fxFlex fxLayout="column">-->
<!-- <label>Direction</label>--> <!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">-->
<!-- <md-select name="direction" required ng-model="relation.direction">--> <!-- <mat-form-field class="mat-block" style="min-width: 100px;">-->
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> <!-- <mat-label>Direction</mat-label>-->
<!-- {{ ("relation.search-direction." + direction) | translate}}--> <!-- <mat-select formControlName="direction" name="direction">-->
<!-- </md-option>--> <!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">-->
<!-- </md-select>--> <!-- {{ ("relation.search-direction." + direction.value) | translate}}-->
<!-- <div ng-messages="addEntityForm.direction.$error">--> <!-- </mat-option>-->
<!-- <div ng-message="required">Relation direction is required.</div>--> <!-- </mat-select>-->
<!-- </div>--> <!-- <mat-error *ngIf="relation.get('direction').hasError('required')">-->
<!-- </md-input-container>--> <!-- Relation direction is required.-->
<!-- <tb-relation-type-autocomplete flex class="md-block"--> <!-- </mat-error>-->
<!-- the-form="addEntityForm"--> <!-- </mat-form-field>-->
<!-- ng-model="relation.relationType"--> <!-- <tb-relation-type-autocomplete-->
<!-- tb-required="true">--> <!-- fxFlex class="mat-block"-->
<!-- </tb-relation-type-autocomplete>--> <!-- formControlName="relationType"-->
<!-- </div>--> <!-- [required]="true">-->
<!-- <div layout="row">--> <!-- </tb-relation-type-autocomplete>-->
<!-- <tb-entity-select flex class="md-block"-->
<!-- the-form="addEntityForm"-->
<!-- tb-required="true"-->
<!-- ng-model="relation.relatedEntity">-->
<!-- </tb-entity-select>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div layout="column" layout-align="center center">-->
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">-->
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
<!-- <md-icon aria-label="Remove" class="material-icons">-->
<!-- close-->
<!-- </md-icon>-->
<!-- </md-button>-->
<!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div fxLayout="row" fxLayout.xs="column">-->
<!-- <tb-entity-select-->
<!-- fxFlex class="mat-block"-->
<!-- [required]="true"-->
<!-- formControlName="relatedEntity">-->
<!-- </tb-entity-select>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div fxLayout="column" fxLayoutAlign="center center">-->
<!-- <button mat-icon-button color="primary"-->
<!-- aria-label="Remove"-->
<!-- type="button"-->
<!-- (click)="removeRelation(i)"-->
<!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">-->
<!-- <mat-icon>close</mat-icon>-->
<!-- </button>-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div>-->
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">-->
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>-->
<!-- Add-->
<!-- </md-button>-->
<!-- </div> -->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- </md-dialog-content>--> <!-- <div>-->
<!-- <md-dialog-actions>--> <!-- <button mat-raised-button color="primary"-->
<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>--> <!-- type="button"-->
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> <!-- (click)="addRelation()"-->
<!-- </md-dialog-actions>--> <!-- matTooltip="Add Relation"-->
<!-- </form>--> <!-- matTooltipPosition="above">-->
<!--</md-dialog>--> <!-- Add-->
<!-- </button>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">-->
<!-- Create-->
<!-- </button>-->
<!-- <button mat-button color="primary"-->
<!-- style="margin-right: 20px;"-->
<!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>-->
<!-- Cancel-->
<!-- </button>-->
<!-- </div>-->
<!--</form>-->

562
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw

@ -1,83 +1,34 @@
/*================================================================================*/
/*======================= New TB 3.0 / Angular 8 Example =======================*/
/*================================================================================*/
//
//let $injector = widgetContext.$scope.$injector;
//let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
//
//deviceService.getDevice(entityId.id).subscribe(function(device) {
// console.log(device);
//});
//
//
//let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
//
//customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(
// function(res) {
// console.log(res);
// }
//);
//
//function EditEntityDialogController(instance) {
// let vm = instance;
// vm.entityId = entityId;
// vm.entityName = entityName;
// vm.entityType = entityId.entityType;
//
// vm.editEntityFormGroup = vm.fb.group({
// entityName: [vm.entityName, [vm.validators.required]]
// });
//
// vm.cancel = function() {
// vm.dialogRef.close(null);
// };
//
// vm.save = function() {
// const newVal = vm.editEntityFormGroup.value;
// vm.dialogRef.close(newVal);
// };
//}
//
/*=======================================================================*/ /*=======================================================================*/
/*===== There are three examples: for delete, edit and add entity =====*/ /*===== There are three examples: for delete, edit and add entity =====*/
/*=======================================================================*/ /*=======================================================================*/
/*======================= Delete entity example =======================*/ /*======================= Delete entity example =======================*/
/*=======================================================================*/ /*=======================================================================*/
// //
//var $injector = widgetContext.$scope.$injector; //let $injector = widgetContext.$scope.$injector;
//var $mdDialog = $injector.get('$mdDialog'), //let dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
// $document = $injector.get('$document'), //let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
// types = $injector.get('types'), //let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
// assetService = $injector.get('assetService'),
// deviceService = $injector.get('deviceService')
// $rootScope = $injector.get('$rootScope'),
// $q = $injector.get('$q');
// //
//openDeleteEntityDialog(); //openDeleteEntityDialog();
// //
//function openDeleteEntityDialog() { //function openDeleteEntityDialog() {
// var title = 'Delete ' + entityId.entityType // let title = 'Delete ' + entityId.entityType.toLowerCase() + ' ' +
// .toLowerCase() + ' ' + // entityName;
// entityName; // let content = 'Are you sure you want to delete the ' +
// var content = 'Are you sure you want to delete the ' + // entityId.entityType.toLowerCase() + ' ' + entityName + '?';
// entityId.entityType.toLowerCase() + ' ' + // dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(
// entityName + '?'; // function(result) {
// var confirm = $mdDialog.confirm() // if (result) {
// .targetEvent($event) // deleteEntity();
// .title(title) // }
// .htmlContent(content) // }
// .ariaLabel(title) // );
// .cancel('Cancel')
// .ok('Delete');
// $mdDialog.show(confirm).then(function() {
// deleteEntity();
// })
//} //}
// //
//function deleteEntity() { //function deleteEntity() {
// deleteEntityPromise(entityId).then( // deleteEntityObservable(entityId).subscribe(
// function success() { // function success() {
// updateAliasData(); // widgetContext.updateAliases();
// }, // },
// function fail() { // function fail() {
// showErrorDialog(); // showErrorDialog();
@ -85,228 +36,243 @@
// ); // );
//} //}
// //
//function deleteEntityPromise(entityId) { //function deleteEntityObservable(entityId) {
// if (entityId.entityType == types.entityType.asset) { // if (entityId.entityType == "ASSET") {
// return assetService.deleteAsset(entityId.id); // return assetService.deleteAsset(entityId.id);
// } else if (entityId.entityType == types.entityType.device) { // } else if (entityId.entityType == "DEVICE") {
// return deviceService.deleteDevice(entityId.id); // return deviceService.deleteDevice(entityId.id);
// } // }
//} //}
// //
//function updateAliasData() {
// var aliasIds = [];
// for (var id in widgetContext.aliasController.resolvedAliases) {
// aliasIds.push(id);
// }
// var tasks = [];
// aliasIds.forEach(function(aliasId) {
// widgetContext.aliasController.setAliasUnresolved(aliasId);
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
// });
// $q.all(tasks).then(function() {
// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
// });
//}
//
//function showErrorDialog() { //function showErrorDialog() {
// var title = 'Error'; // let title = 'Error';
// var content = 'An error occurred while deleting the entity. Please try again.'; // let content = 'An error occurred while deleting the entity. Please try again.';
// var alert = $mdDialog.alert() // dialogs.alert(title, content, 'CLOSE').subscribe(
// .title(title) // function(result) {}
// .htmlContent(content) // );
// .ariaLabel(title)
// .parent(angular.element($document[0].body))
// .targetEvent($event)
// .multiple(true)
// .clickOutsideToClose(true)
// .ok('CLOSE');
// $mdDialog.show(alert);
//} //}
// //
/*=======================================================================*/ /*=======================================================================*/
/*======================== Edit entity example ========================*/ /*======================== Edit entity example ========================*/
/*=======================================================================*/ /*=======================================================================*/
// //
//var $injector = widgetContext.$scope.$injector; //let $injector = widgetContext.$scope.$injector;
//var $mdDialog = $injector.get('$mdDialog'), //let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
// $document = $injector.get('$document'), //let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
// $q = $injector.get('$q'), //let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
// types = $injector.get('types'), //let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
// $rootScope = $injector.get('$rootScope'), //let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
// entityService = $injector.get('entityService'), //let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
// attributeService = $injector.get('attributeService'),
// entityRelationService = $injector.get('entityRelationService');
// //
//openEditEntityDialog(); //openEditEntityDialog();
// //
//function openEditEntityDialog() { //function openEditEntityDialog() {
// $mdDialog.show({ // customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();
// controller: ['$scope','$mdDialog', EditEntityDialogController],
// controllerAs: 'vm',
// template: htmlTemplate,
// locals: {
// entityId: entityId
// },
// parent: angular.element($document[0].body),
// targetEvent: $event,
// multiple: true,
// clickOutsideToClose: false
// });
//} //}
// //
//function EditEntityDialogController($scope,$mdDialog) { //function EditEntityDialogController(instance) {
// var vm = this; // let vm = instance;
// vm.entityId = entityId; //
// vm.entityName = entityName; // vm.entityName = entityName;
// vm.entityType = entityId.entityType; // vm.entityType = entityId.entityType;
// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; // vm.entitySearchDirection = {
// vm.allowedRelatedEntityTypes = []; // from: "FROM",
// vm.entitySearchDirection = types.entitySearchDirection; // to: "TO"
// };
// vm.attributes = {}; // vm.attributes = {};
// vm.serverAttributes = {}; // vm.oldRelationsData = [];
// vm.relations = [];
// vm.newRelations = [];
// vm.relationsToDelete = []; // vm.relationsToDelete = [];
// vm.entity = {};
//
// vm.editEntityFormGroup = vm.fb.group({
// entityName: ['', [vm.validators.required]],
// entityType: [null],
// entityLabel: [null],
// type: ['', [vm.validators.required]],
// attributes: vm.fb.group({
// latitude: [null],
// longitude: [null],
// address: [null],
// owner: [null],
// number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
// booleanValue: [false]
// }),
// oldRelations: vm.fb.array([]),
// relations: vm.fb.array([])
// });
//
// getEntityInfo(); // getEntityInfo();
// //
// vm.cancel = function() {
// vm.dialogRef.close(null);
// };
//
// vm.relations = function() {
// return vm.editEntityFormGroup.get('relations');
// };
//
// vm.oldRelations = function() {
// return vm.editEntityFormGroup.get('oldRelations');
// };
//
// vm.addRelation = function() { // vm.addRelation = function() {
// var relation = { // vm.relations().push(vm.fb.group({
// direction: null, // relatedEntity: [null, [vm.validators.required]],
// relationType: null, // relationType: [null, [vm.validators.required]],
// relatedEntity: null // direction: [null, [vm.validators.required]]
// }; // }));
// vm.newRelations.push(relation);
// $scope.editEntityForm.$setDirty();
// }; // };
//
// function addOldRelation() {
// vm.oldRelations().push(vm.fb.group({
// relatedEntity: [{value: null, disabled: true}, [vm.validators.required]],
// relationType: [{value: null, disabled: true}, [vm.validators.required]],
// direction: [{value: null, disabled: true}, [vm.validators.required]]
// }));
// }
//
// vm.removeRelation = function(index) { // vm.removeRelation = function(index) {
// if (index > -1) { // vm.relations().removeAt(index);
// vm.newRelations.splice(index, 1); // vm.relations().markAsDirty();
// $scope.editEntityForm.$setDirty();
// }
// }; // };
// vm.removeOldRelation = function(index, relation) { //
// if (index > -1) { // vm.removeOldRelation = function(index) {
// vm.relations.splice(index, 1); // vm.oldRelations().removeAt(index);
// vm.relationsToDelete.push(relation); // vm.relationsToDelete.push(vm.oldRelationsData[index]);
// $scope.editEntityForm.$setDirty(); // vm.oldRelations().markAsDirty();
// }
// }; // };
//
// vm.save = function() { // vm.save = function() {
// saveAttributes(); // vm.editEntityFormGroup.markAsPristine();
// saveRelations(); // widgetContext.rxjs.forkJoin([
// $scope.editEntityForm.$setPristine(); // saveAttributes(entityId),
// }; // saveRelations(entityId),
// vm.cancel = function() { // saveEntity()
// $mdDialog.hide(); // ]).subscribe(
// function () {
// widgetContext.updateAliases();
// vm.dialogRef.close(null);
// }
// );
// }; // };
// //
// function getEntityAttributes(attributes) { // function getEntityAttributes(attributes) {
// for (var i = 0; i < attributes.length; i++) { // for (var i = 0; i < attributes.length; i++) {
// vm.attributes[attributes[i].key] = attributes[i].value; // vm.attributes[attributes[i].key] = attributes[i].value;
// } // }
// vm.serverAttributes = angular.copy(vm.attributes);
// } // }
// //
// function getEntityRelations(relations) { // function getEntityRelations(relations) {
// var relationsFrom = relations[0]; // let relationsFrom = relations[0];
// var relationsTo = relations[1]; // let relationsTo = relations[1];
// for (var i=0; i < relationsFrom.length; i++) { // for (let i=0; i < relationsFrom.length; i++) {
// var relation = { // let relation = {
// direction: types.entitySearchDirection.from, // direction: 'FROM',
// relationType: relationsFrom[i].type, // relationType: relationsFrom[i].type,
// relatedEntity: relationsFrom[i].to // relatedEntity: relationsFrom[i].to
// }; // };
// vm.relations.push(relation); // vm.oldRelationsData.push(relation);
// addOldRelation();
// } // }
// for (var i=0; i < relationsTo.length; i++) { // for (let i=0; i < relationsTo.length; i++) {
// var relation = { // let relation = {
// direction: types.entitySearchDirection.to, // direction: 'TO',
// relationType: relationsTo[i].type, // relationType: relationsTo[i].type,
// relatedEntity: relationsTo[i].from // relatedEntity: relationsTo[i].from
// }; // };
// vm.relations.push(relation); // vm.oldRelationsData.push(relation);
// addOldRelation();
// } // }
// } // }
// //
// function getEntityInfo() { // function getEntityInfo() {
// entityService.getEntity(entityId.entityType, entityId.id).then( // entityService.getEntity(entityId.entityType, entityId.id).subscribe(function (entity) {
// function(entity) { // vm.entity = entity;
// vm.entity = entity; // widgetContext.rxjs.forkJoin([
// vm.type = vm.entity.type; // entityRelationService.findInfoByFrom(entityId),
// }); // entityRelationService.findInfoByTo(entityId),
// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then( // attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE')
// function(data){ // ]).subscribe(
// if (data.length) { // function (data) {
// getEntityAttributes(data); // getEntityRelations(data.slice(0,2));
// getEntityAttributes(data[2]);
// vm.editEntityFormGroup.patchValue({
// entityName: vm.entity.name,
// entityType: vm.entityType,
// entityLabel: vm.entity.label,
// type: vm.entity.type,
// attributes: vm.attributes,
// oldRelations: vm.oldRelationsData
// }, {emitEvent: false});
// } // }
// }); // );
// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then( // });
// function(relations){
// getEntityRelations(relations);
// });
// } // }
// //
// function saveAttributes() { // function saveEntity() {
// var attributesArray = []; // const formValues = vm.editEntityFormGroup.value;
// for (var key in vm.attributes) { // if (vm.entity.label !== formValues.entityLabel){
// if (vm.attributes[key] !== vm.serverAttributes[key]) { // vm.entity.label = formValues.entityLabel;
// attributesArray.push({key: key, value: vm.attributes[key]}); // if (formValues.entityType == 'ASSET') {
// return assetService.saveAsset(vm.entity);
// } else if (formValues.entityType == 'DEVICE') {
// return deviceService.saveDevice(vm.entity);
// }
// } else {
// return widgetContext.rxjs.of([]);
// }
// }
//
// function saveAttributes(entityId) {
// let attributes = vm.editEntityFormGroup.get('attributes').value;
// let attributesArray = [];
// for (let key in attributes) {
// if (attributes[key] !== vm.attributes[key]) {
// attributesArray.push({key: key, value: attributes[key]});
// } // }
// } // }
// if (attributesArray.length > 0) { // if (attributesArray.length > 0) {
// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); // return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
// } else {
// return widgetContext.rxjs.of([]);
// } // }
// } // }
// //
// function saveRelations() { // function saveRelations(entityId) {
// var tasks = []; // let relations = vm.editEntityFormGroup.get('relations').value;
// for (var i=0; i < vm.newRelations.length; i++) { // let tasks = [];
// var relation = { // for(let i=0; i < relations.length; i++) {
// type: vm.newRelations[i].relationType // let relation = {
// type: relations[i].relationType,
// typeGroup: 'COMMON'
// }; // };
// if (vm.newRelations[i].direction == types.entitySearchDirection.from) { // if (relations[i].direction == 'FROM') {
// relation.to = vm.newRelations[i].relatedEntity; // relation.to = relations[i].relatedEntity;
// relation.from = entityId; // relation.from = entityId;
// } else { // } else {
// relation.to = entityId; // relation.to = entityId;
// relation.from = vm.newRelations[i].relatedEntity; // relation.from = relations[i].relatedEntity;
// } // }
// tasks.push(entityRelationService.saveRelation(relation)); // tasks.push(entityRelationService.saveRelation(relation));
// } // }
// for (var i=0; i < vm.relationsToDelete.length; i++) { // for (let i=0; i < vm.relationsToDelete.length; i++) {
// var relation = { // let relation = {
// type: vm.relationsToDelete[i].relationType // type: vm.relationsToDelete[i].relationType
// }; // };
// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) { // if (vm.relationsToDelete[i].direction == 'FROM') {
// relation.to = vm.relationsToDelete[i].relatedEntity; // relation.to = vm.relationsToDelete[i].relatedEntity;
// relation.from = entityId; // relation.from = entityId;
// } else { // } else {
// relation.to = entityId; // relation.to = entityId;
// relation.from = vm.relationsToDelete[i].relatedEntity; // relation.from = vm.relationsToDelete[i].relatedEntity;
// } // }
// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType)); // tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to));
// } // }
// $q.all(tasks).then(function(){ // if (tasks.length > 0) {
// vm.relations = vm.relations.concat(vm.newRelations); // return widgetContext.rxjs.forkJoin(tasks);
// vm.newRelations = []; // } else {
// vm.relationsToDelete = []; // return widgetContext.rxjs.of([]);
// updateAliasData();
// });
// }
//
// function updateAliasData() {
// var aliasIds = [];
// for (var id in widgetContext.aliasController.resolvedAliases) {
// aliasIds.push(id);
// } // }
// var tasks = [];
// aliasIds.forEach(function(aliasId) {
// widgetContext.aliasController.setAliasUnresolved(aliasId);
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
// });
// $q.all(tasks).then(function() {
// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
// });
// } // }
//} //}
// //
@ -314,124 +280,132 @@
/*========================= Add entity example =========================*/ /*========================= Add entity example =========================*/
/*========================================================================*/ /*========================================================================*/
// //
//var $injector = widgetContext.$scope.$injector; //let $injector = widgetContext.$scope.$injector;
//var $mdDialog = $injector.get('$mdDialog'), //let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
// $document = $injector.get('$document'), //let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
// $q = $injector.get('$q'), //let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
// $rootScope = $injector.get('$rootScope'), //let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
// types = $injector.get('types'), //let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
// assetService = $injector.get('assetService'),
// deviceService = $injector.get('deviceService'),
// attributeService = $injector.get('attributeService'),
// entityRelationService = $injector.get('entityRelationService');
// //
//openAddEntityDialog(); //openAddEntityDialog();
// //
//function openAddEntityDialog() { //function openAddEntityDialog() {
// $mdDialog.show({ // customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
// controller: ['$scope','$mdDialog', AddEntityDialogController],
// controllerAs: 'vm',
// template: htmlTemplate,
// locals: {
// entityId: entityId
// },
// parent: angular.element($document[0].body),
// targetEvent: $event,
// multiple: true,
// clickOutsideToClose: false
// });
//} //}
// //
//function AddEntityDialogController($scope, $mdDialog) { //function AddEntityDialogController(instance) {
// var vm = this; // let vm = instance;
// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; //
// vm.allowedRelatedEntityTypes = []; // vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
// vm.entitySearchDirection = types.entitySearchDirection; // vm.entitySearchDirection = {
// vm.attributes = {}; // from: "FROM",
// vm.relations = []; // to: "TO"
// }
//
// vm.addEntityFormGroup = vm.fb.group({
// entityName: ['', [vm.validators.required]],
// entityType: ['DEVICE'],
// entityLabel: [null],
// type: ['', [vm.validators.required]],
// attributes: vm.fb.group({
// latitude: [null],
// longitude: [null],
// address: [null],
// owner: [null],
// number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
// booleanValue: [null]
// }),
// relations: vm.fb.array([])
// });
//
// vm.cancel = function() {
// vm.dialogRef.close(null);
// };
//
// vm.relations = function() {
// return vm.addEntityFormGroup.get('relations');
// };
// //
// vm.addRelation = function() { // vm.addRelation = function() {
// var relation = { // vm.relations().push(vm.fb.group({
// direction: null, // relatedEntity: [null, [vm.validators.required]],
// relationType: null, // relationType: [null, [vm.validators.required]],
// relatedEntity: null // direction: [null, [vm.validators.required]]
// }; // }));
// vm.relations.push(relation);
// }; // };
//
// vm.removeRelation = function(index) { // vm.removeRelation = function(index) {
// if (index > -1) { // vm.relations().removeAt(index);
// vm.relations.splice(index, 1); // vm.relations().markAsDirty();
// }
// }; // };
//
// vm.save = function() { // vm.save = function() {
// $scope.addEntityForm.$setPristine(); // vm.addEntityFormGroup.markAsPristine();
// saveEntityPromise().then( // saveEntityObservable().subscribe(
// function (entity) { // function (entity) {
// saveAttributes(entity.id); // widgetContext.rxjs.forkJoin([
// saveRelations(entity.id); // saveAttributes(entity.id),
// $mdDialog.hide(); // saveRelations(entity.id)
// ]).subscribe(
// function () {
// widgetContext.updateAliases();
// vm.dialogRef.close(null);
// }
// );
// } // }
// ); // );
// }; // };
// vm.cancel = function() {
// $mdDialog.hide();
// };
// //
// // function saveEntityObservable() {
// function saveEntityPromise() { // const formValues = vm.addEntityFormGroup.value;
// var entity = { // let entity = {
// name: vm.entityName, // name: formValues.entityName,
// type: vm.type // type: formValues.type,
// label: formValues.entityLabel
// }; // };
// if (vm.entityType == types.entityType.asset) { // if (formValues.entityType == 'ASSET') {
// return assetService.saveAsset(entity); // return assetService.saveAsset(entity);
// } else if (vm.entityType == types.entityType.device) { // } else if (formValues.entityType == 'DEVICE') {
// return deviceService.saveDevice(entity); // return deviceService.saveDevice(entity);
// } // }
// } // }
// //
// function saveAttributes(entityId) { // function saveAttributes(entityId) {
// var attributesArray = []; // let attributes = vm.addEntityFormGroup.get('attributes').value;
// for (var key in vm.attributes) { // let attributesArray = [];
// attributesArray.push({key: key, value: vm.attributes[key]}); // for (let key in attributes) {
// if(attributes[key] !== null) {
// attributesArray.push({key: key, value: attributes[key]});
// }
// } // }
// if (attributesArray.length > 0) { // if (attributesArray.length > 0) {
// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); // return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
// } else {
// return widgetContext.rxjs.of([]);
// } // }
// } // }
// //
// function saveRelations(entityId) { // function saveRelations(entityId) {
// var tasks = []; // let relations = vm.addEntityFormGroup.get('relations').value;
// for (var i=0; i < vm.relations.length; i++) { // let tasks = [];
// var relation = { // for(let i=0; i < relations.length; i++) {
// type: vm.relations[i].relationType // let relation = {
// type: relations[i].relationType,
// typeGroup: 'COMMON'
// }; // };
// if (vm.relations[i].direction == types.entitySearchDirection.from) { // if (relations[i].direction == 'FROM') {
// relation.to = vm.relations[i].relatedEntity; // relation.to = relations[i].relatedEntity;
// relation.from = entityId; // relation.from = entityId;
// } else { // } else {
// relation.to = entityId; // relation.to = entityId;
// relation.from = vm.relations[i].relatedEntity; // relation.from = relations[i].relatedEntity;
// } // }
// tasks.push(entityRelationService.saveRelation(relation)); // tasks.push(entityRelationService.saveRelation(relation));
// } // }
// $q.all(tasks).then(function(){ // if (tasks.length > 0) {
// updateAliasData(); // return widgetContext.rxjs.forkJoin(tasks);
// }); // } else {
// } // return widgetContext.rxjs.of([]);
//
// function updateAliasData() {
// var aliasIds = [];
// for (var id in widgetContext.aliasController.resolvedAliases) {
// aliasIds.push(id);
// } // }
// var tasks = [];
// aliasIds.forEach(function(aliasId) {
// widgetContext.aliasController.setAliasUnresolved(aliasId);
// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
// });
// $q.all(tasks).then(function() {
// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
// });
// } // }
//} //}

262
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html

@ -0,0 +1,262 @@
<!--
Copyright © 2016-2020 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.
-->
<form #formContainer class="gateway-form"
[formGroup]="gatewayConfigurationGroup"
tb-toast toastTarget="{{ toastTargetId }}"
(ngSubmit)="save()">
<mat-accordion multi="true" class="mat-body-1">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.thingsboard' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-entity-gateway-select
formControlName="gateway"
[newGatewayType]="gatewayType"
(gatewayNameExist)="gatewayExist()"
required
>
</tb-entity-gateway-select>
<div fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{'gateway.security-type' | translate }}</mat-label>
<mat-select formControlName="securityType" >
<mat-option *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key">
{{ securityType.value.toString() | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.thingsboard-host' | translate }}</mat-label>
<input matInput type="text" formControlName="host">
<mat-error *ngIf="gatewayConfigurationGroup.get('host').hasError('required')" translate>
gateway.thingsboard-host-required
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.thingsboard-port' | translate }}</mat-label>
<input matInput type="number" formControlName="port">
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('required')" translate>
gateway.thingsboard-port-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('min')" translate>
gateway.thingsboard-port-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('max')" translate>
gateway.thingsboard-port-max
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('pattern')" translate>
gateway.thingsboard-port-pattern
</mat-error>
</mat-form-field>
</div>
<div *ngIf="gatewayConfigurationGroup.get('securityType').value == 'tls'" fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-ca-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="caCertPath">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-private-key' | translate }}</mat-label>
<input matInput type="text" formControlName="privateKeyPath">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-client-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="certPath">
</mat-form-field>
</div>
<mat-checkbox formControlName="remoteConfiguration">{{ 'gateway.remote' | translate }}</mat-checkbox>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{'gateway.remote-logging-level' | translate }}</mat-label>
<mat-select formControlName="remoteLoggingLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevels" [value]="logLevel">
{{ logLevel }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.path-logs' | translate }}</mat-label>
<input matInput type="text" formControlName="remoteLoggingPathToLogs">
<mat-error *ngIf="gatewayConfigurationGroup.get('remoteLoggingPathToLogs').hasError('required')" translate>
gateway.path-logs-required
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.storage' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{'gateway.storage-type' | translate }}</mat-label>
<mat-select formControlName="storageType">
<mat-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value.toString() | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-pack-size' | translate }}</mat-label>
<input matInput type="number" formControlName="readRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('required')" translate>
gateway.storage-pack-size-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('min')" translate>
gateway.storage-pack-size-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('pattern')" translate>
gateway.storage-pack-size-pattern
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label >
{{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}}
</mat-label>
<input matInput type="number" formControlName="maxRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('required')" translate>
gateway.storage-max-records-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('min')" translate>
gateway.storage-max-records-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('pattern')" translate>
gateway.storage-max-records-pattern
</mat-error>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" *ngIf="gatewayConfigurationGroup.get('storageType').value == 'file'">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-max-files' | translate }}</mat-label>
<input matInput type="number" formControlName="maxFilesCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('required')" translate>
gateway.storage-max-files-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('min')" translate>
gateway.storage-max-files-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('pattern')" translate>
gateway.storage-max-files-pattern
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-path' | translate }}</mat-label>
<input matInput type="text" formControlName="dataFolderPath">
<mat-error *ngIf="gatewayConfigurationGroup.get('dataFolderPath').hasError('required')" translate>
gateway.storage-path-required
</mat-error>
</mat-form-field>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column" class="gateway-config">
<section formArrayName="connectors" *ngFor="let connector of connectors.controls; let i = index;">
<div [formGroupName]="i" fxLayout="row" fxLayoutAlign="space-between stretch" fxLayoutGap="8px">
<div fxLayout="column" fxLayoutAlign="center start">
<mat-slide-toggle formControlName="enabled"></mat-slide-toggle>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" fxFlex>
<mat-form-field fxFlex>
<mat-label>{{'gateway.connector-type' | translate }}</mat-label>
<mat-select formControlName="configType" (selectionChange)="changeConnectorType(connector)">
<mat-option *ngFor="let connectorType of connectorTypes" [value]="connectorType">
{{ connectorType }}
</mat-option>
</mat-select>
<mat-error *ngIf="connector.get('configType').hasError('required')" translate>
gateway.connector-type-required
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.connector-name' | translate }}</mat-label>
<input matInput type="text" formControlName="name" (blur)="changeConnectorName(connector, i)">
<mat-error *ngIf="connector.get('name').hasError('required')" translate>
gateway.connector-name-required
</mat-error>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"
fxLayoutAlign="{{alignment == 'row' ? 'end center' : 'space-evenly center'}}" class="action-buttons">
<button mat-icon-button (click)="openConfigDialog($event, i, connector.get('config').value, connector.get('name').value)"
matTooltip="{{ 'gateway.update-config' | translate }}"
matTooltipPosition="above"
[ngClass]="{'mat-warn': connector.get('config').invalid}">
<mat-icon>more_horiz</mat-icon>
</button>
<button mat-icon-button (click)="removeConnector(i)"
matTooltip="{{ 'gateway.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</section>
<span [fxShow]="!connectors.length" fxLayoutAlign="center center" class="no-data-found">{{'gateway.no-connectors' | translate}}</span>
<div>
<button mat-raised-button type="button" (click)="addNewConnector()"
matTooltip="{{ 'gateway.connector-add' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
</div>
</div >
</mat-expansion-panel>
</mat-accordion>
<section fxLayout="row" fxLayoutAlign="end center" class="form-action-buttons">
<button mat-raised-button color="primary" type="button"
(click)="exportConfig()"
*ngIf="!gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.download-tip' | translate }}">
{{'action.download' | translate }}
</button>
<button mat-raised-button color="primary" type="submit"
*ngIf="gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.save-tip' | translate }}">
{{'action.save' | translate }}
</button>
</section>
</form>

41
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 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{
.gateway-form {
height: 100%;
padding: 5px;
background-color: transparent;
overflow-y: auto;
overflow-x: hidden;
.form-action-buttons{
padding-top: 8px;
}
.gateway-config {
.no-data-found {
position: relative;
display: flex;
height: 40px;
text-transform: uppercase;
}
}
}
::ng-deep .mat-form-field-infix {
width: auto !important;
}
}

408
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts

@ -0,0 +1,408 @@
///
/// Copyright © 2016-2020 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, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormArray, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service';
import {
CONFIGURATION_ATTRIBUTE,
CONFIGURATION_DRAFT_ATTRIBUTE,
ConnectorType,
createFormConfig,
CURRENT_CONFIGURATION_ATTRIBUTE,
DEFAULT_CONNECTOR,
gatewayConfigJSON,
GatewayFormConnectorModel,
GatewayFormModels,
GatewayLogLevel,
generateConnectorConfigFiles,
generateLogConfigFile,
generateYAMLConfigFile,
getDraftConnectorsJSON,
getEntityId,
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
SecurityType,
SecurityTypeTranslationMap,
StorageType,
StorageTypeTranslationMap,
ValidateJSON,
WidgetSetting
} from './gateway-form.models';
import { WINDOW } from '@core/services/window.service';
import { MatDialog } from '@angular/material/dialog';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { DeviceService } from '@core/http/device.service';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { forkJoin, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ImportExportService } from '@home/components/import-export/import-export.service';
// @dynamic
@Component({
selector: 'tb-gateway-form',
templateUrl: './gateway-form.component.html',
styleUrls: ['./gateway-form.component.scss']
})
export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy {
constructor(
protected store: Store<AppState>,
private elementRef: ElementRef,
private utils: UtilsService,
private ngZone: NgZone,
private fb: FormBuilder,
@Inject(WINDOW) private window: Window,
private dialog: MatDialog,
private translate: TranslateService,
private deviceService: DeviceService,
private attributeService: AttributeService,
private importExport: ImportExportService
) {
super(store);
}
get connectors(): FormArray {
return this.gatewayConfigurationGroup.get('connectors') as FormArray;
}
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>;
@ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm;
private successfulSaved: string;
private gatewayNameExists: string;
private archiveFileName: string;
private formResizeListener: any;
private subscribeStorageType$: any;
private subscribeGateway$: any;
alignment = 'row';
layoutGap = '5px';
gatewayType: string;
gatewayConfigurationGroup: FormGroup;
securityTypes = SecurityTypeTranslationMap;
gatewayLogLevels = Object.values(GatewayLogLevel);
connectorTypes = Object.keys(ConnectorType);
storageTypes = StorageTypeTranslationMap;
toastTargetId = 'gateway-configuration-widget' + this.utils.guid();
@Input()
ctx: WidgetContext;
ngOnInit(): void {
this.initWidgetSettings(this.ctx.settings);
this.buildForm();
this.ctx.updateWidgetParams();
this.formResizeListener = this.resize.bind(this);
// @ts-ignore
addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
ngOnDestroy(): void {
if (this.formResizeListener) {
// @ts-ignore
removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
this.subscribeGateway$.unsubscribe();
this.subscribeStorageType$.unsubscribe();
}
private initWidgetSettings(settings: WidgetSetting): void {
let widgetTitle;
if (settings.widgetTitle && settings.widgetTitle.length) {
widgetTitle = this.utils.customTranslation(settings.widgetTitle, settings.widgetTitle);
} else {
widgetTitle = this.translate.instant('gateway.gateway');
}
this.ctx.widgetTitle = widgetTitle;
this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration';
this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway';
this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists');
this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved');
this.updateWidgetDisplaying();
}
private resize(): void {
this.ngZone.run(() => {
this.updateWidgetDisplaying();
this.ctx.detectChanges();
});
}
private updateWidgetDisplaying(): void {
if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){
this.layoutGap = '0';
this.alignment = 'column';
} else {
this.layoutGap = '5px';
this.alignment = 'row';
}
}
private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable<any> {
const gatewayId = this.gatewayConfigurationGroup.get('gateway').value;
const attributes: AttributeData = {
key: attributeName,
value: attributeValue
};
return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]);
}
private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void {
this.connectors.push(this.fb.group({
enabled: [setting.enabled],
configType: [setting.configType, [Validators.required]],
name: [setting.name, [Validators.required]],
config: [setting.config, [Validators.nullValidator, ValidateJSON]]
}));
}
private getFormField(name: string): AbstractControl {
return this.gatewayConfigurationGroup.get(name);
}
private buildForm(): void {
this.gatewayConfigurationGroup = this.fb.group({
gateway: [null],
accessToken: [null, [Validators.required]],
securityType: [SecurityType.accessToken],
host: [this.window.location.hostname, [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteConfiguration: [true],
caCertPath: ['/etc/thingsboard-gateway/ca.pem'],
privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'],
certPath: ['/etc/thingsboard-gateway/certificate.pem'],
remoteLoggingLevel: [GatewayLogLevel.debug],
remoteLoggingPathToLogs: ['./logs/', [Validators.required]],
storageType: [StorageType.memory],
readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
dataFolderPath: ['./data/', [Validators.required]],
connectors: this.fb.array([])
});
this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => {
if (value === StorageType.memory) {
this.getFormField('maxFilesCount').disable();
this.getFormField('dataFolderPath').disable();
} else {
this.getFormField('maxFilesCount').enable();
this.getFormField('dataFolderPath').enable();
}
});
this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => {
if (gatewayId !== null) {
forkJoin([
this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => {
this.getFormField('accessToken').patchValue(deviceCredential.credentialsId);
})),
...this.getAttributes(gatewayId)]).subscribe(() => {
this.gatewayConfigurationGroup.markAsPristine();
this.ctx.detectChanges();
});
} else {
this.getFormField('accessToken').patchValue('');
}
})
}
gatewayExist(): void {
this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId);
}
exportConfig(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
const filesZip: any = {};
filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration);
generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors);
generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs);
this.importExport.exportJSZip(filesZip, this.archiveFileName);
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE);
}
addNewConnector(): void {
this.createConnector();
}
removeConnector(index: number): void {
if (index > -1) {
this.connectors.removeAt(index);
this.connectors.markAsDirty();
}
}
openConfigDialog($event: Event, index: number, config: object, type: string): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: config,
title: this.translate.instant('gateway.title-connectors-json', {typeName: type})
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.connectors.at(index).get('config').patchValue(res);
this.ctx.detectChanges();
}
}
);
}
private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string {
const newKeyName = index ? name + index : name;
const indexRes = connectors.findIndex((element) => element.name === newKeyName);
return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index);
}
private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string {
for (let i = 0; i < connectors.length; i++) {
const nameEq = (index === 0) ? name : name + index;
if (i !== connectorIndex && connectors[i].name === nameEq) {
this.validateConnectorName(connectors, name, connectorIndex, ++index);
}
}
return (index === 0) ? name : name + index;
}
changeConnectorType(connector: AbstractControl): void {
if (!connector.get('name').value) {
const typeConnector = connector.get('configType').value;
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector]));
}
}
changeConnectorName(connector: AbstractControl, index: number): void {
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index));
}
save(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
forkJoin([
this.saveAttribute(
CONFIGURATION_ATTRIBUTE,
window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))),
AttributeScope.SHARED_SCOPE),
this.saveAttribute(
CONFIGURATION_DRAFT_ATTRIBUTE,
window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))),
AttributeScope.SERVER_SCOPE),
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE)
]).subscribe(() =>{
this.ctx.showSuccessToast(this.successfulSaved,
2000, 'top', 'left', this.toastTargetId);
this.gatewayConfigurationGroup.markAsPristine();
})
}
private getAttributes(gatewayId: string): Array<Observable<Array<AttributeData>>> {
const tasks = [];
tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId),
this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe(
tap(([currentConfig, draftConfig]) => {
this.setFormGatewaySettings(currentConfig);
this.setFormConnectorsDraft(draftConfig);
})
)
);
tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe(
tap(logsLevel => this.processLoggingLevel(logsLevel))
));
return tasks;
}
private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable<Array<AttributeData>> {
return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]);
}
private setFormGatewaySettings(response: Array<AttributeData>): void {
this.connectors.clear();
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const attributeKey of Object.keys(attribute)) {
const keyValue = attribute[attributeKey];
if (attributeKey === 'thingsboard') {
if (keyValue !== null && Object.keys(keyValue).length > 0) {
this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue));
}
} else {
for (const connector of Object.keys(keyValue)) {
let name = 'No name';
if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) {
name = keyValue[connector].name;
}
const newConnector: GatewayFormConnectorModel = {
enabled: true,
configType: (attributeKey as ConnectorType),
config: keyValue[connector].config,
name
};
this.createConnector(newConnector);
}
}
}
}
}
private setFormConnectorsDraft(response: Array<AttributeData>): void {
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const connectorName of Object.keys(attribute)) {
const newConnector: GatewayFormConnectorModel = {
enabled: false,
configType: (attribute[connectorName].connector as ConnectorType),
config: attribute[connectorName].config,
name: connectorName
};
this.createConnector(newConnector);
}
}
}
private processLoggingLevel(value: Array<AttributeData>): void {
let logsLevel = GatewayLogLevel.debug;
if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) {
logsLevel = GatewayLogLevel[value[0].value.toLowerCase()];
}
this.getFormField('remoteLoggingLevel').patchValue(logsLevel);
}
}

374
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts

@ -0,0 +1,374 @@
///
/// Copyright © 2016-2020 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 { AbstractControl, ValidationErrors } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export enum SecurityType {
tls = 'tls',
accessToken = 'accessToken'
}
export interface WidgetSetting {
widgetTitle: string;
archiveFileName: string;
gatewayType: string;
successfulSave: string;
gatewayNameExists: string;
}
export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration';
export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts';
export const CONFIGURATION_ATTRIBUTE = 'configuration';
export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel';
export const SecurityTypeTranslationMap = new Map<SecurityType, string>(
[
[SecurityType.tls, 'gateway.security-types.tls'],
[SecurityType.accessToken, 'gateway.security-types.access-token']
]
);
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum StorageType {
memory = 'memory',
file = 'file'
}
export const StorageTypeTranslationMap = new Map<StorageType, string>(
[
[StorageType.memory, 'gateway.storage-types.memory-storage'],
[StorageType.file, 'gateway.storage-types.file-storage']
]
);
export enum ConnectorType {
mqtt= 'MQTT',
modbus = 'Modbus',
opc_ua = 'OPC-UA',
ble = 'BLE'
}
export interface GatewayFormModels {
gateway?: string;
accessToken?: string;
securityType?: SecurityType;
host?: string;
port?: number;
remoteConfiguration?: boolean;
caCertPath?: string;
privateKeyPath?: string;
certPath?: string;
remoteLoggingLevel?: GatewayLogLevel;
remoteLoggingPathToLogs?:string;
storageType?: StorageType;
readRecordsCount?: number;
maxRecordsCount?: number;
maxFilesCount?: number;
dataFolderPath?: number;
connectors?: Array<GatewayFormConnectorModel>;
}
export interface GatewayFormConnectorModel {
config: object;
name: string;
configType: ConnectorType;
enabled: boolean;
}
export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = {
config: {},
name: '',
configType: null,
enabled: false
};
type Connector = {
[key in ConnectorType]?: Array<ConnectorConfig>;
}
interface GatewaySetting extends Connector{
thingsboard: GatewayMainSetting;
}
interface ConnectorConfig {
name: string;
config: object;
}
interface GatewayMainSetting {
thingsboard: GatewayMainThingsboardSetting;
connectors: Array<GatewayMainConnector>,
logs: string,
storage: GatewayStorage
}
interface GatewayMainThingsboardSetting {
host: string,
remoteConfiguration: boolean,
port: number,
security: GatewaySecurity
}
type GatewaySecurity = SecurityToken | SecurityCertificate;
interface SecurityToken {
accessToken: string;
}
interface SecurityCertificate {
caCert: string;
privateKey: string;
cert: string;
}
type GatewayStorage = GatewayStorageMemory | GatewayStorageFile;
interface GatewayStorageMemory {
type: string;
max_records_count: number;
read_records_count: number;
}
interface GatewayStorageFile {
type: string;
data_folder_path: number;
max_file_count: number;
max_read_records_count: number;
max_records_per_file: number;
}
interface GatewayMainConnector {
configuration: string;
name: string;
type: ConnectorType;
}
export function ValidateJSON(control: AbstractControl): ValidationErrors | null {
if (JSON.stringify(control.value) === JSON.stringify({})) {
return { validJSON: true };
}
return null;
}
const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"';
export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string {
let config;
config = 'thingsboard:\n';
config += ' host: ' + gatewaySetting.host + '\n';
config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n';
config += ' port: ' + gatewaySetting.port + '\n';
config += ' security:\n';
if (gatewaySetting.securityType === SecurityType.accessToken) {
config += ' access-token: ' + gatewaySetting.accessToken + '\n';
} else {
config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n';
config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n';
config += ' cert: ' + gatewaySetting.certPath + '\n';
}
config += 'storage:\n';
if (gatewaySetting.storageType === StorageType.memory) {
config += ' type: memory\n';
config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n';
} else {
config += ' type: file\n';
config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n';
config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n';
config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n';
}
config += 'connectors:\n';
for(const connector of gatewaySetting.connectors){
if (connector.enabled) {
config += ' -\n';
config += ' name: ' + connector.name + '\n';
config += ' type: ' + connector.configType + '\n';
config += ' configuration: ' + generateFileName(connector.name) + '\n';
}
}
return config;
}
export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array<GatewayFormConnectorModel>): void {
for(const connector of connectors) {
if (connector.enabled) {
fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config);
}
}
}
function generateFileName(fileName): string {
return fileName.replace('_', '')
.replace('-', '')
.replace(/^\s+|\s+/g, '')
.toLowerCase() + '.json';
}
export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void {
fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath);
}
function getLogsConfig(logsLevel: string, logsPath: string): string {
return TEMPLATE_LOGS_CONFIG
.replace(/{ERROR}/g, logsLevel)
.replace(/{.\/logs\/}/g, logsPath);
}
export function getEntityId(gatewayId: string): EntityId {
return {
id: gatewayId,
entityType: EntityType.DEVICE
}
}
export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels {
const formSetting: GatewayFormModels = {};
if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) {
formSetting.host = keyValue.thingsboard.host;
formSetting.port = keyValue.thingsboard.port;
formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration;
if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) {
formSetting.securityType = SecurityType.accessToken;
formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken;
} else {
formSetting.securityType = SecurityType.tls;
formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert;
formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey;
formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert;
}
}
if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) {
if (keyValue.storage.type === StorageType.memory) {
formSetting.storageType = StorageType.memory;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
} else if (keyValue.storage.type === StorageType.file) {
formSetting.storageType = StorageType.file;
formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path;
formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
}
}
return formSetting;
}
export function getDraftConnectorsJSON(currentConnectors: Array<GatewayFormConnectorModel>) {
const draftConnectors = {};
for(const connector of currentConnectors){
if (!connector.enabled) {
draftConnectors[connector.name] = {
connector: connector.configType,
config: connector.config
};
}
}
return draftConnectors;
}
export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting {
const gatewayConfig = {
thingsboard: gatewayMainConfigJSON(gatewayConfiguration)
};
gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors);
return gatewayConfig;
}
function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting {
let security: GatewaySecurity;
if (gatewayConfiguration.securityType === SecurityType.accessToken) {
security = {
accessToken: gatewayConfiguration.accessToken
}
} else {
security = {
caCert: gatewayConfiguration.caCertPath,
privateKey: gatewayConfiguration.privateKeyPath,
cert: gatewayConfiguration.certPath
}
}
const thingsboard: GatewayMainThingsboardSetting = {
host: gatewayConfiguration.host,
remoteConfiguration: gatewayConfiguration.remoteConfiguration,
port: gatewayConfiguration.port,
security
};
let storage: GatewayStorage;
if (gatewayConfiguration.storageType === StorageType.memory) {
storage = {
type: StorageType.memory,
read_records_count: gatewayConfiguration.readRecordsCount,
max_records_count: gatewayConfiguration.maxRecordsCount
};
} else {
storage = {
type: StorageType.file,
data_folder_path: gatewayConfiguration.dataFolderPath,
max_file_count: gatewayConfiguration.maxFilesCount,
max_read_records_count: gatewayConfiguration.readRecordsCount,
max_records_per_file: gatewayConfiguration.maxRecordsCount
};
}
const connectors: Array<GatewayMainConnector> = [];
for (const connector of gatewayConfiguration.connectors) {
if (connector.enabled) {
const connectorConfig: GatewayMainConnector = {
configuration: generateFileName(connector.name),
name: connector.name,
type: connector.configType
};
connectors.push(connectorConfig);
}
}
return {
thingsboard,
connectors,
storage,
logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs))
}
}
function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array<GatewayFormConnectorModel>): void {
for (const connector of currentConnectors) {
if (connector.enabled) {
const typeConnector = connector.configType;
if (!Array.isArray(gatewayConfiguration[typeConnector])) {
gatewayConfiguration[typeConnector] = [];
}
const connectorConfig: ConnectorConfig = {
name: connector.name,
config: connector.config
};
gatewayConfiguration[typeConnector].push(connectorConfig);
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -170,8 +170,8 @@ export default abstract class LeafletMap {
this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
this.map.panTo(bounds.getCenter(), { animate: false }); this.map.panTo(bounds.getCenter(), { animate: false });
} else { } else {
this.map.once('zoomend', function () { this.map.once('zoomend', () => {
if (!this.options.defaultZoomLevel && this.options.map.getZoom() > this.options.minZoomLevel) { if (!this.options.defaultZoomLevel && this.map.getZoom() > this.options.minZoomLevel) {
this.map.setZoom(this.options.minZoomLevel, { animate: false }); this.map.setZoom(this.options.minZoomLevel, { animate: false });
} }
}); });
@ -323,4 +323,4 @@ export default abstract class LeafletMap {
this.polygons.get(key).updatePolygon(data, dataSources, settings); this.polygons.get(key).updatePolygon(data, dataSources, settings);
}); });
} }
} }

17
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -39,10 +39,9 @@ import { JsonSettingsSchema } from '@shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { Type } from '@angular/core';
let providerSets; // @dynamic
let defaultSettings;
export class MapWidgetController implements MapWidgetInterface { export class MapWidgetController implements MapWidgetInterface {
constructor(public mapProvider: MapProviders, private drawRoutes: boolean, public ctx: WidgetContext, $element: HTMLElement) { constructor(public mapProvider: MapProviders, private drawRoutes: boolean, public ctx: WidgetContext, $element: HTMLElement) {
@ -141,7 +140,7 @@ export class MapWidgetController implements MapWidgetInterface {
}); });
} }
initSettings(settings: UnitedMapSettings) { initSettings(settings: UnitedMapSettings): UnitedMapSettings {
const functionParams = ['data', 'dsData', 'dsIndex']; const functionParams = ['data', 'dsData', 'dsIndex'];
this.provider = settings.provider || this.mapProvider; this.provider = settings.provider || this.mapProvider;
const customOptions = { const customOptions = {
@ -190,12 +189,12 @@ export class MapWidgetController implements MapWidgetInterface {
export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController;
interface IProvider { interface IProvider {
MapClass: LeafletMap, MapClass: Type<LeafletMap>,
schema: JsonSettingsSchema, schema: JsonSettingsSchema,
name: string name: string
} }
providerSets = { export const providerSets: {[key: string]: IProvider} = {
'openstreet-map': { 'openstreet-map': {
MapClass: OpenStreetMap, MapClass: OpenStreetMap,
schema: openstreetMapSettingsSchema, schema: openstreetMapSettingsSchema,
@ -221,9 +220,9 @@ providerSets = {
schema: imageMapSettingsSchema, schema: imageMapSettingsSchema,
name: 'image-map' name: 'image-map'
} }
} };
defaultSettings = { export const defaultSettings: any = {
xPosKeyName: 'xPos', xPosKeyName: 'xPos',
yPosKeyName: 'yPos', yPosKeyName: 'yPos',
markerOffsetX: 0.5, markerOffsetX: 0.5,
@ -257,4 +256,4 @@ defaultSettings = {
credentials: '', credentials: '',
markerClusteringSetting: null, markerClusteringSetting: null,
draggableMarker: false draggableMarker: false
} };

1
ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts

@ -46,6 +46,7 @@ interface WebCameraInputWidgetSettings {
maxHeight: number; maxHeight: number;
} }
// @dynamic
@Component({ @Component({
selector: 'tb-web-camera-widget', selector: 'tb-web-camera-widget',
templateUrl: './web-camera-input.component.html', templateUrl: './web-camera-input.component.html',

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

@ -33,6 +33,8 @@ import {
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component'; import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
import { TripAnimationComponent } from './trip-animation/trip-animation.component'; import { TripAnimationComponent } from './trip-animation/trip-animation.component';
import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component'; import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@NgModule({ @NgModule({
declarations: declarations:
@ -47,7 +49,8 @@ import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component'
DateRangeNavigatorPanelComponent, DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent, MultipleInputWidgetComponent,
TripAnimationComponent, TripAnimationComponent,
WebCameraInputWidgetComponent WebCameraInputWidgetComponent,
GatewayFormComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -64,10 +67,12 @@ import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component'
DateRangeNavigatorWidgetComponent, DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent, MultipleInputWidgetComponent,
TripAnimationComponent, TripAnimationComponent,
WebCameraInputWidgetComponent WebCameraInputWidgetComponent,
GatewayFormComponent
], ],
providers: [ providers: [
CustomDialogService CustomDialogService,
ImportExportService
] ]
}) })
export class WidgetComponentsModule { } export class WidgetComponentsModule { }

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

@ -29,7 +29,6 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
Type,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
@ -44,7 +43,8 @@ import {
Widget, Widget,
WidgetActionDescriptor, WidgetActionDescriptor,
widgetActionSources, widgetActionSources,
WidgetActionType, WidgetComparisonSettings, WidgetActionType,
WidgetComparisonSettings,
WidgetResource, WidgetResource,
widgetType, widgetType,
WidgetTypeParameters WidgetTypeParameters
@ -90,24 +90,7 @@ import { DashboardService } from '@core/http/dashboard.service';
import { DatasourceService } from '@core/api/datasource.service'; import { DatasourceService } from '@core/api/datasource.service';
import { WidgetSubscription } from '@core/api/widget-subscription'; import { WidgetSubscription } from '@core/api/widget-subscription';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AssetService } from '@core/http/asset.service'; import { ServicesMap } from '@home/models/services.map';
import { DialogService } from '@core/services/dialog.service';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { DatePipe } from '@angular/common';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
const ServicesMap = new Map<string, Type<any>>();
ServicesMap.set('deviceService', DeviceService);
ServicesMap.set('assetService', AssetService);
ServicesMap.set('attributeService', AttributeService);
ServicesMap.set('dialogs', DialogService);
ServicesMap.set('customDialog', CustomDialogService);
ServicesMap.set('date', DatePipe);
ServicesMap.set('utils', UtilsService);
ServicesMap.set('translate', TranslateService);
ServicesMap.set('http', HttpClient);
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -706,6 +689,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
this.cd.detectChanges(); this.cd.detectChanges();
} catch (e) { } catch (e) {
console.error(e);
if (this.dynamicWidgetComponentRef) { if (this.dynamicWidgetComponentRef) {
this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef.destroy();
this.dynamicWidgetComponentRef = null; this.dynamicWidgetComponentRef = null;
@ -1010,7 +994,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
'entityName', 'additionalParams', 'entityLabel', customFunction); 'entityName', 'additionalParams', 'entityLabel', customFunction);
customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
} catch (e) { } catch (e) {
// console.error(e);
} }
} }
break; break;
@ -1035,7 +1019,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction);
customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel);
} catch (e) { } catch (e) {
// console.error(e);
} }
} }
}, },

8
ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html

@ -16,7 +16,7 @@
--> -->
<form #addEntitiesToCustomerForm="ngForm" [formGroup]="addEntitiesToCustomerFormGroup" (ngSubmit)="assign()"> <form #addEntitiesToCustomerForm="ngForm" [formGroup]="addEntitiesToCustomerFormGroup" (ngSubmit)="assign()">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2>{{ assignToCustomerTitle | translate }}</h2> <h2>{{ assignToCustomerTitle | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-button mat-icon-button
@ -39,16 +39,14 @@
</tb-entity-list> </tb-entity-list>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayoutAlign="end center">
<span fxFlex></span> <button mat-raised-button color="primary"
<button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || addEntitiesToCustomerForm.invalid [disabled]="(isLoading$ | async) || addEntitiesToCustomerForm.invalid
|| !addEntitiesToCustomerForm.dirty"> || !addEntitiesToCustomerForm.dirty">
{{ 'action.assign' | translate }} {{ 'action.assign' | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>

21
ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts

@ -14,19 +14,18 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {Component, Inject, OnInit, SkipSelf} from '@angular/core'; import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {PageComponent} from '@shared/components/page.component'; import { Store } from '@ngrx/store';
import {Store} from '@ngrx/store'; import { AppState } from '@core/core.state';
import {AppState} from '@core/core.state'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms'; import { DeviceService } from '@core/http/device.service';
import {DeviceService} from '@core/http/device.service'; import { EntityType } from '@shared/models/entity-type.models';
import {EntityType} from '@shared/models/entity-type.models'; import { forkJoin, Observable } from 'rxjs';
import {forkJoin, Observable} from 'rxjs'; import { AssetService } from '@core/http/asset.service';
import {AssetService} from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service';
import {EntityViewService} from '@core/http/entity-view.service'; import { DashboardService } from '@core/http/dashboard.service';
import {DashboardService} from '@core/http/dashboard.service';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';

10
ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html

@ -16,7 +16,7 @@
--> -->
<form #assignToCustomerForm="ngForm" [formGroup]="assignToCustomerFormGroup" (ngSubmit)="assign()"> <form #assignToCustomerForm="ngForm" [formGroup]="assignToCustomerFormGroup" (ngSubmit)="assign()">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar color="primary">
<h2>{{ assignToCustomerTitle | translate }}</h2> <h2>{{ assignToCustomerTitle | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-button mat-icon-button <button mat-button mat-icon-button
@ -30,7 +30,7 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<span>{{ assignToCustomerText | translate }}</span> <div style="padding-bottom: 0.5em;">{{ assignToCustomerText | translate }}</div>
<tb-entity-autocomplete <tb-entity-autocomplete
formControlName="customerId" formControlName="customerId"
required required
@ -38,16 +38,14 @@
</tb-entity-autocomplete> </tb-entity-autocomplete>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<span fxFlex></span> <button mat-raised-button color="primary"
<button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || assignToCustomerForm.invalid [disabled]="(isLoading$ | async) || assignToCustomerForm.invalid
|| !assignToCustomerForm.dirty"> || !assignToCustomerForm.dirty">
{{ 'action.assign' | translate }} {{ 'action.assign' | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"
style="margin-right: 20px;"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>

21
ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts

@ -14,19 +14,18 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {Component, Inject, OnInit, SkipSelf} from '@angular/core'; import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {PageComponent} from '@shared/components/page.component'; import { Store } from '@ngrx/store';
import {Store} from '@ngrx/store'; import { AppState } from '@core/core.state';
import {AppState} from '@core/core.state'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms'; import { DeviceService } from '@core/http/device.service';
import {DeviceService} from '@core/http/device.service'; import { EntityId } from '@shared/models/id/entity-id';
import {EntityId} from '@shared/models/id/entity-id'; import { EntityType } from '@shared/models/entity-type.models';
import {EntityType} from '@shared/models/entity-type.models'; import { forkJoin, Observable } from 'rxjs';
import {forkJoin, Observable} from 'rxjs'; import { AssetService } from '@core/http/asset.service';
import {AssetService} from '@core/http/asset.service'; import { EntityViewService } from '@core/http/entity-view.service';
import {EntityViewService} from '@core/http/entity-view.service';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';

4
ui-ngx/src/app/modules/home/dialogs/home-dialogs.module.ts

@ -17,8 +17,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import {AssignToCustomerDialogComponent} from '@modules/home/dialogs/assign-to-customer-dialog.component'; import { AssignToCustomerDialogComponent } from '@modules/home/dialogs/assign-to-customer-dialog.component';
import {AddEntitiesToCustomerDialogComponent} from '@modules/home/dialogs/add-entities-to-customer-dialog.component'; import { AddEntitiesToCustomerDialogComponent } from '@modules/home/dialogs/add-entities-to-customer-dialog.component';
import { HomeDialogsService } from './home-dialogs.service'; import { HomeDialogsService } from './home-dialogs.service';
@NgModule({ @NgModule({

3
ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts

@ -15,8 +15,6 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -38,7 +36,6 @@ export class HomeDialogsService {
return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file'); return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file');
case EntityType.ASSET: case EntityType.ASSET:
return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file'); return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file');
break;
} }
} }

11
ui-ngx/src/app/modules/home/home.component.html

@ -35,17 +35,17 @@
<mat-sidenav-content> <mat-sidenav-content>
<div fxLayout="column" role="main" style="height: 100%;"> <div fxLayout="column" role="main" style="height: 100%;">
<mat-toolbar fxLayout="row" color="primary" class="mat-elevation-z1 tb-primary-toolbar"> <mat-toolbar fxLayout="row" color="primary" class="mat-elevation-z1 tb-primary-toolbar">
<button [fxShow]="!forceFullscreen" mat-button mat-icon-button id="main" <button [fxShow]="!forceFullscreen" mat-icon-button id="main"
[ngClass]="{'tb-invisible': displaySearchMode()}" [ngClass]="{'tb-invisible': displaySearchMode()}"
fxHide.gt-sm (click)="sidenav.toggle()"> fxHide.gt-sm (click)="sidenav.toggle()">
<mat-icon class="material-icons">menu</mat-icon> <mat-icon class="material-icons">menu</mat-icon>
</button> </button>
<button [fxShow]="forceFullscreen" mat-button mat-icon-button <button [fxShow]="forceFullscreen" mat-icon-button
[ngClass]="{'tb-invisible': displaySearchMode()}" [ngClass]="{'tb-invisible': displaySearchMode()}"
(click)="goBack()"> (click)="goBack()">
<mat-icon class="material-icons">arrow_back</mat-icon> <mat-icon class="material-icons">arrow_back</mat-icon>
</button> </button>
<button mat-button mat-icon-button <button mat-icon-button
[ngClass]="{'tb-invisible': !displaySearchMode()}" [ngClass]="{'tb-invisible': !displaySearchMode()}"
(click)="closeSearch()"> (click)="closeSearch()">
<mat-icon class="material-icons">arrow_back</mat-icon> <mat-icon class="material-icons">arrow_back</mat-icon>
@ -62,12 +62,11 @@
</mat-form-field> </mat-form-field>
</div> </div>
<button [fxShow]="searchEnabled" <button [fxShow]="searchEnabled"
mat-button mat-icon-button mat-icon-button (click)="openSearch()">
(click)="openSearch()">
<mat-icon class="material-icons">search</mat-icon> <mat-icon class="material-icons">search</mat-icon>
</button> </button>
<button *ngIf="fullscreenEnabled" [fxShow]="!displaySearchMode()" <button *ngIf="fullscreenEnabled" [fxShow]="!displaySearchMode()"
mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()"> mat-icon-button fxHide.lt-md (click)="toggleFullscreen()">
<mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> <mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button> </button>
<tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu> <tb-user-menu [displayUserInfo]="!displaySearchMode()"></tb-user-menu>

12
ui-ngx/src/app/modules/home/home.component.ts

@ -17,24 +17,19 @@
import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { fromEvent, Observable } from 'rxjs'; import { fromEvent, Observable } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { debounceTime, distinctUntilChanged, map, mergeMap, take, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { User } from '@shared/models/user.model'; import { User } from '@shared/models/user.model';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AuthService } from '@core/auth/auth.service';
import { UserService } from '@core/http/user.service';
import { MenuService } from '@core/services/menu.service';
import { getCurrentAuthState, selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; import { getCurrentAuthState, selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { Router } from '@angular/router';
import * as _screenfull from 'screenfull'; import * as _screenfull from 'screenfull';
import { MatSidenav } from '@angular/material/sidenav'; import { MatSidenav } from '@angular/material/sidenav';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { ISearchableComponent, instanceOfSearchableComponent } from '@home/models/searchable-component.models'; import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models';
const screenfull = _screenfull as _screenfull.Screenfull; const screenfull = _screenfull as _screenfull.Screenfull;
@ -74,9 +69,6 @@ export class HomeComponent extends PageComponent implements AfterViewInit, OnIni
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
private authService: AuthService,
private router: Router,
private userService: UserService, private menuService: MenuService,
public breakpointObserver: BreakpointObserver) { public breakpointObserver: BreakpointObserver) {
super(store); super(store);
} }

10
ui-ngx/src/app/modules/home/menu/menu-link.component.html

@ -15,10 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<a class="mat-button" routerLinkActive="tb-active" [routerLinkActiveOptions]="{exact: true}" routerLink="{{section.path}}"> <a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{exact: true}" routerLink="{{section.path}}">
<span class="mat-button-wrapper"> <mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon>
<mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon> <mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon> <span>{{section.name | translate}}</span>
<span>{{section.name | translate}}</span>
</span>
</a> </a>

14
ui-ngx/src/app/modules/home/menu/menu-toggle.component.html

@ -15,15 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<a class="mat-button tb-button-toggle" <a mat-button class="tb-button-toggle"
routerLinkActive="tb-active" [routerLinkActiveOptions]="{exact: true}" routerLink="{{section.path}}"> routerLinkActive="tb-active" [routerLinkActiveOptions]="{exact: true}" routerLink="{{section.path}}">
<span class="mat-button-wrapper"> <mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon>
<mat-icon *ngIf="!section.isMdiIcon && section.icon != null" class="material-icons">{{section.icon}}</mat-icon> <mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon != null" [svgIcon]="section.icon"></mat-icon> <span>{{section.name | translate}}</span>
<span>{{section.name | translate}}</span> <span class=" pull-right fa fa-chevron-down tb-toggle-icon"
<span class=" pull-right fa fa-chevron-down tb-toggle-icon" [ngClass]="{'tb-toggled' : sectionActive()}"></span>
[ngClass]="{'tb-toggled' : sectionActive()}"></span>
</span>
</a> </a>
<ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}"> <ul id="docs-menu-{{section.name | nospace}}" class="tb-menu-toggle-list" [ngStyle]="{height: sectionHeight()}">
<li *ngFor="let page of section.pages"> <li *ngFor="let page of section.pages">

30
ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss

@ -13,6 +13,34 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { :host ::ng-deep {
.tb-button-toggle {
.tb-toggle-icon {
display: inline-block;
width: 15px;
margin: auto 0 auto auto;
background-size: 100% auto;
transition: transform .3s ease-in-out;
&.tb-toggled {
transform: rotateZ(180deg);
}
}
}
.tb-menu-toggle-list {
position: relative;
z-index: 1;
overflow: hidden;
transition: .75s cubic-bezier(.35, 0, .25, 1);
transition-property: height;
a.mat-button {
padding: 0 16px 0 32px;
font-weight: 500;
text-transform: none !important;
text-rendering: optimizeLegibility;
}
}
} }

43
ui-ngx/src/app/modules/home/menu/side-menu.component.scss

@ -34,18 +34,8 @@
a.mat-button { a.mat-button {
text-transform: uppercase; text-transform: uppercase;
display: flex; display: flex;
width: 100%;
max-height: 40px;
padding: 0 16px;
margin: 0;
overflow: hidden; overflow: hidden;
line-height: 40px; line-height: 40px;
color: inherit;
text-align: left;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
border-radius: 0; border-radius: 0;
&:hover { &:hover {
border-bottom: none; border-bottom: none;
@ -61,6 +51,8 @@
.mat-button-wrapper { .mat-button-wrapper {
width: 100%; width: 100%;
display: inline-flex;
align-items: center;
mat-icon { mat-icon {
margin-right: 8px; margin-right: 8px;
margin-left: 0; margin-left: 0;
@ -76,35 +68,4 @@
} }
} }
} }
.tb-button-toggle {
.tb-toggle-icon {
display: inline-block;
width: 15px;
margin: auto 0 auto auto;
background-size: 100% auto;
transition: transform .3s, ease-in-out;
&.tb-toggled {
transform: rotateZ(180deg);
}
}
}
.tb-menu-toggle-list {
position: relative;
z-index: 1;
overflow: hidden;
transition: .75s cubic-bezier(.35, 0, .25, 1);
transition-property: height;
a.mat-button {
padding: 0 16px 0 32px;
font-weight: 500;
text-transform: none !important;
text-rendering: optimizeLegibility;
}
}
} }

12
ui-ngx/src/app/modules/home/models/datasource/entity-datasource.ts

@ -36,8 +36,8 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
public currentEntity: T = null; public currentEntity: T = null;
constructor(private fetchFunction: EntitiesFetchFunction<T, P>, constructor(private fetchFunction: EntitiesFetchFunction<T, P>,
private selectionEnabledFunction: EntityBooleanFunction<T>, protected selectionEnabledFunction: EntityBooleanFunction<T>,
private dataLoadedFunction: () => void) {} protected dataLoadedFunction: (col?: number, row?: number) => void) {}
connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
return this.entitiesSubject.asObservable(); return this.entitiesSubject.asObservable();
@ -50,7 +50,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
reset() { reset() {
const pageData = emptyPageData<T>(); const pageData = emptyPageData<T>();
this.entitiesSubject.next(pageData.data); this.onEntities(pageData.data);
this.pageDataSubject.next(pageData); this.pageDataSubject.next(pageData);
this.dataLoadedFunction(); this.dataLoadedFunction();
} }
@ -64,7 +64,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
catchError(() => of(emptyPageData<T>())), catchError(() => of(emptyPageData<T>())),
).subscribe( ).subscribe(
(pageData) => { (pageData) => {
this.entitiesSubject.next(pageData.data); this.onEntities(pageData.data);
this.pageDataSubject.next(pageData); this.pageDataSubject.next(pageData);
result.next(pageData); result.next(pageData);
this.dataLoadedFunction(); this.dataLoadedFunction();
@ -73,6 +73,10 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
return result; return result;
} }
protected onEntities(entities: T[]) {
this.entitiesSubject.next(entities);
}
isAllSelected(): Observable<boolean> { isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe( return this.entitiesSubject.pipe(

70
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -14,21 +14,21 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {BaseData, HasId} from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import {EntitiesFetchFunction} from '@home/models/datasource/entity-datasource'; import { EntitiesDataSource, EntitiesFetchFunction } from '@home/models/datasource/entity-datasource';
import {Observable, of} from 'rxjs'; import { Observable, of } from 'rxjs';
import {emptyPageData} from '@shared/models/page/page-data'; import { emptyPageData } from '@shared/models/page/page-data';
import {DatePipe} from '@angular/common'; import { DatePipe } from '@angular/common';
import {Direction, SortOrder} from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import {EntityType, EntityTypeResource, EntityTypeTranslation} from '@shared/models/entity-type.models'; import { EntityType, EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
import {EntityComponent} from '@home/components/entity/entity.component'; import { EntityComponent } from '@home/components/entity/entity.component';
import {Type} from '@angular/core'; import { Type } from '@angular/core';
import {EntityAction} from './entity-component.models'; import { EntityAction } from './entity-component.models';
import {HasUUID} from '@shared/models/id/has-uuid'; import { HasUUID } from '@shared/models/id/has-uuid';
import {PageLink} from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import {EntitiesTableComponent} from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import {EntityTableHeaderComponent} from '@home/components/entity/entity-table-header.component'; import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import {ActivatedRoute} from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean; export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean;
@ -39,6 +39,7 @@ export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Ob
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>; export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean; export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>; export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>;
export type EntityRowClickFunction<T extends BaseData<HasId>> = (event: Event, entity: T) => boolean;
export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string; export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string;
export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined; export type CellTooltipFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string | undefined;
@ -76,7 +77,9 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
public key: string, public key: string,
public title: string, public title: string,
public width: string = '0px', public width: string = '0px',
public sortable: boolean = true) { public sortable: boolean = true,
public ignoreTranslate: boolean = false,
public mobileHide: boolean = false) {
} }
} }
@ -120,7 +123,7 @@ export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTabl
export type EntityColumn<T extends BaseData<HasId>> = EntityTableColumn<T> | EntityActionTableColumn<T>; export type EntityColumn<T extends BaseData<HasId>> = EntityTableColumn<T> | EntityActionTableColumn<T>;
export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink> { export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink, L extends BaseData<HasId> = T> {
constructor() {} constructor() {}
@ -137,32 +140,41 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
addEnabled = true; addEnabled = true;
entitiesDeleteEnabled = true; entitiesDeleteEnabled = true;
detailsPanelEnabled = true; detailsPanelEnabled = true;
hideDetailsTabsOnEdit = true;
actionsColumnTitle = null; actionsColumnTitle = null;
entityTranslations: EntityTypeTranslation; entityTranslations: EntityTypeTranslation;
entityResources: EntityTypeResource; entityResources: EntityTypeResource<T>;
entityComponent: Type<EntityComponent<T>>; entityComponent: Type<EntityComponent<T, P, L>>;
entityTabsComponent: Type<EntityTabsComponent<T>>; entityTabsComponent: Type<EntityTabsComponent<T, P, L>>;
addDialogStyle = {}; addDialogStyle = {};
defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC}; defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC};
columns: Array<EntityColumn<T>> = []; displayPagination = true;
cellActionDescriptors: Array<CellActionDescriptor<T>> = []; defaultPageSize = 10;
groupActionDescriptors: Array<GroupActionDescriptor<T>> = []; columns: Array<EntityColumn<L>> = [];
cellActionDescriptors: Array<CellActionDescriptor<L>> = [];
groupActionDescriptors: Array<GroupActionDescriptor<L>> = [];
headerActionDescriptors: Array<HeaderActionDescriptor> = []; headerActionDescriptors: Array<HeaderActionDescriptor> = [];
addActionDescriptors: Array<HeaderActionDescriptor> = []; addActionDescriptors: Array<HeaderActionDescriptor> = [];
headerComponent: Type<EntityTableHeaderComponent<T>>; headerComponent: Type<EntityTableHeaderComponent<T, P, L>>;
addEntity: CreateEntityOperation<T> = null; addEntity: CreateEntityOperation<T> = null;
dataSource: (dataLoadedFunction: (col?: number, row?: number) => void)
=> EntitiesDataSource<L> = (dataLoadedFunction: (col?: number, row?: number) => void) => {
return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction);
};
detailsReadonly: EntityBooleanFunction<T> = () => false; detailsReadonly: EntityBooleanFunction<T> = () => false;
entitySelectionEnabled: EntityBooleanFunction<T> = () => true; entitySelectionEnabled: EntityBooleanFunction<L> = () => true;
deleteEnabled: EntityBooleanFunction<T> = () => true; deleteEnabled: EntityBooleanFunction<T | L> = () => true;
deleteEntityTitle: EntityStringFunction<T> = () => ''; deleteEntityTitle: EntityStringFunction<L> = () => '';
deleteEntityContent: EntityStringFunction<T> = () => ''; deleteEntityContent: EntityStringFunction<L> = () => '';
deleteEntitiesTitle: EntityCountStringFunction = () => ''; deleteEntitiesTitle: EntityCountStringFunction = () => '';
deleteEntitiesContent: EntityCountStringFunction = () => ''; deleteEntitiesContent: EntityCountStringFunction = () => '';
loadEntity: EntityByIdOperation<T> = () => of(); loadEntity: EntityByIdOperation<T> = () => of();
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity); saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
deleteEntity: EntityIdOneWayOperation = () => of(); deleteEntity: EntityIdOneWayOperation = () => of();
entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>()); entitiesFetchFunction: EntitiesFetchFunction<L, P> = () => of(emptyPageData<L>());
onEntityAction: EntityActionFunction<T> = () => false; onEntityAction: EntityActionFunction<T> = () => false;
handleRowClick: EntityRowClickFunction<L> = () => false;
entityTitle: EntityStringFunction<T> = (entity) => entity?.name;
} }
export function checkBoxCell(value: boolean): string { export function checkBoxCell(value: boolean): string {

44
ui-ngx/src/app/modules/home/models/services.map.ts

@ -0,0 +1,44 @@
///
/// Copyright © 2016-2020 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 { Type } from '@angular/core';
import { DeviceService } from '@core/http/device.service';
import { AssetService } from '@core/http/asset.service';
import { AttributeService } from '@core/http/attribute.service';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { EntityService } from '@core/http/entity.service';
import { DialogService } from '@core/services/dialog.service';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
export const ServicesMap = new Map<string, Type<any>>(
[
['deviceService', DeviceService],
['assetService', AssetService],
['attributeService', AttributeService],
['entityRelationService', EntityRelationService],
['entityService', EntityService],
['dialogs', DialogService],
['customDialog', CustomDialogService],
['date', DatePipe],
['utils', UtilsService],
['translate', TranslateService],
['http', HttpClient]
]
);

6
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -15,13 +15,13 @@
/// ///
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import {GeneralSettingsComponent} from '@modules/home/pages/admin/general-settings.component'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import {SecuritySettingsComponent} from '@modules/home/pages/admin/security-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
const routes: Routes = [ const routes: Routes = [
{ {

6
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -20,9 +20,9 @@ import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module'; import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '@app/shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import {GeneralSettingsComponent} from '@modules/home/pages/admin/general-settings.component'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import {SecuritySettingsComponent} from '@modules/home/pages/admin/security-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import {HomeComponentsModule} from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
@NgModule({ @NgModule({
declarations: declarations:

5
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts

@ -20,10 +20,8 @@ import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {AdminSettings, GeneralSettings} from '@shared/models/settings.models'; import { AdminSettings, GeneralSettings } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service'; import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
@Component({ @Component({
@ -39,7 +37,6 @@ export class GeneralSettingsComponent extends PageComponent implements OnInit, H
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private router: Router, private router: Router,
private adminService: AdminService, private adminService: AdminService,
private translate: TranslateService,
public fb: FormBuilder) { public fb: FormBuilder) {
super(store); super(store);
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save