Browse Source

Merge branch 'master' into hotfix/3.6.3

pull/10394/head
ViacheslavKlimov 3 years ago
parent
commit
b3b27210b0
  1. 44
      application/src/main/data/json/demo/dashboards/thermostats.json
  2. 2
      application/src/main/data/json/system/widget_types/bar_chart.json
  3. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  4. 2
      application/src/main/data/json/system/widget_types/line_chart.json
  5. 2
      application/src/main/data/json/system/widget_types/point_chart.json
  6. 5
      application/src/main/data/json/system/widget_types/range_chart.json
  7. 2
      application/src/main/data/json/system/widget_types/time_series_chart.json
  8. 4
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  9. 7
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java
  10. 16
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  11. 2
      application/src/main/resources/thingsboard.yml
  12. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  13. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  14. 37
      ui-ngx/package.json
  15. 2
      ui-ngx/patches/@angular+core+15.2.10.patch
  16. 229
      ui-ngx/patches/echarts+5.5.0.patch
  17. 2
      ui-ngx/pom.xml
  18. 37
      ui-ngx/src/app/core/services/dialog.service.ts
  19. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  20. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  21. 11
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  22. 10
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  23. 3
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts
  24. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts
  25. 15
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  26. 8
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  27. 152
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  28. 84
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  29. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
  30. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html
  31. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  32. 49
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html
  33. 73
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.scss
  34. 92
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts
  35. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.html
  36. 99
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts
  37. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html
  38. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts
  39. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html
  40. 49
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts
  41. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  42. 5
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  43. 14
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html
  44. 18
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  45. 14
      ui-ngx/src/app/shared/components/dialog/alert-dialog.component.ts
  46. 14
      ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.ts
  47. 11
      ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts
  48. 157
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  49. 14
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  50. 14
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  51. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  52. 4060
      ui-ngx/yarn.lock

44
application/src/main/data/json/demo/dashboards/thermostats.json

@ -1147,7 +1147,8 @@
"showLine": true,
"lineColor": "rgba(0, 0, 0, 0.54)",
"showSplitLines": true,
"splitLinesColor": "rgba(0, 0, 0, 0.12)"
"splitLinesColor": "rgba(0, 0, 0, 0.12)",
"ticksFormat": {}
},
"legendLabelFont": {
"family": "Roboto",
@ -1173,7 +1174,9 @@
"tooltipDateFormat": {
"format": "MMM dd yyyy HH:mm",
"lastUpdateAgo": false,
"custom": false
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",
@ -1537,7 +1540,8 @@
"showLine": true,
"lineColor": "rgba(0, 0, 0, 0.54)",
"showSplitLines": true,
"splitLinesColor": "rgba(0, 0, 0, 0.12)"
"splitLinesColor": "rgba(0, 0, 0, 0.12)",
"ticksFormat": {}
},
"legendLabelFont": {
"family": "Roboto",
@ -1561,9 +1565,11 @@
"tooltipValueColor": "rgba(0, 0, 0, 0.76)",
"tooltipShowDate": true,
"tooltipDateFormat": {
"format": "MMM dd yyyy HH:mm",
"format": null,
"lastUpdateAgo": false,
"custom": false
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",
@ -1712,19 +1718,25 @@
"sizeX": 11,
"sizeY": 11,
"row": 0,
"col": 0
"col": 0,
"mobileOrder": 1,
"mobileHeight": 5
},
"7943196b-eedb-d422-f9c3-b32d379ad172": {
"sizeX": 13,
"sizeY": 5,
"row": 0,
"col": 11
"col": 11,
"mobileOrder": 2,
"mobileHeight": 5
},
"3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": {
"sizeX": 13,
"sizeY": 6,
"row": 5,
"col": 11
"col": 11,
"mobileOrder": 3,
"mobileHeight": 5
}
},
"gridSettings": {
@ -1778,25 +1790,33 @@
"sizeX": 6,
"sizeY": 6,
"row": 0,
"col": 0
"col": 0,
"mobileOrder": 3,
"mobileHeight": 5
},
"0a430429-9078-9ae6-2b67-e4a15a2bf8bf": {
"sizeX": 6,
"sizeY": 6,
"row": 6,
"col": 0
"col": 0,
"mobileOrder": 4,
"mobileHeight": 6
},
"eda8a397-0959-690c-405c-11e2c9b2bc7e": {
"sizeX": 18,
"sizeY": 6,
"row": 0,
"col": 6
"col": 6,
"mobileOrder": 1,
"mobileHeight": 6
},
"ac90f089-197f-b767-82c3-2668844265a2": {
"sizeX": 18,
"sizeY": 6,
"row": 6,
"col": 6
"col": 6,
"mobileOrder": 2,
"mobileHeight": 6
}
},
"gridSettings": {

2
application/src/main/data/json/system/widget_types/bar_chart.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/bar_chart_with_labels.json

@ -20,7 +20,7 @@
"latestDataKeySettingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-bar-chart-with-labels-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"tooltipDateInterval\":true},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
},
"tags": [
"bar chart",

2
application/src/main/data/json/system/widget_types/line_chart.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/point_chart.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/range_chart.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/time_series_chart.json

File diff suppressed because one or more lines are too long

4
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
@ -232,6 +233,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} catch (InvalidParameterException e) {
log.warn("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.BAD_DATA.withReason(e.getMessage()));
} catch (JwtExpiredTokenException e) {
log.trace("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.SERVER_ERROR.withReason(e.getMessage()));
} catch (Exception e) {
log.warn("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.SERVER_ERROR.withReason(e.getMessage()));

7
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java

@ -22,6 +22,8 @@ import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
@ -71,6 +73,11 @@ public class DefaultFirebaseService implements FirebaseService {
.setAndroidConfig(AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setContentAvailable(true)
.build())
.build())
.putAllData(data)
.build();
firebaseContext.getMessaging().send(message);

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

@ -191,6 +191,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private int telemetryTtl;
private ListeningExecutorService deviceStateExecutor;
private ListeningExecutorService deviceStateCallbackExecutor;
final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
@ -199,6 +200,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
super.init();
deviceStateExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state"));
deviceStateCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state-callback"));
scheduledExecutor.scheduleWithFixedDelay(this::checkStates, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
scheduledExecutor.scheduleWithFixedDelay(this::reportActivityStats, defaultActivityStatsIntervalInSec, defaultActivityStatsIntervalInSec, TimeUnit.SECONDS);
}
@ -209,6 +212,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
if (deviceStateExecutor != null) {
deviceStateExecutor.shutdownNow();
}
if (deviceStateCallbackExecutor != null) {
deviceStateCallbackExecutor.shutdownNow();
}
}
@Override
@ -372,7 +378,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
log.warn("Failed to register device to the state service", t);
callback.onFailure(t);
}
}, deviceStateExecutor);
}, deviceStateCallbackExecutor);
} else if (proto.getUpdated()) {
DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
TbMsgMetaData md = new TbMsgMetaData();
@ -635,10 +641,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
ListenableFuture<DeviceStateData> future;
if (persistToTelemetry) {
ListenableFuture<List<TsKvEntry>> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES);
future = Futures.transform(tsData, extractDeviceStateData(device), deviceStateExecutor);
future = Futures.transform(tsData, extractDeviceStateData(device), MoreExecutors.directExecutor());
} else {
ListenableFuture<List<AttributeKvEntry>> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
future = Futures.transform(attrData, extractDeviceStateData(device), deviceStateExecutor);
future = Futures.transform(attrData, extractDeviceStateData(device), MoreExecutors.directExecutor());
}
return transformInactivityTimeout(future);
}
@ -656,8 +662,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
});
return deviceStateData;
}, deviceStateExecutor);
}, deviceStateExecutor);
}, MoreExecutors.directExecutor());
}, deviceStateCallbackExecutor);
}
private <T extends KvEntry> Function<List<T>, DeviceStateData> extractDeviceStateData(Device device) {

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

@ -193,7 +193,7 @@ ui:
# Help parameters
help:
# Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.2}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.3}"
# Database telemetry parameters
database:

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

@ -222,7 +222,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
} else {
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(
tenantId, requiredNotificationTypes, new PageLink(1))
tenantId, requiredNotificationTypes, new PageLink(2))
.getData()
.stream()
.map(NotificationTemplate::getNotificationType)

50
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

37
ui-ngx/package.json

@ -14,17 +14,17 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.9",
"@angular/animations": "^15.2.10",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.2.9",
"@angular/compiler": "^15.2.9",
"@angular/core": "^15.2.9",
"@angular/common": "^15.2.10",
"@angular/compiler": "^15.2.10",
"@angular/core": "^15.2.10",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.2.9",
"@angular/forms": "^15.2.10",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.2.9",
"@angular/platform-browser-dynamic": "^15.2.9",
"@angular/router": "^15.2.9",
"@angular/platform-browser": "^15.2.10",
"@angular/platform-browser-dynamic": "^15.2.10",
"@angular/router": "^15.2.10",
"@auth0/angular-jwt": "^5.1.2",
"@date-io/core": "1.3.7",
"@date-io/date-fns": "1.3.7",
@ -62,7 +62,7 @@
"html2canvas": "^1.4.1",
"jquery": "^3.6.3",
"jquery.terminal": "^2.35.3",
"js-beautify": "^1.14.7",
"js-beautify": "1.14.7",
"json-schema-defaults": "^0.4.0",
"jstree": "^3.3.15",
"jstree-bootstrap-theme": "^1.0.1",
@ -112,16 +112,16 @@
},
"devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "^15.2.8",
"@angular-devkit/build-angular": "^15.2.10",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.8",
"@angular/compiler-cli": "^15.2.9",
"@angular/language-service": "^15.2.9",
"@ngtools/webpack": "15.2.1",
"@angular/cli": "^15.2.10",
"@angular/compiler-cli": "^15.2.10",
"@angular/language-service": "^15.2.10",
"@ngtools/webpack": "15.2.10",
"@types/ace-diff": "^2.1.1",
"@types/canvas-gauges": "^2.1.4",
"@types/flot": "^0.0.32",
@ -130,7 +130,7 @@
"@types/jasminewd2": "^2.0.10",
"@types/jquery": "^3.5.16",
"@types/js-beautify": "^1.13.3",
"@types/leaflet": "^1.8.0",
"@types/leaflet": "~1.8.0",
"@types/leaflet-polylinedecorator": "^1.6.1",
"@types/leaflet-providers": "^1.2.1",
"@types/leaflet.gridlayer.googlemutant": "^0.4.6",
@ -166,11 +166,14 @@
"raw-loader": "^4.0.2",
"ts-node": "^10.9.1",
"typescript": "~4.9.5",
"webpack": "^5.77.0"
"webpack": "5.77.0"
},
"resolutions": {
"@types/react": "17.0.37",
"ace-builds": "1.4.13",
"@date-io/core": "1.3.7"
"@date-io/core": "1.3.7",
"rc-virtual-list": "3.4.13",
"read-package-json": "6.0.0",
"cacache": "17.0.4"
}
}

2
ui-ngx/patches/@angular+core+15.2.9.patch → ui-ngx/patches/@angular+core+15.2.10.patch

@ -1,5 +1,5 @@
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
index 3e93015..9efcb96 100755
index e9a9b75..17044d9 100755
--- a/node_modules/@angular/core/fesm2020/core.mjs
+++ b/node_modules/@angular/core/fesm2020/core.mjs
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {

229
ui-ngx/patches/echarts+5.5.0.patch

@ -1,5 +1,5 @@
diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
index d6c05f3..d09baed 100644
index d6c05f3..aafb0b8 100644
--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
+++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
@@ -362,7 +362,10 @@ var DataZoomModel = /** @class */function (_super) {
@ -28,7 +28,7 @@ index d6c05f3..d09baed 100644
}
}
diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
index 06469b2..ccfbfa6 100644
index 06469b2..cf0b2ea 100644
--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
@@ -96,11 +96,14 @@ var getRangeHandlers = {
@ -52,7 +52,7 @@ index 06469b2..ccfbfa6 100644
},
pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) {
diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
index 98912e0..2e809af 100644
index 98912e0..0cda6be 100644
--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7;
@ -147,7 +147,7 @@ index 98912e0..2e809af 100644
SliderZoomView.prototype._updateView = function (nonRealtime) {
var displaybles = this._displayables;
diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
index ce98fed..361b138 100644
index ce98fed..e154118 100644
--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
+++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
@@ -90,7 +90,10 @@ var dataZoomProcessor = {
@ -175,7 +175,7 @@ index ce98fed..361b138 100644
});
ecModel.eachComponent('dataZoom', function (dataZoomModel) {
diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
index cf8d6bc..9b30ec1 100644
index cf8d6bc..f9b9f90 100644
--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
+++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
@@ -109,9 +109,12 @@ var DataZoomFeature = /** @class */function (_super) {
@ -195,7 +195,7 @@ index cf8d6bc..9b30ec1 100644
dataZoomModel && (snapshot[dataZoomModel.id] = {
dataZoomId: dataZoomModel.id,
diff --git a/node_modules/echarts/lib/component/tooltip/TooltipView.js b/node_modules/echarts/lib/component/tooltip/TooltipView.js
index b8a9b95..8e4cb2f 100644
index b8a9b95..11e49c0 100644
--- a/node_modules/echarts/lib/component/tooltip/TooltipView.js
+++ b/node_modules/echarts/lib/component/tooltip/TooltipView.js
@@ -360,7 +360,7 @@ var TooltipView = /** @class */function (_super) {
@ -207,3 +207,220 @@ index b8a9b95..8e4cb2f 100644
return;
}
var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt);
diff --git a/node_modules/echarts/lib/coord/axisHelper.js b/node_modules/echarts/lib/coord/axisHelper.js
index a76c66b..e5b7ee5 100644
--- a/node_modules/echarts/lib/coord/axisHelper.js
+++ b/node_modules/echarts/lib/coord/axisHelper.js
@@ -187,7 +187,9 @@ export function createScaleByModel(model, axisType) {
});
default:
// case 'value'/'interval', 'log', or others.
- return new (Scale.getClass(axisType) || IntervalScale)();
+ return new (Scale.getClass(axisType) || IntervalScale)({
+ ticksGenerator: model.get('ticksGenerator')
+ });
}
}
}
diff --git a/node_modules/echarts/lib/coord/cartesian/Grid.js b/node_modules/echarts/lib/coord/cartesian/Grid.js
index 5b18f02..39a57f8 100644
--- a/node_modules/echarts/lib/coord/cartesian/Grid.js
+++ b/node_modules/echarts/lib/coord/cartesian/Grid.js
@@ -91,11 +91,11 @@ var Grid = /** @class */function () {
var scale = axis.scale;
if (
// Only value and log axis without interval support alignTicks.
- isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null) {
+ isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null && model.get('ticksGenerator') == null) {
axisNeedsAlign.push(axis);
} else {
niceScaleExtent(scale, model);
- if (isIntervalOrLogScale(scale)) {
+ if (isIntervalOrLogScale(scale) && !scale.isBlank()) {
// Can only align to interval or log axis.
alignTo = axis;
}
@@ -105,10 +105,15 @@ var Grid = /** @class */function () {
// All axes has set alignTicks. Pick the first one.
// PENDING. Should we find the axis that both set interval, min, max and align to this one?
if (axisNeedsAlign.length) {
- if (!alignTo) {
- alignTo = axisNeedsAlign.pop();
- niceScaleExtent(alignTo.scale, alignTo.model);
+ while (!alignTo && axisNeedsAlign.length) {
+ var axis = axisNeedsAlign.pop();
+ niceScaleExtent(axis.scale, axis.model);
+ if (!axis.scale.isBlank()) {
+ alignTo = axis;
+ }
}
+ }
+ if (axisNeedsAlign.length && alignTo) {
each(axisNeedsAlign, function (axis) {
alignScaleTicks(axis.scale, axis.model, alignTo.scale);
});
diff --git a/node_modules/echarts/lib/data/SeriesData.js b/node_modules/echarts/lib/data/SeriesData.js
index 98d5ce8..1c293a6 100644
--- a/node_modules/echarts/lib/data/SeriesData.js
+++ b/node_modules/echarts/lib/data/SeriesData.js
@@ -900,13 +900,16 @@ var SeriesData = /** @class */function () {
var dimInfo = data._dimInfos[dim];
// Currently, only dimensions that has ordinalMeta can create inverted indices.
var ordinalMeta = dimInfo.ordinalMeta;
+ var stack = dimInfo.stack;
var store = data._store;
- if (ordinalMeta) {
- invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array(ordinalMeta.categories.length);
- // The default value of TypedArray is 0. To avoid miss
- // mapping to 0, we should set it as INDEX_NOT_FOUND.
- for (var i = 0; i < invertedIndices.length; i++) {
- invertedIndices[i] = INDEX_NOT_FOUND;
+ if (ordinalMeta || stack) {
+ invertedIndices = invertedIndicesMap[dim] = stack ? new Array(store.count()) : new CtorInt32Array(ordinalMeta.categories.length);
+ if (ordinalMeta) {
+ // The default value of TypedArray is 0. To avoid miss
+ // mapping to 0, we should set it as INDEX_NOT_FOUND.
+ for (var i = 0; i < invertedIndices.length; i++) {
+ invertedIndices[i] = INDEX_NOT_FOUND;
+ }
}
for (var i = 0; i < store.count(); i++) {
// Only support the case that all values are distinct.
diff --git a/node_modules/echarts/lib/data/Source.js b/node_modules/echarts/lib/data/Source.js
index 7dda49b..2dd2b98 100644
--- a/node_modules/echarts/lib/data/Source.js
+++ b/node_modules/echarts/lib/data/Source.js
@@ -252,7 +252,8 @@ function normalizeDimensionsOption(dimensionsDefine) {
var item = {
name: rawItem.name,
displayName: rawItem.displayName,
- type: rawItem.type
+ type: rawItem.type,
+ stack: rawItem.stack
};
// User can set null in dimensions.
// We don't auto specify name, otherwise a given name may
diff --git a/node_modules/echarts/lib/data/helper/createDimensions.js b/node_modules/echarts/lib/data/helper/createDimensions.js
index 00d7eb7..dd514b1 100644
--- a/node_modules/echarts/lib/data/helper/createDimensions.js
+++ b/node_modules/echarts/lib/data/helper/createDimensions.js
@@ -110,6 +110,9 @@ source, opt) {
}
dimDefItem.type != null && (resultItem.type = dimDefItem.type);
dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName);
+ if (dimDefItem.stack) {
+ resultItem.stack = true;
+ }
var newIdx = resultList.length;
indicesMap[dimIdx] = newIdx;
resultItem.storeDimIndex = dimIdx;
diff --git a/node_modules/echarts/lib/data/helper/dataStackHelper.js b/node_modules/echarts/lib/data/helper/dataStackHelper.js
index c25de1b..ea8300d 100644
--- a/node_modules/echarts/lib/data/helper/dataStackHelper.js
+++ b/node_modules/echarts/lib/data/helper/dataStackHelper.js
@@ -91,7 +91,7 @@ export function enableDataStack(seriesModel, dimensionsInput, opt) {
}
if (mayStack && !dimensionInfo.isExtraCoord) {
// Find the first ordinal dimension as the stackedByDimInfo.
- if (!byIndex && !stackedByDimInfo && dimensionInfo.ordinalMeta) {
+ if (!byIndex && !stackedByDimInfo && (dimensionInfo.ordinalMeta || dimensionInfo.stack)) {
stackedByDimInfo = dimensionInfo;
}
// Find the first stackable dimension as the stackedDimInfo.
diff --git a/node_modules/echarts/lib/scale/Interval.js b/node_modules/echarts/lib/scale/Interval.js
index 1094662..363c0a5 100644
--- a/node_modules/echarts/lib/scale/Interval.js
+++ b/node_modules/echarts/lib/scale/Interval.js
@@ -46,12 +46,17 @@ import * as numberUtil from '../util/number.js';
import * as formatUtil from '../util/format.js';
import Scale from './Scale.js';
import * as helper from './helper.js';
+import { isFunction } from 'zrender/lib/core/util.js';
var roundNumber = numberUtil.round;
var IntervalScale = /** @class */function (_super) {
__extends(IntervalScale, _super);
- function IntervalScale() {
- var _this = _super !== null && _super.apply(this, arguments) || this;
+ function IntervalScale(setting) {
+ var _this = _super.call(this, setting) || this;
_this.type = 'interval';
+ var ticksGenerator = _this.getSetting('ticksGenerator');
+ if (isFunction(ticksGenerator)) {
+ _this._ticksGenerator = ticksGenerator;
+ }
// Step is calculated in adjustExtent.
_this._interval = 0;
_this._intervalPrecision = 2;
@@ -104,7 +109,17 @@ var IntervalScale = /** @class */function (_super) {
var extent = this._extent;
var niceTickExtent = this._niceExtent;
var intervalPrecision = this._intervalPrecision;
- var ticks = [];
+ var ticksGenerator = this._ticksGenerator;
+ var ticks;
+ if (ticksGenerator) {
+ try {
+ ticks = ticksGenerator(extent, interval, niceTickExtent, intervalPrecision);
+ if (ticks) {
+ return ticks;
+ }
+ } catch (_e) {}
+ }
+ ticks = [];
// If interval is 0, return [];
if (!interval) {
return ticks;
diff --git a/node_modules/echarts/types/dist/shared.d.ts b/node_modules/echarts/types/dist/shared.d.ts
index ca74097..ef41ce2 100644
--- a/node_modules/echarts/types/dist/shared.d.ts
+++ b/node_modules/echarts/types/dist/shared.d.ts
@@ -2422,6 +2422,9 @@ interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMixin {
max: number;
}) => ScaleDataValue);
}
+
+declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
+
interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
boundaryGap?: [number | string, number | string];
/**
@@ -2447,6 +2450,8 @@ interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
* Will be ignored if interval is set.
*/
alignTicks?: boolean;
+
+ ticksGenerator?: NumericAxisTicksGenerator;
}
interface CategoryAxisBaseOption extends AxisBaseOptionCommon {
type?: 'category';
@@ -6412,6 +6417,7 @@ declare type DimensionDefinition = {
type?: DataStoreDimensionType;
name?: DimensionName;
displayName?: string;
+ stack?: boolean;
};
declare type DimensionDefinitionLoose = DimensionDefinition['name'] | DimensionDefinition;
declare const SOURCE_FORMAT_ORIGINAL: "original";
diff --git a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
index c5c2792..d524b70 100644
--- a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
+++ b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
@@ -56,6 +56,9 @@ export interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMi
max: number;
}) => ScaleDataValue);
}
+
+export declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
+
export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
boundaryGap?: [number | string, number | string];
/**
@@ -81,6 +84,8 @@ export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
* Will be ignored if interval is set.
*/
alignTicks?: boolean;
+
+ ticksGenerator?: NumericAxisTicksGenerator;
}
export interface CategoryAxisBaseOption extends AxisBaseOptionCommon {
type?: 'category';

2
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v16.20.2</nodeVersion>
<nodeVersion>v20.11.1</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion>
</configuration>
</execution>

37
ui-ngx/src/app/core/services/dialog.service.ts

@ -21,22 +21,25 @@ import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import {
ColorPickerDialogComponent,
ColorPickerDialogData, ColorPickerDialogResult
ColorPickerDialogData,
ColorPickerDialogResult
} from '@shared/components/dialog/color-picker-dialog.component';
import {
MaterialIconsDialogComponent,
MaterialIconsDialogData, MaterialIconsDialogResult
MaterialIconsDialogData,
MaterialIconsDialogResult
} from '@shared/components/dialog/material-icons-dialog.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component';
import { ConfirmDialogComponent, ConfirmDialogData } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent, AlertDialogData } from '@shared/components/dialog/alert-dialog.component';
import {
ErrorAlertDialogComponent,
ErrorAlertDialogData
} from '@shared/components/dialog/error-alert-dialog.component';
import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component';
@Injectable(
{
providedIn: 'root'
}
)
@Injectable({
providedIn: 'root'
})
export class DialogService {
constructor(
@ -47,7 +50,7 @@ export class DialogService {
}
confirm(title: string, message: string, cancel: string = null, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig = {
const dialogConfig: MatDialogConfig<ConfirmDialogData> = {
disableClose: true,
data: {
title,
@ -59,12 +62,12 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<ConfirmDialogComponent, ConfirmDialogData, boolean>(ConfirmDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
alert(title: string, message: string, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig = {
const dialogConfig: MatDialogConfig<AlertDialogData> = {
disableClose: true,
data: {
title,
@ -75,12 +78,12 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(AlertDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<AlertDialogComponent, AlertDialogData, boolean>(AlertDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable<any> {
const dialogConfig: MatDialogConfig = {
errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig<ErrorAlertDialogData> = {
disableClose: true,
data: {
title,
@ -92,7 +95,7 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(ErrorAlertDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<ErrorAlertDialogComponent, ErrorAlertDialogData, boolean>(ErrorAlertDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}

3
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html

@ -195,7 +195,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

3
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html

@ -169,7 +169,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

11
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html

@ -25,10 +25,6 @@
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)">
</tb-time-series-chart-y-axes-panel>
<tb-data-keys-panel
panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
@ -45,6 +41,10 @@
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)">
</tb-time-series-chart-y-axes-panel>
<tb-time-series-chart-thresholds-panel
formControlName="thresholds"
[aliasController]="aliasController"
@ -189,7 +189,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

10
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts

@ -37,13 +37,13 @@ import {
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import {
autoDateFormat,
backgroundStyle,
ComponentStyle,
DateFormatProcessor,
getDataKey,
getLatestSingleTsValue,
overlayStyle,
simpleDateFormat,
textStyle
} from '@shared/models/widget-settings.models';
import { DataKey } from '@shared/models/widget.models';
@ -54,10 +54,9 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import {
TimeSeriesChartAxisSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings, TimeSeriesChartYAxisSettings
TimeSeriesChartSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
@ -188,12 +187,11 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
showSplitLines: true,
min: 'dataMin',
max: 'dataMax',
intervalCalculator:
'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;'
ticksGenerator: (extent?: number[]) => (extent ? [{ value: (extent[0] + extent[1]) / 2}] : [])
}
},
tooltipDateInterval: false,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss')
tooltipDateFormat: autoDateFormat()
};
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true);

3
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts

@ -30,6 +30,7 @@ import {
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import {
autoDateFormat,
backgroundStyle,
ColorProcessor,
ComponentStyle,
@ -174,7 +175,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
}
},
tooltipDateInterval: false,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss')
tooltipDateFormat: autoDateFormat()
};
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false);

9
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts

@ -57,7 +57,7 @@ import {
echartsTooltipFormatter,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { IntervalMath } from '@shared/models/time/time.models';
import { AggregationType, IntervalMath } from '@shared/models/time/time.models';
type BarChartDataItem = EChartsSeriesItem;
@ -97,6 +97,10 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
legendLabelStyle: ComponentStyle;
disabledLegendLabelStyle: ComponentStyle;
private get noAggregation(): boolean {
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
}
private shapeResize$: ResizeObserver;
private decimals = 0;
@ -388,7 +392,8 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
if (this.settings.showTooltip) {
const focusedSeriesIndex = this.focusedSeriesIndex();
return echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.decimals, this.units, focusedSeriesIndex);
this.settings, params, this.decimals, this.units, focusedSeriesIndex, null,
this.noAggregation ? null : this.ctx.timeWindow.interval);
} else {
return undefined;
}

15
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -44,9 +44,10 @@ import {
} from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
import { DataEntry, DataKey, DataSet, LegendDirection } from '@shared/models/widget.models';
import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models';
import {
calculateAggIntervalWithWidgetTimeWindow,
Interval,
IntervalMath,
WidgetTimewindow
} from '@shared/models/time/time.models';
@ -333,11 +334,15 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
decimals: number,
units: string,
focusedSeriesIndex: number,
series?: EChartsSeriesItem[]): null | HTMLElement => {
series?: EChartsSeriesItem[],
interval?: Interval): null | HTMLElement => {
if (!params || Array.isArray(params) && !params[0]) {
return null;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return null;
}
const tooltipElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipElement, 'display', 'flex');
renderer.setStyle(tooltipElement, 'flex-direction', 'column');
@ -349,8 +354,8 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
const startTs = firstParam.value[2];
const endTs = firstParam.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs);
const endDateText = tooltipDateFormat.update(endTs - 1);
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
@ -358,7 +363,7 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
}
} else {
const ts = firstParam.value[0];
dateText = tooltipDateFormat.update(ts);
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);

8
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts

@ -54,6 +54,7 @@ import {
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { AggregationType } from '@shared/models/time/time.models';
interface VisualPiece {
lt?: number;
@ -197,6 +198,10 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
disabledLegendLabelStyle: ComponentStyle;
visibleRangeItems: RangeItem[];
private get noAggregation(): boolean {
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
}
private rangeItems: RangeItem[];
private shapeResize$: ResizeObserver;
@ -324,7 +329,8 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.decimals, this.units, 0) : undefined,
this.settings, params, this.decimals, this.units, 0, null,
this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0,

152
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -22,12 +22,20 @@ import {
EChartsTooltipWidgetSettings,
measureThresholdLabelOffset
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models';
import {
autoDateFormat,
AutoDateFormatSettings,
ComponentStyle,
Font,
textStyle,
tsToFormatTimeUnit
} from '@shared/models/widget-settings.models';
import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared';
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts';
import {
formatValue,
isDefinedAndNotNull,
isFunction,
isNumeric,
isUndefined,
isUndefinedOrNull,
@ -36,7 +44,6 @@ import {
} from '@core/utils';
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import tinycolor from 'tinycolor2';
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D';
import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes';
import { SeriesLabelOption } from 'echarts/types/src/util/types';
import {
@ -50,6 +57,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TbColorScheme } from '@shared/models/color.models';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { MarkLine2DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { DatePipe } from '@angular/common';
export enum TimeSeriesChartType {
default = 'default',
@ -297,17 +305,39 @@ export interface TimeSeriesChartAxisSettings {
splitLinesColor: string;
}
export interface TimeSeriesChartXAxisSettings extends TimeSeriesChartAxisSettings {
ticksFormat: AutoDateFormatSettings;
}
export const defaultXAxisTicksFormat: AutoDateFormatSettings = {
millisecond: 'HH:mm:ss SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'HH:mm',
day: 'MMM dd',
month: 'MMM',
year: 'yyyy'
};
export type TimeSeriesChartYAxisId = 'default' | string;
export type TimeSeriesChartTicksGenerator =
(extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
export type TimeSeriesChartTicksFormatter =
(value: any) => string;
export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings {
id?: TimeSeriesChartYAxisId;
order?: number;
units?: string;
decimals?: number;
interval?: number;
splitNumber?: number;
min?: number | string;
max?: number | string;
intervalCalculator?: string;
ticksFormatter?: string;
ticksGenerator?: TimeSeriesChartTicksGenerator | string;
ticksFormatter?: TimeSeriesChartTicksFormatter | string;
}
export const timeSeriesChartYAxisValid = (axis: TimeSeriesChartYAxisSettings): boolean =>
@ -536,7 +566,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings {
dataZoom: boolean;
stack: boolean;
yAxes: TimeSeriesChartYAxes;
xAxis: TimeSeriesChartAxisSettings;
xAxis: TimeSeriesChartXAxisSettings;
animation: TimeSeriesChartAnimationSettings;
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
}
@ -574,6 +604,7 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
lineHeight: '1'
},
tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light,
ticksFormat: {},
showTicks: true,
ticksColor: timeSeriesChartColorScheme['axis.ticks'].light,
showLine: true,
@ -584,7 +615,7 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
animation: {
animation: true,
animationThreshold: 2000,
animationDuration: 1000,
animationDuration: 500,
animationEasing: TimeSeriesChartAnimationEasing.cubicOut,
animationDelay: 0,
animationDurationUpdate: 300,
@ -616,7 +647,7 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
},
tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
tooltipShowDate: true,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss'),
tooltipDateFormat: autoDateFormat(),
tooltipDateFont: {
family: 'Roboto',
size: 11,
@ -764,8 +795,6 @@ export interface TimeSeriesChartYAxis {
decimals: number;
settings: TimeSeriesChartYAxisSettings;
option: YAXisOption & ValueAxisBaseOption;
intervalCalculator?: (axis: Axis2D) => number;
ticksFormatter?: (value: any) => string;
}
export const createTimeSeriesYAxis = (units: string,
@ -776,11 +805,37 @@ export const createTimeSeriesYAxis = (units: string,
settings.tickLabelColor, darkMode, 'axis.tickLabel');
const yAxisNameStyle = createChartTextStyle(settings.labelFont,
settings.labelColor, darkMode, 'axis.label');
let ticksFormatter: (value: any) => string;
if (settings.ticksFormatter && settings.ticksFormatter.length) {
ticksFormatter = parseFunction(settings.ticksFormatter, ['value']);
let ticksFormatter: TimeSeriesChartTicksFormatter;
if (settings.ticksFormatter) {
if (isFunction(settings.ticksFormatter)) {
ticksFormatter = settings.ticksFormatter as TimeSeriesChartTicksFormatter;
} else if (settings.ticksFormatter.length) {
ticksFormatter = parseFunction(settings.ticksFormatter, ['value']);
}
}
let ticksGenerator: TimeSeriesChartTicksGenerator;
let minInterval: number;
let interval: number;
let splitNumber: number;
if (settings.ticksGenerator) {
if (isFunction(settings.ticksGenerator)) {
ticksGenerator = settings.ticksGenerator as TimeSeriesChartTicksGenerator;
} else if (settings.ticksGenerator.length) {
ticksGenerator = parseFunction(settings.ticksGenerator, ['extent', 'interval', 'niceTickExtent', 'intervalPrecision']);
}
}
const yAxis: TimeSeriesChartYAxis = {
if (!ticksGenerator) {
interval = settings.interval;
if (isUndefinedOrNull(interval)) {
if (isDefinedAndNotNull(settings.splitNumber)) {
splitNumber = settings.splitNumber;
} else {
minInterval = (1 / Math.pow(10, decimals));
}
}
}
return {
id: settings.id,
decimals,
settings,
@ -794,6 +849,10 @@ export const createTimeSeriesYAxis = (units: string,
scale: true,
min: settings.min,
max: settings.max,
minInterval,
splitNumber,
interval,
ticksGenerator,
name: settings.label,
nameLocation: 'middle',
nameRotate: settings.position === AxisPosition.left ? 90 : -90,
@ -829,7 +888,8 @@ export const createTimeSeriesYAxis = (units: string,
if (ticksFormatter) {
try {
result = ticksFormatter(value);
} catch (_e) {}
} catch (_e) {
}
}
if (isUndefined(result)) {
result = formatValue(value, decimals, units, false);
@ -845,18 +905,17 @@ export const createTimeSeriesYAxis = (units: string,
}
}
};
if (settings.intervalCalculator && settings.intervalCalculator.length) {
yAxis.intervalCalculator = parseFunction(settings.intervalCalculator, ['axis']);
}
return yAxis;
};
export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartAxisSettings,
min: number, max: number, darkMode: boolean): XAXisOption => {
export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartXAxisSettings,
min: number, max: number,
datePipe: DatePipe,
darkMode: boolean): XAXisOption => {
const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel');
const xAxisNameStyle = createChartTextStyle(settings.labelFont,
settings.labelColor, darkMode, 'axis.label');
const ticksFormat = mergeDeep({}, defaultXAxisTicksFormat, settings.ticksFormat);
return {
show: settings.show,
type: 'time',
@ -885,15 +944,16 @@ export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartAxisSetting
fontFamily: xAxisTickLabelStyle.fontFamily,
fontSize: xAxisTickLabelStyle.fontSize,
hideOverlap: true,
/* formatter: {
year: '{yyyy}',
month: '{MMM}',
day: '{d}',
hour: '{HH}:{mm}',
minute: '{HH}:{mm}',
second: '{HH}:{mm}:{ss}',
millisecond: '{hh}:{mm}:{ss} {SSS}'
} */
formatter: (value: number, index: number, extra: {level: number}) => {
const unit = tsToFormatTimeUnit(value);
const format = ticksFormat[unit];
const formatted = datePipe.transform(value, format);
if (extra.level > 0) {
return `{primary|${formatted}}`;
} else {
return formatted;
}
}
},
axisLine: {
show: settings.showLine,
@ -963,20 +1023,20 @@ const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[],
let seriesOption = item.option;
if (!item.option) {
const thresholdLabelStyle = createChartTextStyle(item.settings.labelFont,
item.settings.labelColor, darkMode, 'threshold.label');
item.settings.labelColor, false, 'threshold.label');
seriesOption = {
type: 'line',
id: item.id,
dataGroupId: item.id,
yAxisIndex: item.yAxisIndex,
data: [],
tooltip: {
show: false
},
markLine: {
tooltip: {
show: false
},
lineStyle: {
width: item.settings.lineWidth,
color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'),
color: prepareChartThemeColor(item.settings.lineColor, false, 'threshold.line'),
type: item.settings.lineType
},
label: {
@ -1115,17 +1175,6 @@ export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChart
}
}
}
for (const item of thresholdDataItems) {
if (Array.isArray(options.series)) {
const series = options.series.find(s => s.id === item.id);
if (series) {
series.markLine.lineStyle.color = prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line');
if (series.markLine?.label?.show) {
series.markLine.label.color = prepareChartThemeColor(item.settings.labelColor, darkMode, 'threshold.label');
}
}
}
}
return options;
};
@ -1149,12 +1198,14 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
focus: 'series'
},
dimensions: [
{name: 'intervalStart', type: 'number'},
{name: 'intervalEnd', type: 'number'}
{name: 'x', type: 'time', stack},
{name: 'y', type: 'float'},
{name: 'intervalStart', type: 'time'},
{name: 'intervalEnd', type: 'time'}
],
encode: {
intervalStart: 2,
intervalEnd: 3
x: [0, 2, 3],
y: [1]
}
};
item.option = seriesOption;
@ -1166,6 +1217,9 @@ const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem,
lineSettings.pointLabelFont, lineSettings.pointLabelColor, lineSettings.pointLabelPosition, darkMode);
lineSeriesOption.step = lineSettings.step ? lineSettings.stepType : false;
lineSeriesOption.smooth = lineSettings.smooth;
if (lineSettings.smooth) {
lineSeriesOption.smoothMonotone = 'x';
}
lineSeriesOption.lineStyle = {
width: lineSettings.showLine ? lineSettings.lineWidth : 0,
type: lineSettings.lineType

84
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -27,7 +27,8 @@ import {
TimeSeriesChartDataItem,
timeSeriesChartDefaultSettings,
timeSeriesChartKeyDefaultSettings,
TimeSeriesChartKeySettings, TimeSeriesChartNoAggregationBarWidthStrategy,
TimeSeriesChartKeySettings,
TimeSeriesChartNoAggregationBarWidthStrategy,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings,
TimeSeriesChartShape,
@ -51,7 +52,6 @@ import {
echartsTooltipFormatter,
EChartsTooltipTrigger,
getAxisExtent,
getYAxis,
measureXAxisNameHeight,
measureYAxisNameWidth,
toNamedData
@ -74,7 +74,7 @@ import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-s
export class TbTimeSeriesChart {
public static dataKeySettings(type = TimeSeriesChartType.default): DataKeySettingsFunction {
return (key, isLatestDataKey) => {
return (_key, isLatestDataKey) => {
if (!isLatestDataKey) {
const settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings);
@ -117,7 +117,7 @@ export class TbTimeSeriesChart {
private darkMode = false;
private messageChannel = new BroadcastChannel('tbMessageChannel');
private darkModeObserver: MutationObserver;
private topPointLabels = false;
@ -139,7 +139,8 @@ export class TbTimeSeriesChart {
this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings);
this.darkMode = this.settings.darkMode;
const dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
this.darkMode = this.settings.darkMode || dashboardPageElement.hasClass('dark');
this.setupYAxes();
this.setupData();
this.setupThresholds();
@ -153,12 +154,15 @@ export class TbTimeSeriesChart {
});
this.shapeResize$.observe(this.chartElement);
}
this.messageChannel.addEventListener('message', (event) => {
if (event?.data?.type === 'tbDarkMode') {
const darkMode = !!event?.data?.darkMode;
this.setDarkMode(darkMode);
this.darkModeObserver = new MutationObserver(mutations => {
for(let mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const darkMode = dashboardPageElement.hasClass('dark');
this.setDarkMode(darkMode);
}
}
});
this.darkModeObserver.observe(dashboardPageElement[0], { attributes: true });
}
public update(): void {
@ -265,7 +269,7 @@ export class TbTimeSeriesChart {
}
this.yMinSubject.complete();
this.yMaxSubject.complete();
this.messageChannel.close();
this.darkModeObserver.disconnect();
}
public resize(): void {
@ -485,7 +489,8 @@ export class TbTimeSeriesChart {
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, 0, '', -1, this.dataItems) : undefined,
this.settings, params, 0, '', -1, this.dataItems,
this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0,
@ -499,7 +504,7 @@ export class TbTimeSeriesChart {
}],
xAxis: [
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.darkMode)
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode)
],
yAxis: this.yAxisList.map(axis => axis.option),
dataZoom: [
@ -611,11 +616,6 @@ export class TbTimeSeriesChart {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true});
}
changed = this.calculateYAxisInterval(this.yAxisList);
if (changed) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis'], lazyUpdate: true});
}
if (this.yAxisList.length) {
const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id);
const min = extent[0];
@ -629,18 +629,6 @@ export class TbTimeSeriesChart {
}
}
private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean {
let changed = false;
for (const yAxis of axisList) {
const scaleYAxis = this.scaleYAxis(yAxis);
if (yAxis.option.scale !== scaleYAxis) {
yAxis.option.scale = scaleYAxis;
changed = true;
}
}
return changed;
}
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} {
const result = {offset: 0, changed: false};
let width = 0;
@ -684,42 +672,24 @@ export class TbTimeSeriesChart {
return result;
}
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean {
const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&
d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar);
return !axisBarDataItems.length;
}
private yAxisEmpty(yAxis: TimeSeriesChartYAxis): boolean {
const axisDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&
d.data.length);
return !axisDataItems.length;
}
private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean {
private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean {
let changed = false;
for (const yAxis of axisList) {
const minInterval = this.yAxisEmpty(yAxis) ? undefined : (1 / Math.pow(10, yAxis.decimals));
if (yAxis.option.minInterval !== minInterval) {
yAxis.option.minInterval = minInterval;
const scaleYAxis = this.scaleYAxis(yAxis);
if (yAxis.option.scale !== scaleYAxis) {
yAxis.option.scale = scaleYAxis;
changed = true;
}
if (yAxis.intervalCalculator) {
const axis = getYAxis(this.timeSeriesChart, yAxis.id);
if (axis) {
try {
const interval = yAxis.intervalCalculator(axis);
if ((yAxis.option as any).interval !== interval) {
(yAxis.option as any).interval = interval;
changed = true;
}
} catch (_e) {}
}
}
}
return changed;
}
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean {
const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&
d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar);
return !axisBarDataItems.length;
}
private minTopOffset(): number {
const showTickLabels =
!!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels);

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html

@ -114,7 +114,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html

@ -104,7 +104,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html

@ -123,7 +123,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>

49
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.html

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2024 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.
-->
<div class="tb-auto-date-format-settings-panel" [formGroup]="autoDateFormatFormGroup">
<div class="tb-auto-date-format-settings-title" translate>date.time-granularity-formats</div>
<div class="tb-auto-date-format-settings-panel-content">
<div class="tb-form-row" *ngFor="let unit of formatTimeUnits">
<div class="fixed-title-width">{{ formatTimeUnitTranslations.get(unit) | translate }}</div>
<div fxFlex fxLayout="column" fxLayoutGap="8px"
fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center" fxLayoutGap.gt-xs="16px">
<mat-form-field class="tb-date-format-input flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput [formControlName]="unit" placeholder="{{ 'widget-config.set' | translate }}">
<div matIconSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
</mat-form-field>
<mat-divider fxHide fxShow.gt-xs vertical></mat-divider>
<div class="preview-text" fxFlex>{{ previewText[unit] }}</div>
</div>
</div>
</div>
<div class="tb-auto-date-format-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyAutoDateFormatSettings()"
[disabled]="autoDateFormatFormGroup.invalid || !autoDateFormatFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

73
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.scss

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2024 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 '../../../../../../../../scss/constants';
.tb-auto-date-format-settings-panel {
width: 620px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-auto-date-format-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-auto-date-format-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
margin: -10px;
padding: 10px;
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
.mat-mdc-form-field.tb-date-format-input {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-flex {
.mat-mdc-form-field-icon-suffix {
display: flex;
align-items: center;
line-height: normal;
}
}
}
}
.preview-text {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.38);
}
}
.tb-auto-date-format-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

92
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings-panel.component.ts

@ -0,0 +1,92 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
AutoDateFormatSettings, defaultAutoDateFormatSettings,
FormatTimeUnit,
formatTimeUnits,
formatTimeUnitTranslations
} from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
@Component({
selector: 'tb-auto-date-format-settings-panel',
templateUrl: './auto-date-format-settings-panel.component.html',
providers: [],
styleUrls: ['./auto-date-format-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AutoDateFormatSettingsPanelComponent extends PageComponent implements OnInit {
formatTimeUnits = formatTimeUnits;
formatTimeUnitTranslations = formatTimeUnitTranslations;
@Input()
autoDateFormatSettings: AutoDateFormatSettings;
@Input()
defaultValues = defaultAutoDateFormatSettings;
@Input()
popover: TbPopoverComponent<AutoDateFormatSettingsPanelComponent>;
@Output()
autoDateFormatSettingsApplied = new EventEmitter<AutoDateFormatSettings>();
autoDateFormatFormGroup: UntypedFormGroup;
previewText: {[unit in FormatTimeUnit]: string} = {} as any;
constructor(private date: DatePipe,
private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.autoDateFormatFormGroup = this.fb.group({});
for (const unit of formatTimeUnits) {
this.autoDateFormatFormGroup.addControl(unit,
this.fb.control(this.autoDateFormatSettings[unit] || this.defaultValues[unit], [Validators.required]));
this.autoDateFormatFormGroup.get(unit).valueChanges.subscribe((value: string) => {
this.previewText[unit] = this.date.transform(Date.now(), value);
});
this.previewText[unit] = this.date.transform(Date.now(), this.autoDateFormatSettings[unit] || this.defaultValues[unit]);
}
}
cancel() {
this.popover?.hide();
}
applyAutoDateFormatSettings() {
const autoDateFormatSettings: AutoDateFormatSettings = this.autoDateFormatFormGroup.value;
for (const unit of formatTimeUnits) {
if (autoDateFormatSettings[unit] === this.defaultValues[unit]) {
delete autoDateFormatSettings[unit];
}
}
this.autoDateFormatSettingsApplied.emit(autoDateFormatSettings);
}
}

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2024 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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openAutoFormatSettingsPopup($event, matButton)">
<tb-icon matButtonIcon>mdi:clock-edit-outline</tb-icon>
</button>

99
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/auto-date-format-settings.component.ts

@ -0,0 +1,99 @@
///
/// Copyright © 2016-2024 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, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AutoDateFormatSettings, defaultAutoDateFormatSettings } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { deepClone, mergeDeep } from '@core/utils';
import {
AutoDateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/auto-date-format-settings-panel.component';
@Component({
selector: 'tb-auto-date-format-settings',
templateUrl: './auto-date-format-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoDateFormatSettingsComponent),
multi: true
}
]
})
export class AutoDateFormatSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
defaultValues = defaultAutoDateFormatSettings;
private modelValue: AutoDateFormatSettings;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: AutoDateFormatSettings): void {
this.modelValue = value;
}
openAutoFormatSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
autoDateFormatSettings: deepClone(this.modelValue),
defaultValues: this.defaultValues
};
const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, AutoDateFormatSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
autoDateFormatSettingsPanelPopover.tbComponentRef.instance.popover = autoDateFormatSettingsPanelPopover;
autoDateFormatSettingsPanelPopover.tbComponentRef.instance.autoDateFormatSettingsApplied.subscribe((autoDateFormatSettings) => {
autoDateFormatSettingsPanelPopover.hide();
this.modelValue = autoDateFormatSettings;
this.propagateChange(this.modelValue);
});
}
}
}

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.html

@ -74,6 +74,10 @@
<div translate>widgets.time-series-chart.axis.tick-labels</div>
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-auto-date-format-settings *ngIf="axisType === 'xAxis'"
formControlName="ticksFormat"
[defaultValues]="defaultXAxisTicksFormat">
</tb-auto-date-format-settings>
<tb-font-settings formControlName="tickLabelFont"
clearButton
disabledLineHeight
@ -126,6 +130,24 @@
formControlName="splitLinesColor">
</tb-color-input>
</div>
<div *ngIf="axisType === 'yAxis'" class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'widgets.time-series-chart.axis.ticks-interval-hint' | translate }}">
{{ 'widgets.time-series-chart.axis.ticks-interval' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="interval"
type="number" min="0" placeholder="{{ 'widgets.time-series-chart.axis.scale-auto' | translate }}">
</mat-form-field>
</div>
<div *ngIf="axisType === 'yAxis'" class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{ 'widgets.time-series-chart.axis.split-number-hint' | translate }}">
{{ 'widgets.time-series-chart.axis.split-number' | translate }}
</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="splitNumber"
type="number" min="1" placeholder="{{ 'widgets.time-series-chart.axis.scale-auto' | translate }}">
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
<div *ngIf="axisType === 'yAxis'" class="tb-form-panel no-border no-padding-top">

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts

@ -23,9 +23,9 @@ import {
Validators
} from '@angular/forms';
import {
AxisPosition,
AxisPosition, defaultXAxisTicksFormat,
timeSeriesAxisPositionTranslations,
TimeSeriesChartAxisSettings,
TimeSeriesChartAxisSettings, TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { merge } from 'rxjs';
@ -58,6 +58,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
defaultXAxisTicksFormat = defaultXAxisTicksFormat;
@Input()
disabled: boolean;
@ -68,7 +70,7 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
@coerceBoolean()
advanced = false;
private modelValue: TimeSeriesChartAxisSettings | TimeSeriesChartYAxisSettings;
private modelValue: TimeSeriesChartXAxisSettings | TimeSeriesChartYAxisSettings;
private propagateChange = null;
@ -103,8 +105,12 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
this.axisSettingsFormGroup.addControl('units', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('decimals', this.fb.control(null, [Validators.min(0)]));
this.axisSettingsFormGroup.addControl('ticksFormatter', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)]));
this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)]));
this.axisSettingsFormGroup.addControl('min', this.fb.control(null, []));
this.axisSettingsFormGroup.addControl('max', this.fb.control(null, []));
} else if (this.axisType === 'xAxis') {
this.axisSettingsFormGroup.addControl('ticksFormat', this.fb.control(null, []));
}
this.axisSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
@ -161,12 +167,18 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu
if (this.axisType === 'yAxis') {
this.axisSettingsFormGroup.get('ticksFormatter').enable({emitEvent: false});
}
if (this.axisType === 'xAxis') {
this.axisSettingsFormGroup.get('ticksFormat').enable({emitEvent: false});
}
} else {
this.axisSettingsFormGroup.get('tickLabelFont').disable({emitEvent: false});
this.axisSettingsFormGroup.get('tickLabelColor').disable({emitEvent: false});
if (this.axisType === 'yAxis') {
this.axisSettingsFormGroup.get('ticksFormatter').disable({emitEvent: false});
}
if (this.axisType === 'xAxis') {
this.axisSettingsFormGroup.get('ticksFormat').disable({emitEvent: false});
}
}
if (showTicks) {
this.axisSettingsFormGroup.get('ticksColor').enable({emitEvent: false});

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html

@ -23,4 +23,9 @@
*ngIf="dateFormatFormControl.value?.custom" matSuffix mat-icon-button (click)="openDateFormatSettingsPopup($event, customFormatButton)">
<tb-icon>edit</tb-icon>
</button>
<button #autoFormatButton
*ngIf="dateFormatFormControl.value?.auto && !dateFormatFormControl.value?.custom"
matSuffix mat-icon-button (click)="openAutoFormatSettingsPopup($event, autoFormatButton)">
<tb-icon>edit</tb-icon>
</button>
</mat-form-field>

49
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts

@ -16,16 +16,26 @@
import { Component, forwardRef, Input, OnInit, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { compareDateFormats, dateFormats, DateFormatSettings } from '@shared/models/widget-settings.models';
import {
AutoDateFormatSettings,
compareDateFormats,
dateFormats,
DateFormatSettings,
dateFormatsWithAuto,
defaultAutoDateFormatSettings
} from '@shared/models/widget-settings.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { deepClone } from '@core/utils';
import { deepClone, mergeDeep } from '@core/utils';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
AutoDateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/auto-date-format-settings-panel.component';
@Component({
selector: 'tb-date-format-select',
@ -51,6 +61,10 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
@coerceBoolean()
excludeLastUpdateAgo = false;
@Input()
@coerceBoolean()
includeAuto = false;
dateFormatList: DateFormatSettings[];
dateFormatsCompare = compareDateFormats;
@ -70,8 +84,9 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
const targetDateFormats = this.includeAuto ? dateFormatsWithAuto : dateFormats;
this.dateFormatList = this.excludeLastUpdateAgo ?
dateFormats.filter(format => !format.lastUpdateAgo) : dateFormats;
targetDateFormats.filter(format => !format.lastUpdateAgo) : dateFormats;
this.dateFormatFormControl = new UntypedFormControl();
this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => {
this.updateModel(value);
@ -116,6 +131,8 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
return this.translate.instant('date.custom-date');
} else if (value.lastUpdateAgo) {
return this.translate.instant('date.last-update-n-ago');
} else if (value.auto) {
return this.translate.instant('date.auto');
} else {
if (!this.formatCache[value.format]) {
this.formatCache[value.format] = this.date.transform(Date.now(), value.format);
@ -148,4 +165,30 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
});
}
}
openAutoFormatSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
autoDateFormatSettings: mergeDeep({} as AutoDateFormatSettings,
defaultAutoDateFormatSettings, this.modelValue.autoDateFormatSettings)
};
const autoDateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, AutoDateFormatSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
autoDateFormatSettingsPanelPopover.tbComponentRef.instance.popover = autoDateFormatSettingsPanelPopover;
autoDateFormatSettingsPanelPopover.tbComponentRef.instance.autoDateFormatSettingsApplied.subscribe((autoDateFormatSettings) => {
autoDateFormatSettingsPanelPopover.hide();
this.modelValue.autoDateFormatSettings = autoDateFormatSettings;
this.propagateChange(this.modelValue);
});
}
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -121,6 +121,12 @@ import {
import {
TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
import {
AutoDateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/auto-date-format-settings-panel.component';
import {
AutoDateFormatSettingsComponent
} from '@home/components/widget/lib/settings/common/auto-date-format-settings.component';
@NgModule({
declarations: [
@ -134,6 +140,8 @@ import {
CssSizeInputComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent,
AutoDateFormatSettingsComponent,
AutoDateFormatSettingsPanelComponent,
BackgroundSettingsComponent,
BackgroundSettingsPanelComponent,
ValueSourceComponent,
@ -185,6 +193,8 @@ import {
CssSizeInputComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent,
AutoDateFormatSettingsComponent,
AutoDateFormatSettingsPanelComponent,
BackgroundSettingsComponent,
BackgroundSettingsPanelComponent,
ValueSourceComponent,

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

@ -120,8 +120,9 @@ export class EntityLinkTableColumn<T extends BaseData<HasId>> extends BaseEntity
public title: string,
public width: string = '0px',
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property] ? entity[property] : '',
public entityURL: (entity) => string) {
super('link', key, title, width, false);
public entityURL: (entity) => string,
public sortable: boolean = true) {
super('link', key, title, width, sortable);
}
}

14
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html

@ -30,7 +30,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.WEB"
*ngIf="templateConfigurationForm.get('WEB.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.WEB) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }}</tb-icon>
@ -83,7 +83,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.MOBILE_APP"
*ngIf="templateConfigurationForm.get('MOBILE_APP.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.MOBILE_APP) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }}</tb-icon>
@ -138,7 +138,7 @@
<tb-notification-action-button-configuration
formControlName="onClick"
hideButtonText
[sliderHint]="hotificationTapActionHint"
[sliderHint]="notificationTapActionHint"
actionTitle="{{ 'notification.notification-tap-action' | translate }}">
</tb-notification-action-button-configuration>
</div>
@ -148,7 +148,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.SMS"
*ngIf="templateConfigurationForm.get('SMS.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.SMS) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }}</tb-icon>
@ -180,7 +180,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.EMAIL"
*ngIf="templateConfigurationForm.get('EMAIL.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.EMAIL) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }}</tb-icon>
@ -210,7 +210,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.SLACK"
*ngIf="templateConfigurationForm.get('SLACK.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.SLACK) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }}</tb-icon>
@ -236,7 +236,7 @@
<section class="tb-form-panel tb-slide-toggle stroked"
[formGroupName]="NotificationDeliveryMethod.MICROSOFT_TEAMS"
*ngIf="templateConfigurationForm.get('MICROSOFT_TEAMS.enabled').value">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel class="tb-settings" expanded="{{ expandedForm(NotificationDeliveryMethod.MICROSOFT_TEAMS) }}">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title class="template-tittle">
<tb-icon>{{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }}</tb-icon>

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

@ -66,6 +66,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
if (isDefinedAndNotNull(value)) {
this.templateConfigurationForm.patchValue(value, {emitEvent: false});
this.updateDisabledForms();
this.updateExpandedForm();
this.templateConfigurationForm.updateValueAndValidity();
}
}
@ -96,6 +97,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
private propagateChange = (v: any) => { };
private readonly destroy$ = new Subject<void>();
private expendedBlocks: NotificationDeliveryMethod[];
constructor(private fb: FormBuilder,
private translate: TranslateService) {
@ -122,6 +124,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
}
}
this.templateConfigurationForm.patchValue(settings, {emitEvent: false});
this.updateExpandedForm();
}
registerOnChange(fn: any): void {
@ -147,7 +150,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
};
}
get hotificationTapActionHint(): string {
get notificationTapActionHint(): string {
switch (this.notificationType) {
case NotificationType.ALARM:
case NotificationType.ALARM_ASSIGNMENT:
@ -157,6 +160,19 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
return '';
}
expandedForm(name: NotificationDeliveryMethod): boolean {
return this.expendedBlocks.includes(name);
}
private updateExpandedForm() {
this.expendedBlocks = [];
Object.keys(this.templateConfigurationForm.controls).forEach((name: NotificationDeliveryMethod) => {
if (this.templateConfigurationForm.get(name).invalid) {
this.expendedBlocks.push(name);
}
});
}
private updateDisabledForms(){
Object.values(NotificationDeliveryMethod).forEach((method) => {
const form = this.templateConfigurationForm.get(method);

14
ui-ngx/src/app/shared/components/dialog/alert-dialog.component.ts

@ -16,6 +16,10 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DialogComponent } from '@shared/components/dialog.component';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
export interface AlertDialogData {
title: string;
@ -29,7 +33,11 @@ export interface AlertDialogData {
templateUrl: './alert-dialog.component.html',
styleUrls: ['./alert-dialog.component.scss']
})
export class AlertDialogComponent {
constructor(public dialogRef: MatDialogRef<AlertDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: AlertDialogData) {}
export class AlertDialogComponent extends DialogComponent<AlertDialogComponent, boolean>{
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<AlertDialogComponent, boolean>,
@Inject(MAT_DIALOG_DATA) public data: AlertDialogData) {
super(store, router, dialogRef);
}
}

14
ui-ngx/src/app/shared/components/dialog/confirm-dialog.component.ts

@ -16,6 +16,10 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DialogComponent } from '@shared/components/dialog.component';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
export interface ConfirmDialogData {
title: string;
@ -30,7 +34,11 @@ export interface ConfirmDialogData {
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent {
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData) {}
export class ConfirmDialogComponent extends DialogComponent<ConfirmDialogComponent, boolean>{
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData) {
super(store, router, dialogRef);
}
}

11
ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts

@ -16,6 +16,10 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { DialogComponent } from '@shared/components/dialog.component';
export interface ErrorAlertDialogData {
title: string;
@ -29,15 +33,18 @@ export interface ErrorAlertDialogData {
templateUrl: './error-alert-dialog.component.html',
styleUrls: ['./error-alert-dialog.component.scss']
})
export class ErrorAlertDialogComponent {
export class ErrorAlertDialogComponent extends DialogComponent<ErrorAlertDialogComponent, boolean>{
title: string;
message: string;
errorMessage: string;
errorDetails?: string;
constructor(public dialogRef: MatDialogRef<ErrorAlertDialogComponent>,
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<ErrorAlertDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ErrorAlertDialogData) {
super(store, router, dialogRef);
this.title = this.data.title;
this.message = this.data.message;
this.errorMessage = this.data.error.message ? this.data.error.message : JSON.stringify(this.data.error);

157
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, parseFunction } from '@core/utils';
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, mergeDeep, parseFunction } from '@core/utils';
import {
DataEntry,
DataKey,
@ -34,6 +34,8 @@ import { Observable, of } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { map } from 'rxjs/operators';
import { DomSanitizer } from '@angular/platform-browser';
import { AVG_MONTH, DAY, HOUR, Interval, IntervalMath, MINUTE, SECOND, YEAR } from '@shared/models/time/time.models';
import moment from 'moment';
export type ComponentStyle = {[klass: string]: any};
@ -323,35 +325,85 @@ class FunctionColorProcessor extends ColorProcessor {
}
}
export type FormatTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour'
| 'day' | 'month' | 'year';
export const formatTimeUnits: FormatTimeUnit[] = [
'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'
];
export const formatTimeUnitTranslations = new Map<FormatTimeUnit, string>(
[
['year', 'date.unit-year'],
['month', 'date.unit-month'],
['day', 'date.unit-day'],
['hour', 'date.unit-hour'],
['minute', 'date.unit-minute'],
['second', 'date.unit-second'],
['millisecond', 'date.unit-millisecond']
]
);
export type AutoDateFormatSettings = {
[unit in FormatTimeUnit]?: string;
};
export interface DateFormatSettings {
format?: string;
lastUpdateAgo?: boolean;
custom?: boolean;
auto?: boolean;
hideLastUpdatePrefix?: boolean;
autoDateFormatSettings?: AutoDateFormatSettings;
}
export const simpleDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: false
custom: false,
auto: false
});
export const lastUpdateAgoDateFormat = (): DateFormatSettings => ({
format: null,
lastUpdateAgo: true,
custom: false
custom: false,
auto: false
});
export const customDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: true
custom: true,
auto: false
});
export const defaultAutoDateFormatSettings: AutoDateFormatSettings = {
millisecond: 'MMM dd yyyy HH:mm:ss.SSS',
second: 'MMM dd yyyy HH:mm:ss',
minute: 'MMM dd yyyy HH:mm',
hour: 'MMM dd yyyy HH:mm',
day: 'MMM dd yyyy',
month: 'MMM yyyy',
year: 'yyyy'
};
export const autoDateFormat = (): DateFormatSettings => ({
format: null,
lastUpdateAgo: false,
custom: false,
auto: true,
autoDateFormatSettings: {}
});
export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'dd MMM yyyy HH:mm:ss', 'yyyy MMM dd HH:mm',
'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'yyyy/MM/dd HH:mm:ss', 'yyyy-MM-dd HH:mm:ss', 'yyyy-MM-dd HH:mm:ss.SSS']
export const dateFormats = ['MMM yyyy', 'MMM dd yyyy', 'MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'dd MMM yyyy HH:mm:ss',
'yyyy MMM dd HH:mm', 'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'MMM dd yyyy HH:mm:ss', 'yyyy/MM/dd HH:mm:ss', 'yyyy-MM-dd HH:mm:ss',
'MMM dd yyyy HH:mm:ss.SSS', 'yyyy-MM-dd HH:mm:ss.SSS']
.map(f => simpleDateFormat(f)).concat([lastUpdateAgoDateFormat(), customDateFormat('EEE, MMMM dd, yyyy')]);
export const dateFormatsWithAuto = [autoDateFormat()].concat(dateFormats);
export const compareDateFormats = (df1: DateFormatSettings, df2: DateFormatSettings): boolean => {
if (df1 === df2) {
return true;
@ -360,7 +412,9 @@ export const compareDateFormats = (df1: DateFormatSettings, df2: DateFormatSetti
return true;
} else if (df1.custom && df2.custom) {
return true;
} else if (!df1.lastUpdateAgo && !df2.lastUpdateAgo && !df1.custom && !df2.custom) {
} else if (df1.auto && df2.auto && !df1.custom && !df2.custom) {
return true;
} else if (!df1.lastUpdateAgo && !df2.lastUpdateAgo && !df1.custom && !df2.custom && !df1.auto && !df2.auto) {
return df1.format === df2.format;
}
}
@ -372,6 +426,8 @@ export abstract class DateFormatProcessor {
static fromSettings($injector: Injector, settings: DateFormatSettings): DateFormatProcessor {
if (settings.lastUpdateAgo) {
return new LastUpdateAgoDateFormatProcessor($injector, settings);
} else if (settings.auto && !settings.custom) {
return new AutoDateFormatProcessor($injector, settings);
} else {
return new SimpleDateFormatProcessor($injector, settings);
}
@ -383,7 +439,7 @@ export abstract class DateFormatProcessor {
protected settings: DateFormatSettings) {
}
abstract update(ts: string | number | Date): string;
abstract update(ts: string | number | Date, interval?: Interval): string;
}
@ -437,6 +493,91 @@ export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
}
export class AutoDateFormatProcessor extends DateFormatProcessor {
private datePipe: DatePipe;
private readonly autoDateFormatSettings: AutoDateFormatSettings;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.datePipe = $injector.get(DatePipe);
this.autoDateFormatSettings = mergeDeep({} as AutoDateFormatSettings,
defaultAutoDateFormatSettings, this.settings.autoDateFormatSettings);
}
update(ts: string| number | Date, interval?: Interval): string {
if (ts) {
const unit = interval ? intervalToFormatTimeUnit(interval) : tsToFormatTimeUnit(ts);
const format = this.autoDateFormatSettings[unit];
if (format) {
this.formatted = this.datePipe.transform(ts, format);
} else {
this.formatted = '&nbsp;';
}
} else {
this.formatted = '&nbsp;';
}
return this.formatted;
}
}
const intervalToFormatTimeUnit = (interval: Interval): FormatTimeUnit => {
const intervalValue = IntervalMath.numberValue(interval);
if (intervalValue < SECOND) {
return 'millisecond';
} else if (intervalValue < MINUTE) {
return 'second';
} else if (intervalValue < HOUR) {
return 'minute';
} else if (intervalValue < DAY) {
return 'hour';
} else if (intervalValue < AVG_MONTH) {
return 'day';
} else if (intervalValue < YEAR) {
return 'month';
} else {
return 'year';
}
};
export const tsToFormatTimeUnit = (ts: string | number | Date): FormatTimeUnit => {
const date = moment(ts);
const M = date.month() + 1;
const d = date.date();
const h = date.hours();
const m = date.minutes();
const s = date.seconds();
const S = date.milliseconds();
const isSecond = S === 0;
const isMinute = isSecond && s === 0;
const isHour = isMinute && m === 0;
const isDay = isHour && h === 0;
const isMonth = isDay && d === 1;
const isYear = isMonth && M === 1;
if (isYear) {
return 'year';
}
else if (isMonth) {
return 'month';
}
else if (isDay) {
return 'day';
}
else if (isHour) {
return 'hour';
}
else if (isMinute) {
return 'minute';
}
else if (isSecond) {
return 'second';
}
else {
return 'millisecond';
}
};
export enum BackgroundType {
image = 'image',
color = 'color'

14
ui-ngx/src/assets/dashboard/sys_admin_home_page.json

@ -2023,6 +2023,7 @@
"lineHeight": "1"
},
"tickLabelColor": "rgba(0, 0, 0, 0.54)",
"ticksFormat": {},
"showTicks": false,
"ticksColor": "rgba(0, 0, 0, 0.54)",
"showLine": false,
@ -2076,9 +2077,11 @@
"tooltipValueColor": "rgba(0, 0, 0, 0.76)",
"tooltipShowDate": true,
"tooltipDateFormat": {
"format": "MMMM dd, yyyy",
"format": null,
"lastUpdateAgo": false,
"custom": true
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",
@ -2494,6 +2497,7 @@
"lineHeight": "1"
},
"tickLabelColor": "rgba(0, 0, 0, 0.54)",
"ticksFormat": {},
"showTicks": false,
"ticksColor": "rgba(0, 0, 0, 0.54)",
"showLine": false,
@ -2547,9 +2551,11 @@
"tooltipValueColor": "rgba(0, 0, 0, 0.76)",
"tooltipShowDate": true,
"tooltipDateFormat": {
"format": "dd MMM yyyy HH:mm:ss",
"format": null,
"lastUpdateAgo": false,
"custom": false
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",

14
ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

@ -671,6 +671,7 @@
"lineHeight": "1"
},
"tickLabelColor": "rgba(0, 0, 0, 0.54)",
"ticksFormat": {},
"showTicks": false,
"ticksColor": "rgba(0, 0, 0, 0.54)",
"showLine": false,
@ -724,9 +725,11 @@
"tooltipValueColor": "rgba(0, 0, 0, 0.76)",
"tooltipShowDate": true,
"tooltipDateFormat": {
"format": "MMMM dd, yyyy",
"format": null,
"lastUpdateAgo": false,
"custom": true
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",
@ -997,6 +1000,7 @@
"lineHeight": "1"
},
"tickLabelColor": "rgba(0, 0, 0, 0.54)",
"ticksFormat": {},
"showTicks": false,
"ticksColor": "rgba(0, 0, 0, 0.54)",
"showLine": false,
@ -1050,9 +1054,11 @@
"tooltipValueColor": "rgba(0, 0, 0, 0.76)",
"tooltipShowDate": true,
"tooltipDateFormat": {
"format": "MMMM dd, yyyy",
"format": null,
"lastUpdateAgo": false,
"custom": true
"custom": false,
"auto": true,
"autoDateFormatSettings": {}
},
"tooltipDateFont": {
"family": "Roboto",

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

@ -1025,7 +1025,16 @@
"last-update-n-ago-text": "Last update {{ agoText }}",
"custom-date": "Custom date",
"format": "Format",
"preview": "Preview"
"preview": "Preview",
"auto": "Auto",
"time-granularity-formats": "Time granularity formats",
"unit-year": "Years",
"unit-month": "Months",
"unit-day": "Days",
"unit-hour": "Hours",
"unit-minute": "Minutes",
"unit-second": "Seconds",
"unit-millisecond": "Milliseconds"
},
"datetime": {
"date-from": "Date from",
@ -6691,6 +6700,10 @@
"show-split-lines": "Show split lines",
"show-split-lines-x-axis-hint": "If enabled, the vertical lines on the chart will be shown.",
"show-split-lines-y-axis-hint": "If enabled, the horizontal lines on the chart will be shown.",
"ticks-interval": "Ticks interval",
"ticks-interval-hint": "Compulsively set segmentation interval for axis.",
"split-number": "Split number",
"split-number-hint": "Number of segments that the axis is split into.",
"scale": "Scale",
"scale-min": "min",
"scale-max": "max",

4060
ui-ngx/yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save