diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
new file mode 100644
index 0000000000..93c496f2e3
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
@@ -0,0 +1,970 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg
new file mode 100644
index 0000000000..b3f6488f3e
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg
new file mode 100644
index 0000000000..9e44380509
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg
new file mode 100644
index 0000000000..c1bef78aa5
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg
@@ -0,0 +1,611 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/cross-pipe.svg b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg
new file mode 100644
index 0000000000..bfed29df3a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg
@@ -0,0 +1,1047 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg
new file mode 100644
index 0000000000..da140b4383
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
new file mode 100644
index 0000000000..8b34deaffd
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
@@ -0,0 +1,948 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg
new file mode 100644
index 0000000000..d2ddc67657
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg
@@ -0,0 +1,422 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg
new file mode 100644
index 0000000000..a96f61781d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg
@@ -0,0 +1,300 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg
new file mode 100644
index 0000000000..cd9b5f1b7a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
new file mode 100644
index 0000000000..2a5c3ce47a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
@@ -0,0 +1,166 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
new file mode 100644
index 0000000000..2f91847b71
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
@@ -0,0 +1,181 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
new file mode 100644
index 0000000000..b1da702871
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
@@ -0,0 +1,964 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg
new file mode 100644
index 0000000000..5be0a454e5
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg
@@ -0,0 +1,526 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg
new file mode 100644
index 0000000000..a0e01f639b
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg
@@ -0,0 +1,866 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg
new file mode 100644
index 0000000000..276f3aa004
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
new file mode 100644
index 0000000000..d9defd33d9
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg
new file mode 100644
index 0000000000..ffc77b0ac0
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg
@@ -0,0 +1,487 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg
new file mode 100644
index 0000000000..a63b679f0a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg
new file mode 100644
index 0000000000..7f4e30edad
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg
@@ -0,0 +1,484 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
new file mode 100644
index 0000000000..3fe3501e16
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
@@ -0,0 +1,167 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
new file mode 100644
index 0000000000..14ede98e81
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
@@ -0,0 +1,178 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
new file mode 100644
index 0000000000..e5df51e561
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
@@ -0,0 +1,964 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg
new file mode 100644
index 0000000000..7ada9a91e3
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg
@@ -0,0 +1,526 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg
new file mode 100644
index 0000000000..4c84999b1f
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
new file mode 100644
index 0000000000..b3235c5906
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
new file mode 100644
index 0000000000..ffe74a681e
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
@@ -0,0 +1,160 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
new file mode 100644
index 0000000000..306898e3ae
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
@@ -0,0 +1,160 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg
new file mode 100644
index 0000000000..d50eada2aa
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg
new file mode 100644
index 0000000000..85e617ff22
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg
new file mode 100644
index 0000000000..7b9b1c260d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
new file mode 100644
index 0000000000..bac39d98dc
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
@@ -0,0 +1,965 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg
new file mode 100644
index 0000000000..5537c23bb6
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg
new file mode 100644
index 0000000000..1dfa27e706
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
new file mode 100644
index 0000000000..3aee266486
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
new file mode 100644
index 0000000000..1c22c8d117
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
@@ -0,0 +1,949 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg
new file mode 100644
index 0000000000..18ea6837e0
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg
@@ -0,0 +1,422 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg
new file mode 100644
index 0000000000..57d4d1978a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg
@@ -0,0 +1,1394 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg
new file mode 100644
index 0000000000..d43083040f
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg
@@ -0,0 +1,300 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/scada_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_symbols.json
new file mode 100644
index 0000000000..f1fbfbc297
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/scada_symbols.json
@@ -0,0 +1,14 @@
+{
+ "widgetsBundle": {
+ "alias": "scada_symbols",
+ "title": "SCADA symbols",
+ "scada": true,
+ "image": null,
+ "description": "Bundle with SCADA symbols",
+ "order": 9200,
+ "name": "SCADA symbols"
+ },
+ "widgetTypeFqns": [
+ "scada_symbol"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
new file mode 100644
index 0000000000..224938c019
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
@@ -0,0 +1,52 @@
+{
+ "widgetsBundle": {
+ "alias": "scada_water_system_symbols",
+ "title": "SCADA water system symbols",
+ "scada": true,
+ "image": null,
+ "description": "Bundle with SCADA symbols for water system",
+ "order": 9300,
+ "name": "SCADA water system symbols"
+ },
+ "widgetTypeFqns": [
+ "horizontal_pipe",
+ "long_horizontal_pipe",
+ "vertical_pipe",
+ "long_vertical_pipe",
+ "left_bottom_elbow_pipe",
+ "bottom_right_elbow_pipe",
+ "top_right_elbow_pipe",
+ "left_top_elbow_pipe",
+ "cross_pipe",
+ "left_tee_pipe",
+ "bottom_tee_pipe",
+ "right_tee_pipe",
+ "top_tee_pipe",
+ "right_elbow_drain_pipe",
+ "left_elbow_drain_pipe",
+ "left_drain_pipe",
+ "right_drain_pipe",
+ "short_left_drain_pipe",
+ "short_right_drain_pipe",
+ "top_flow_meter",
+ "right_flow_meter",
+ "bottom_flow_meter",
+ "left_flow_meter",
+ "horizontal_inline_flow_meter",
+ "vertical_inline_flow_meter",
+ "centrifugal_pump",
+ "small_right_motor_pump",
+ "small_left_motor_pump",
+ "right_motor_pump",
+ "left_motor_pump",
+ "short_bottom_filter",
+ "long_bottom_filter",
+ "short_top_filter",
+ "long_top_filter",
+ "horizontal_wheel_valve",
+ "vertical_wheel_valve",
+ "horizontal_ball_valve",
+ "vertical_ball_valve",
+ "vertical_tank"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/power_button.json b/application/src/main/data/json/system/widget_types/power_button.json
index 821873f0b4..30a3facd7b 100644
--- a/application/src/main/data/json/system/widget_types/power_button.json
+++ b/application/src/main/data/json/system/widget_types/power_button.json
@@ -2,7 +2,7 @@
"fqn": "power_button",
"name": "Power button",
"deprecated": false,
- "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=",
+ "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=",
"description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when power on/off states.",
"descriptor": {
"type": "rpc",
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-power-button-widget-settings",
diff --git a/application/src/main/data/json/system/widget_types/scada_symbol.json b/application/src/main/data/json/system/widget_types/scada_symbol.json
new file mode 100644
index 0000000000..15c12e1b81
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/scada_symbol.json
@@ -0,0 +1,24 @@
+{
+ "fqn": "scada_symbol",
+ "name": "SCADA symbol",
+ "deprecated": false,
+ "scada": true,
+ "image": null,
+ "description": "",
+ "descriptor": {
+ "type": "rpc",
+ "sizeX": 3,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "",
+ "dataKeySettingsSchema": "{}\n",
+ "settingsDirective": "tb-scada-symbol-widget-settings",
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-scada-symbol-basic-config",
+ "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"imageUrl\":null,\"color\":\"rgb(255, 255, 255)\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"scadaSymbolUrl\":\"\",\"scadaSymbolObjectSettings\":{\"behavior\":{},\"properties\":{}}},\"title\":\"SCADA symbol\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":null,\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":\"0px\",\"margin\":\"0px\"}"
+ },
+ "tags": null
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/toggle_button.json b/application/src/main/data/json/system/widget_types/toggle_button.json
index f3f3449b13..e756b31e0f 100644
--- a/application/src/main/data/json/system/widget_types/toggle_button.json
+++ b/application/src/main/data/json/system/widget_types/toggle_button.json
@@ -2,7 +2,7 @@
"fqn": "toggle_button",
"name": "Toggle button",
"deprecated": false,
- "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K",
+ "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K",
"description": "Sends the command to the device or updates attribute/time series when the user toggles the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when button checked/unchecked states.",
"descriptor": {
"type": "rpc",
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-toggle-button-widget-settings",
diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql
index afbd86fee7..51e78a504e 100644
--- a/application/src/main/data/upgrade/3.7.0/schema_update.sql
+++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql
@@ -14,6 +14,55 @@
-- limitations under the License.
--
+-- UPDATE RESOURCE SUB TYPE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'resource' AND column_name = 'resource_sub_type'
+ ) THEN
+ ALTER TABLE resource ADD COLUMN resource_sub_type varchar(32);
+ UPDATE resource SET resource_sub_type = 'IMAGE' WHERE resource_type = 'IMAGE';
+ END IF;
+ END;
+$$;
+
+-- UPDATE RESOURCE SUB TYPE END
+
+-- UPDATE WIDGETS BUNDLE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'widgets_bundle' AND column_name = 'scada'
+ ) THEN
+ ALTER TABLE widgets_bundle ADD COLUMN scada boolean NOT NULL DEFAULT false;
+ END IF;
+ END;
+$$;
+
+-- UPDATE WIDGETS BUNDLE END
+
+-- UPDATE WIDGET TYPE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'widget_type' AND column_name = 'scada'
+ ) THEN
+ ALTER TABLE widget_type ADD COLUMN scada boolean NOT NULL DEFAULT false;
+ END IF;
+ END;
+$$;
+
+-- UPDATE WIDGET TYPE END
+
-- KV VERSIONING UPDATE START
CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1;
diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
index 011119886d..4d83d4debb 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -117,6 +117,8 @@ public class ControllerConstants {
protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
+ protected static final String RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION = "A string value representing resource sub-type.";
+
protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority.";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
index e8e7ca1d8f..6ebb6fffe8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
@@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
@@ -65,6 +66,7 @@ import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
@@ -96,7 +98,8 @@ public class ImageController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/api/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
- @RequestPart(required = false) String title) throws Exception {
+ @RequestPart(required = false) String title,
+ @RequestPart(required = false) String imageSubType) throws Exception {
SecurityUser user = getCurrentUser();
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
@@ -109,7 +112,14 @@ public class ImageController extends BaseController {
} else {
image.setTitle(file.getOriginalFilename());
}
+
+ ResourceSubType subType = ResourceSubType.IMAGE;
+ if (StringUtils.isNotEmpty(imageSubType)) {
+ subType = ResourceSubType.valueOf(imageSubType);
+ }
+
image.setResourceType(ResourceType.IMAGE);
+ image.setResourceSubType(subType);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(file.getContentType());
image.setDescriptorValue(descriptor);
@@ -194,6 +204,7 @@ public class ImageController extends BaseController {
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
+ .subType(imageInfo.getResourceSubType().name())
.resourceKey(imageInfo.getResourceKey())
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
@@ -215,6 +226,11 @@ public class ImageController extends BaseController {
} else {
image.setTitle(imageData.getFileName());
}
+ if (StringUtils.isNotEmpty(imageData.getSubType())) {
+ image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType()));
+ } else {
+ image.setResourceSubType(ResourceSubType.IMAGE);
+ }
image.setResourceType(ResourceType.IMAGE);
image.setResourceKey(imageData.getResourceKey());
image.setPublic(imageData.isPublic());
@@ -251,6 +267,8 @@ public class ImageController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
+ @Parameter(description = RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"IMAGE", "SCADA_SYMBOL"}))
+ @RequestParam(required = false) String imageSubType,
@Parameter(description = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION)
@RequestParam(required = false) boolean includeSystemImages,
@Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@@ -262,10 +280,14 @@ public class ImageController extends BaseController {
// PE: generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TenantId tenantId = getTenantId();
+ ResourceSubType subType = ResourceSubType.IMAGE;
+ if (StringUtils.isNotEmpty(imageSubType)) {
+ subType = ResourceSubType.valueOf(imageSubType);
+ }
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) {
- return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink));
+ return checkNotNull(imageService.getImagesByTenantId(tenantId, subType, pageLink));
} else {
- return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink));
+ return checkNotNull(imageService.getAllImagesByTenantId(tenantId, subType, pageLink));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 649a58f22e..03f1c52faf 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -41,8 +41,10 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.ImageService;
@@ -83,6 +85,7 @@ public class WidgetTypeController extends AutoCommitController {
private static final String WIDGET_TYPE_INFO_DESCRIPTION = "Widget Type Info is a lightweight object that represents Widget Type but does not contain the heavyweight widget descriptor JSON";
private static final String TENANT_ONLY_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether only tenant widget types should be returned";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether search widgets by description not only by name";
+ private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch SCADA symbol widgets first";
private static final String DEPRECATED_FILTER_PARAM_DESCRIPTION = "Optional string parameter indicating whether to include deprecated widgets";
private static final String UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to update existing widget type by FQN if present instead of creating new one";
private static final String WIDGET_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the widget type value";
@@ -186,18 +189,26 @@ public class WidgetTypeController extends AutoCommitController {
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"timeseries", "latest", "control", "alarm", "static"})))
- @RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
+ @RequestParam(required = false) String[] widgetTypeList,
+ @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION)
+ @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
- boolean fullSearchBool = fullSearch != null && fullSearch;
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
+ WidgetTypeFilter widgetTypeFilter = WidgetTypeFilter.builder()
+ .tenantId(getTenantId())
+ .widgetTypes(widgetTypes)
+ .deprecatedFilter(widgetTypeDeprecatedFilter)
+ .fullSearch(fullSearch != null && fullSearch)
+ .scadaFirst(scadaFirst != null && scadaFirst)
+ .build();
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
- return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(widgetTypeFilter, pageLink));
} else {
if (tenantOnly != null && tenantOnly) {
- return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink));
} else {
- return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink));
}
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index ecd63561f4..8d39e6c83d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@@ -72,6 +73,7 @@ public class WidgetsBundleController extends BaseController {
private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. ";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types";
+ private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch widgets bundles with SCADA symbols first. Works only when fullSearch parameter is enabled";
private static final String TENANT_BUNDLES_ONLY_DESCRIPTION = "Optional boolean parameter to include only tenant-level bundles without system";
@ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)",
@@ -198,16 +200,22 @@ public class WidgetsBundleController extends BaseController {
@Parameter(description = TENANT_BUNDLES_ONLY_DESCRIPTION)
@RequestParam(required = false) Boolean tenantOnly,
@Parameter(description = FULL_SEARCH_PARAM_DESCRIPTION)
- @RequestParam(required = false) Boolean fullSearch) throws ThingsboardException {
+ @RequestParam(required = false) Boolean fullSearch,
+ @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION)
+ @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ WidgetsBundleFilter widgetsBundleFilter = WidgetsBundleFilter.builder()
+ .tenantId(getTenantId())
+ .fullSearch(fullSearch != null && fullSearch)
+ .scadaFirst(scadaFirst != null && scadaFirst)
+ .build();
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
- return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(widgetsBundleFilter, pageLink));
} else {
- TenantId tenantId = getCurrentUser().getTenantId();
if (tenantOnly != null && tenantOnly) {
- return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink));
} else {
- return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink));
}
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
index 7e957fd0fe..2cf47c6abd 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -32,6 +33,12 @@ public class SystemWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
@Override
protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) {
- return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
+ return widgetTypeService.findSystemWidgetTypesByPageLink(
+ WidgetTypeFilter.builder()
+ .tenantId(tenantId)
+ .fullSearch(false)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ pageLink);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
index 5ec2af5fb2..1e6a345db7 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@Slf4j
@@ -31,6 +32,6 @@ public class SystemWidgetsBundlesEdgeEventFetcher extends BaseWidgetsBundlesEdge
@Override
protected PageData findWidgetsBundles(TenantId tenantId, PageLink pageLink) {
- return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
+ return widgetsBundleService.findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
index c2a971a0c0..62fac80fbf 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -31,6 +32,12 @@ public class TenantWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
}
@Override
protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) {
- return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
+ return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(
+ WidgetTypeFilter.builder()
+ .tenantId(tenantId)
+ .fullSearch(false)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ pageLink);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
index 9db2a99e7f..7f76ce860f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
@@ -26,8 +27,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.ImageDescriptor;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -43,8 +48,10 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
@@ -84,12 +91,14 @@ public class InstallScripts {
public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_TYPES_DIR = "widget_types";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
+ public static final String SCADA_SYMBOLS_DIR = "scada_symbols";
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials";
public static final String JSON_EXT = ".json";
+ public static final String SVG_EXT = ".svg";
public static final String XML_EXT = ".xml";
@Value("${install.data_dir:}")
@@ -118,6 +127,9 @@ public class InstallScripts {
@Getter @Setter
private boolean updateImages = false;
+ @Autowired
+ private ImageService imageService;
+
Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
@@ -253,6 +265,7 @@ public class InstallScripts {
);
}
}
+ this.loadSystemScadaSymbols();
for (var widgetsBundleDescriptorEntry : widgetsBundlesMap.entrySet()) {
Path path = widgetsBundleDescriptorEntry.getKey();
try {
@@ -290,6 +303,94 @@ public class InstallScripts {
}
}
+ private void loadSystemScadaSymbols() throws Exception {
+ log.info("Loading system SCADA symbols");
+ Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
+ if (Files.exists(scadaSymbolsDir)) {
+ WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol");
+ try (DirectoryStream dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) {
+ dirStream.forEach(
+ path -> {
+ try {
+ var fileName = path.getFileName().toString();
+ var scadaSymbolData = Files.readAllBytes(path);
+ var metadata = ImageUtils.processScadaSymbolMetadata(fileName, scadaSymbolData);
+ TbResourceInfo savedScadaSymbol = saveScadaSymbol(metadata, fileName, scadaSymbolData);
+ if (scadaSymbolWidgetTemplate != null) {
+ saveScadaSymbolWidget(scadaSymbolWidgetTemplate, savedScadaSymbol, metadata);
+ }
+ } catch (Exception e) {
+ log.error("Unable to load SCADA symbol from file: [{}]", path.toString());
+ throw new RuntimeException("Unable to load SCADA symbol from file", e);
+ }
+ }
+ );
+ }
+ }
+ }
+
+ private TbResourceInfo saveScadaSymbol(ImageUtils.ScadaSymbolMetadataInfo metadata, String fileName, byte[] scadaSymbolData) {
+ String etag = imageService.calculateImageEtag(scadaSymbolData);
+ var existingImage = imageService.findSystemOrTenantImageByEtag(TenantId.SYS_TENANT_ID, etag);
+ if (existingImage != null && ResourceSubType.SCADA_SYMBOL.equals(existingImage.getResourceSubType())) {
+ return existingImage;
+ } else {
+ var existing = imageService.getImageInfoByTenantIdAndKey(TenantId.SYS_TENANT_ID, fileName);
+ if (existing != null && ResourceSubType.SCADA_SYMBOL.equals(existing.getResourceSubType())) {
+ imageService.deleteImage(existing, true);
+ }
+ TbResource image = new TbResource();
+ image.setTenantId(TenantId.SYS_TENANT_ID);
+ image.setFileName(fileName);
+ image.setTitle(metadata.getTitle());
+ image.setResourceSubType(ResourceSubType.SCADA_SYMBOL);
+ image.setResourceType(ResourceType.IMAGE);
+ image.setPublic(true);
+ ImageDescriptor descriptor = new ImageDescriptor();
+ descriptor.setMediaType("image/svg+xml");
+ image.setDescriptorValue(descriptor);
+ image.setData(scadaSymbolData);
+ return imageService.saveImage(image);
+ }
+ }
+
+ private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
+ ImageUtils.ScadaSymbolMetadataInfo metadata) {
+ String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
+ WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
+ JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
+ scadaSymbolWidget.setDescriptor(descriptor);
+ scadaSymbolWidget.setName(metadata.getTitle());
+ scadaSymbolWidget.setImage(symbolUrl);
+ scadaSymbolWidget.setDescription(metadata.getDescription());
+ scadaSymbolWidget.setTags(metadata.getSearchTags());
+ scadaSymbolWidget.setScada(true);
+ ObjectNode defaultConfig = null;
+ if (descriptor.has("defaultConfig")) {
+ defaultConfig = JacksonUtil.fromString(descriptor.get("defaultConfig").asText(), ObjectNode.class);
+ }
+ if (defaultConfig == null) {
+ defaultConfig = JacksonUtil.newObjectNode();
+ }
+ defaultConfig.put("title", metadata.getTitle());
+ ObjectNode settings;
+ if (defaultConfig.has("settings")) {
+ settings = (ObjectNode)defaultConfig.get("settings");
+ } else {
+ settings = JacksonUtil.newObjectNode();
+ defaultConfig.set("settings", settings);
+ }
+ settings.put("scadaSymbolUrl", symbolUrl);
+ ((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
+ ((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX());
+ ((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY());
+ String controllerScript = descriptor.get("controllerScript").asText();
+ controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
+ controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'");
+ ((ObjectNode)descriptor).put("controllerScript", controllerScript);
+ return widgetTypeService.saveWidgetType(scadaSymbolWidget);
+ }
+
private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 19af8a8537..0dbac50e4b 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -773,7 +773,28 @@ spring:
leakDetectionThreshold: "${SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}"
# This property increases the number of connections in the pool as demand increases. At the same time, the property ensures that the pool doesn't grow to the point of exhausting a system's resources, which ultimately affects an application's performance and availability
maximumPoolSize: "${SPRING_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
- registerMbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}" # true - enable MBean to diagnose pools state via JMX
+ # Enable MBean to diagnose pools state via JMX
+ registerMbeans: "${SPRING_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
+ events:
+ # Enable dedicated datasource (a separate database) for events and audit logs.
+ # Before enabling this, make sure you have set up the following tables in the new DB:
+ # error_event, lc_event, rule_chain_debug_event, rule_node_debug_event, stats_event, audit_log
+ enabled: "${SPRING_DEDICATED_EVENTS_DATASOURCE_ENABLED:false}"
+ # Database driver for Spring JPA for events datasource
+ driverClassName: "${SPRING_EVENTS_DATASOURCE_DRIVER_CLASS_NAME:org.postgresql.Driver}"
+ # Database connection URL for events datasource
+ url: "${SPRING_EVENTS_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard_events}"
+ # Database username for events datasource
+ username: "${SPRING_EVENTS_DATASOURCE_USERNAME:postgres}"
+ # Database user password for events datasource
+ password: "${SPRING_EVENTS_DATASOURCE_PASSWORD:postgres}"
+ hikari:
+ # This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a possible connection leak for events datasource. A value of 0 means leak detection is disabled
+ leakDetectionThreshold: "${SPRING_EVENTS_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD:0}"
+ # This property increases the number of connections in the pool as demand increases for events datasource. At the same time, the property ensures that the pool doesn't grow to the point of exhausting a system's resources, which ultimately affects an application's performance and availability
+ maximumPoolSize: "${SPRING_EVENTS_DATASOURCE_MAXIMUM_POOL_SIZE:16}"
+ # Enable MBean to diagnose pools state via JMX for events datasource
+ registerMbeans: "${SPRING_EVENTS_DATASOURCE_HIKARI_REGISTER_MBEANS:false}"
# Audit log parameters
audit-log:
diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
index 4c80e12390..2386b91c6e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
@@ -64,6 +65,7 @@ public class AuditLogControllerTest extends AbstractControllerTest {
@Autowired
private AuditLogDao auditLogDao;
+ @Getter
@SpyBean
private SqlPartitioningRepository partitioningRepository;
@SpyBean
@@ -183,12 +185,12 @@ public class AuditLogControllerTest extends AbstractControllerTest {
@Test
public void whenSavingNewAuditLog_thenCheckAndCreatePartitionIfNotExists() throws ParseException {
long entityTs = ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2024-01-01T01:43:11Z").getTime();
- reset(partitioningRepository);
+ reset(getPartitioningRepository());
AuditLog auditLog = createAuditLog(ActionType.LOGIN, tenantAdminUserId, entityTs);
- verify(partitioningRepository).createPartitionIfNotExists(eq("audit_log"), eq(auditLog.getCreatedTime()), eq(partitionDurationInMs));
+ verify(getPartitioningRepository()).createPartitionIfNotExists(eq("audit_log"), eq(auditLog.getCreatedTime()), eq(partitionDurationInMs));
- List partitions = partitioningRepository.fetchPartitions("audit_log");
- assertThat(partitions).contains(partitioningRepository.calculatePartitionStartTime(auditLog.getCreatedTime(), partitionDurationInMs));
+ List partitions = getPartitioningRepository().fetchPartitions("audit_log");
+ assertThat(partitions).contains(getPartitioningRepository().calculatePartitionStartTime(auditLog.getCreatedTime(), partitionDurationInMs));
}
@Test
@@ -197,15 +199,15 @@ public class AuditLogControllerTest extends AbstractControllerTest {
final long oldAuditLogTs = ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2020-10-01T00:00:00Z").getTime();
final long currentTimeMillis = oldAuditLogTs + TimeUnit.SECONDS.toMillis(auditLogsTtlInSec) * 2;
- final long partitionStartTs = partitioningRepository.calculatePartitionStartTime(oldAuditLogTs, partitionDurationInMs);
- partitioningRepository.createPartitionIfNotExists("audit_log", oldAuditLogTs, partitionDurationInMs);
- List partitions = partitioningRepository.fetchPartitions("audit_log");
+ final long partitionStartTs = getPartitioningRepository().calculatePartitionStartTime(oldAuditLogTs, partitionDurationInMs);
+ getPartitioningRepository().createPartitionIfNotExists("audit_log", oldAuditLogTs, partitionDurationInMs);
+ List partitions = getPartitioningRepository().fetchPartitions("audit_log");
assertThat(partitions).contains(partitionStartTs);
willReturn(currentTimeMillis).given(auditLogsCleanUpService).getCurrentTimeMillis();
auditLogsCleanUpService.cleanUp();
- partitions = partitioningRepository.fetchPartitions("audit_log");
+ partitions = getPartitioningRepository().fetchPartitions("audit_log");
assertThat(partitions).as("partitions cleared").doesNotContain(partitionStartTs);
assertThat(partitions).as("only newer partitions left").allSatisfy(partitionsStart -> {
long partitionEndTs = partitionsStart + partitionDurationInMs;
@@ -218,18 +220,18 @@ public class AuditLogControllerTest extends AbstractControllerTest {
// creating partition bigger than sql.audit_logs.partition_size
long entityTs = ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-04-29T07:43:11Z").getTime();
//the partition 7 days is overlapping default partition size 1 day, use in the far past to not affect other tests
- partitioningRepository.createPartitionIfNotExists("audit_log", entityTs, TimeUnit.DAYS.toMillis(7));
- List partitions = partitioningRepository.fetchPartitions("audit_log");
+ getPartitioningRepository().createPartitionIfNotExists("audit_log", entityTs, TimeUnit.DAYS.toMillis(7));
+ List partitions = getPartitioningRepository().fetchPartitions("audit_log");
log.warn("entityTs [{}], fetched partitions {}", entityTs, partitions);
assertThat(partitions).contains(ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-04-28T00:00:00Z").getTime());
- partitioningRepository.cleanupPartitionsCache("audit_log", entityTs, 0);
+ getPartitioningRepository().cleanupPartitionsCache("audit_log", entityTs, 0);
assertDoesNotThrow(() -> {
// expecting partition overlap error on partition save
createAuditLog(ActionType.LOGIN, tenantAdminUserId, entityTs);
});
- assertThat(partitioningRepository.fetchPartitions("audit_log"))
- .contains(ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-04-28T00:00:00Z").getTime());;
+ assertThat(getPartitioningRepository().fetchPartitions("audit_log"))
+ .contains(ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-04-28T00:00:00Z").getTime());
}
private AuditLog createAuditLog(ActionType actionType, EntityId entityId, long entityTs) {
diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest_DedicatedEventsDataSource.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest_DedicatedEventsDataSource.java
new file mode 100644
index 0000000000..7678b2a91f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest_DedicatedEventsDataSource.java
@@ -0,0 +1,36 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.controller;
+
+import lombok.Getter;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.dao.sqlts.insert.sql.DedicatedEventsSqlPartitioningRepository;
+
+@DaoSqlTest
+@TestPropertySource(properties = {
+ "spring.datasource.events.enabled=true",
+ "spring.datasource.events.url=${spring.datasource.url}",
+ "spring.datasource.events.driverClassName=${spring.datasource.driverClassName}",
+})
+public class AuditLogControllerTest_DedicatedEventsDataSource extends AuditLogControllerTest {
+
+ @Getter
+ @SpyBean
+ private DedicatedEventsSqlPartitioningRepository partitioningRepository;
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
index e272a40857..29bf048082 100644
--- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
@@ -23,11 +23,14 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.page.PageData;
@@ -35,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
+import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
@@ -66,6 +70,8 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_png_image.png";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE);
+ assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
+
assertThat(imageInfo.getTitle()).isEqualTo(filename);
assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE);
assertThat(imageInfo.getResourceKey()).isEqualTo(filename);
@@ -85,6 +91,8 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_jpeg_image.jpg";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE);
+ assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
+
ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
checkJpegImageDescriptor(imageDescriptor);
@@ -97,6 +105,22 @@ public class ImageControllerTest extends AbstractControllerTest {
String filename = "my_svg_image.svg";
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE);
+ assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.IMAGE);
+
+ ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
+ checkSvgImageDescriptor(imageDescriptor);
+
+ assertThat(downloadImage("tenant", filename)).containsExactly(SVG_IMAGE);
+ assertThat(downloadImagePreview("tenant", filename)).hasSize((int) imageDescriptor.getPreviewDescriptor().getSize());
+ }
+
+ @Test
+ public void testUploadScadaSymbolImage() throws Exception {
+ String filename = "my_scada_symbol_image.svg";
+ TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), filename, "image/svg+xml", SVG_IMAGE);
+
+ assertThat(imageInfo.getResourceSubType()).isEqualTo(ResourceSubType.SCADA_SYMBOL);
+
ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class);
checkSvgImageDescriptor(imageDescriptor);
@@ -175,6 +199,7 @@ public class ImageControllerTest extends AbstractControllerTest {
assertThat(exportData.getMediaType()).isEqualTo("image/png");
assertThat(exportData.getFileName()).isEqualTo(filename);
assertThat(exportData.getTitle()).isEqualTo(filename);
+ assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name());
assertThat(exportData.getResourceKey()).isEqualTo(filename);
assertThat(exportData.getData()).isEqualTo(Base64.getEncoder().encodeToString(PNG_IMAGE));
assertThat(exportData.isPublic()).isTrue();
@@ -184,6 +209,7 @@ public class ImageControllerTest extends AbstractControllerTest {
TbResourceInfo importedImageInfo = doPut("/api/image/import", exportData, TbResourceInfo.class);
assertThat(importedImageInfo.getTitle()).isEqualTo(filename);
+ assertThat(exportData.getSubType()).isEqualTo(ResourceSubType.IMAGE.name());
assertThat(importedImageInfo.getResourceKey()).isEqualTo(filename);
assertThat(importedImageInfo.getFileName()).isEqualTo(filename);
assertThat(importedImageInfo.isPublic()).isTrue();
@@ -198,20 +224,37 @@ public class ImageControllerTest extends AbstractControllerTest {
String systemImageName = "my_system_png_image.png";
TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE);
+ String systemScadaSymbolName = "my_system_scada_symbol_image.svg";
+ TbResourceInfo systemScadaSymbol = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), systemScadaSymbolName, "image/svg+xml", SVG_IMAGE);
+
loginTenantAdmin();
String tenantImageName = "my_jpeg_image.jpg";
TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE);
+ String tenantScadaSymbolName = "my_scada_symbol_image.svg";
+ TbResourceInfo tenantScadaSymbol = uploadImage(HttpMethod.POST, "/api/image", ResourceSubType.SCADA_SYMBOL.name(), tenantScadaSymbolName, "image/svg+xml", SVG_IMAGE);
+
List tenantImages = getImages(null, false, 10);
assertThat(tenantImages).containsOnly(tenantImage);
+ List tenantScadaSymbols = getImages(null, ResourceSubType.SCADA_SYMBOL.name(), false, 10);
+ assertThat(tenantScadaSymbols).containsOnly(tenantScadaSymbol);
+
List allImages = getImages(null, true, 10);
assertThat(allImages).containsOnly(tenantImage, systemImage);
+ List allScadaSymbols = getImages(null, ResourceSubType.SCADA_SYMBOL.name(), true, 10);
+ assertThat(allScadaSymbols).containsOnly(tenantScadaSymbol, systemScadaSymbol);
+
assertThat(getImages("png", true, 10))
.containsOnly(systemImage);
assertThat(getImages("jpg", true, 10))
.containsOnly(tenantImage);
+
+ assertThat(getImages("my_system_scada_symbol", ResourceSubType.SCADA_SYMBOL.name(), true, 10))
+ .containsOnly(systemScadaSymbol);
+ assertThat(getImages("my_scada_symbol", ResourceSubType.SCADA_SYMBOL.name(),true, 10))
+ .containsOnly(tenantScadaSymbol);
}
@Test
@@ -312,7 +355,15 @@ public class ImageControllerTest extends AbstractControllerTest {
}
private List getImages(String searchText, boolean includeSystemImages, int limit) throws Exception {
- PageData images = doGetTypedWithPageLink("/api/images?includeSystemImages=" + includeSystemImages + "&", new TypeReference<>() {}, new PageLink(limit, 0, searchText));
+ return this.getImages(searchText, null, includeSystemImages, limit);
+ }
+
+ private List getImages(String searchText, String imageSubType, boolean includeSystemImages, int limit) throws Exception {
+ var url = "/api/images?includeSystemImages=" + includeSystemImages + "&";
+ if (StringUtils.isNotEmpty(imageSubType)) {
+ url += "imageSubType=" + imageSubType+ "&";
+ }
+ PageData images = doGetTypedWithPageLink(url, new TypeReference<>() {}, new PageLink(limit, 0, searchText));
return images.getData();
}
@@ -332,8 +383,16 @@ public class ImageControllerTest extends AbstractControllerTest {
}
private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception {
+ return this.uploadImage(httpMethod, url, null, filename, mediaType, content);
+ }
+
+ private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String subType, String filename, String mediaType, byte[] content) throws Exception {
MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content);
var request = MockMvcRequestBuilders.multipart(httpMethod, url).file(file);
+ if (StringUtils.isNotEmpty(subType)) {
+ var imageSubTypePart = new MockPart("imageSubType", subType.getBytes(StandardCharsets.UTF_8));
+ request.part(imageSubTypePart);
+ }
setJwtToken(request);
return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class);
}
diff --git a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
index 2d6009b15c..37b50fda4f 100644
--- a/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.validator.RuleChainDataValidator;
@@ -65,6 +66,8 @@ class InstallScriptsTest {
@MockBean
ResourceService resourceService;
@MockBean
+ ImageService imageService;
+ @MockBean
ImagesUpdater imagesUpdater;
@SpyBean
InstallScripts installScripts;
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
index 9a65fcfdae..96b97933aa 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.HasImage;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
@@ -36,9 +37,9 @@ public interface ImageService {
TbResourceInfo getPublicImageInfoByKey(String publicResourceKey);
- PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);
- PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink);
+ PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink);
byte[] getImageData(TenantId tenantId, TbResourceId imageId);
@@ -46,6 +47,8 @@ public interface ImageService {
TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force);
+ String calculateImageEtag(byte[] imageData);
+
TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag);
boolean replaceBase64WithImageUrl(HasImage entity, String type);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
index 98360e4039..39a5210849 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.entity.EntityDaoService;
@@ -40,11 +41,11 @@ public interface WidgetTypeService extends EntityDaoService {
void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId);
- PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findSystemWidgetTypesByPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
- PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findAllTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
- PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId);
@@ -56,6 +57,8 @@ public interface WidgetTypeService extends EntityDaoService {
WidgetType findWidgetTypeByTenantIdAndFqn(TenantId tenantId, String fqn);
+ WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn);
+
void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds);
void updateWidgetsBundleWidgetFqns(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetFqns);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
index 396ba4e3fa..9dbc0149a9 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
@@ -34,15 +35,15 @@ public interface WidgetsBundleService extends EntityDaoService {
WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias);
- PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
List findSystemWidgetsBundles(TenantId tenantId);
PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink);
- PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
- PageData findTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
List findAllTenantWidgetsBundlesByTenantId(TenantId tenantId);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java
index b407563d24..3be7b6f92d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java
@@ -33,6 +33,7 @@ public class ImageExportData {
private String mediaType;
private String fileName;
private String title;
+ private String subType;
private String resourceKey;
private boolean isPublic;
private String publicResourceKey;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventCleanupRepository.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java
similarity index 74%
rename from dao/src/main/java/org/thingsboard/server/dao/sql/event/EventCleanupRepository.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java
index 3b9aa3d5c1..10a033f75d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventCleanupRepository.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao.sql.event;
+package org.thingsboard.server.common.data;
-public interface EventCleanupRepository {
-
- void cleanupEvents(long eventExpTime, boolean debug);
-
- void migrateEvents(long regularEventTs, long debugEventTs);
+public enum ResourceSubType {
+ IMAGE,
+ SCADA_SYMBOL
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
index f1318c2257..8537fe68ed 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
@@ -46,6 +46,8 @@ public class TbResourceInfo extends BaseData implements HasName, H
private String title;
@Schema(description = "Resource type.", example = "LWM2M_MODEL", accessMode = Schema.AccessMode.READ_ONLY)
private ResourceType resourceType;
+ @Schema(description = "Resource sub type.", example = "IOT_SVG", accessMode = Schema.AccessMode.READ_ONLY)
+ private ResourceSubType resourceSubType;
@NoXss
@Length(fieldName = "resourceKey")
@Schema(description = "Resource key.", example = "19_1.0", accessMode = Schema.AccessMode.READ_ONLY)
@@ -78,6 +80,7 @@ public class TbResourceInfo extends BaseData implements HasName, H
this.tenantId = resourceInfo.tenantId;
this.title = resourceInfo.title;
this.resourceType = resourceInfo.resourceType;
+ this.resourceSubType = resourceInfo.resourceSubType;
this.resourceKey = resourceInfo.resourceKey;
this.searchText = resourceInfo.searchText;
this.isPublic = resourceInfo.isPublic;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
index 87d9b69749..60583dbb96 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
@@ -27,5 +27,6 @@ public class TbResourceInfoFilter {
private TenantId tenantId;
private Set resourceTypes;
+ private Set resourceSubTypes;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java
index 6e1198d840..36692f5738 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java
@@ -47,6 +47,9 @@ public class BaseWidgetType extends BaseData implements HasName, H
@Schema(description = "Whether widget type is deprecated.", example = "true")
private boolean deprecated;
+ @Schema(description = "Whether widget type is SCADA symbol.", example = "true")
+ private boolean scada;
+
private Long version;
public BaseWidgetType() {
@@ -63,6 +66,7 @@ public class BaseWidgetType extends BaseData implements HasName, H
this.fqn = widgetType.getFqn();
this.name = widgetType.getName();
this.deprecated = widgetType.isDeprecated();
+ this.scada = widgetType.isScada();
this.version = widgetType.getVersion();
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java
new file mode 100644
index 0000000000..e8d1601b64
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeFilter.java
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.common.data.widget;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.List;
+
+@Data
+@Builder
+public class WidgetTypeFilter {
+
+ private TenantId tenantId;
+ private boolean fullSearch;
+ private boolean scadaFirst;
+ DeprecatedFilter deprecatedFilter;
+ List widgetTypes;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
index e8e964a033..005e4ca252 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
@@ -62,6 +62,11 @@ public class WidgetsBundle extends BaseData implements HasName,
@Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = Schema.AccessMode.READ_ONLY)
private String image;
+ @Getter
+ @Setter
+ @Schema(description = "Whether widgets bundle contains SCADA symbol widget types.", accessMode = Schema.AccessMode.READ_ONLY)
+ private boolean scada;
+
@NoXss
@Length(fieldName = "description", max = 1024)
@Getter
@@ -95,6 +100,7 @@ public class WidgetsBundle extends BaseData implements HasName,
this.alias = widgetsBundle.getAlias();
this.title = widgetsBundle.getTitle();
this.image = widgetsBundle.getImage();
+ this.scada = widgetsBundle.isScada();
this.description = widgetsBundle.getDescription();
this.order = widgetsBundle.getOrder();
this.externalId = widgetsBundle.getExternalId();
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java
new file mode 100644
index 0000000000..932bfa8fc8
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundleFilter.java
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.common.data.widget;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@Data
+@Builder
+public class WidgetsBundleFilter {
+
+ private TenantId tenantId;
+ private boolean fullSearch;
+ private boolean scadaFirst;
+
+ public static WidgetsBundleFilter fromTenantId(TenantId tenantId) {
+ return WidgetsBundleFilter.builder().tenantId(tenantId).fullSearch(false).scadaFirst(false).build();
+ }
+
+ public static WidgetsBundleFilter fullSearchFromTenantId(TenantId tenantId) {
+ return WidgetsBundleFilter.builder().tenantId(tenantId).fullSearch(true).scadaFirst(false).build();
+ }
+
+}
diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
index b0574da7b1..5646a37a68 100644
--- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
+++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant;
@@ -865,6 +866,9 @@ public class ProtoUtils {
if (isNotNull(resource.getPreview())) {
builder.setPreview(ByteString.copyFrom(resource.getPreview()));
}
+ if (isNotNull(resource.getResourceSubType())) {
+ builder.setResourceSubType(resource.getResourceSubType().name());
+ }
return builder.build();
}
@@ -896,6 +900,9 @@ public class ProtoUtils {
if (proto.hasPreview()) {
resource.setPreview(proto.getPreview().toByteArray());
}
+ if (proto.hasResourceSubType()) {
+ resource.setResourceSubType(ResourceSubType.valueOf(proto.getResourceSubType()));
+ }
return resource;
}
diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto
index 800250d1cc..7c3bfddc08 100644
--- a/common/proto/src/main/proto/queue.proto
+++ b/common/proto/src/main/proto/queue.proto
@@ -301,6 +301,7 @@ message TbResourceProto {
optional int64 externalIdLSB = 16;
optional bytes data = 17;
optional bytes preview = 18;
+ optional string resourceSubType = 19;
}
message ApiUsageStateProto {
diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
index 78f76adc1d..ab52a024da 100644
--- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
+++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
@@ -108,9 +108,9 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
@Override
public ObjectModel getObjectModel(int objectId) {
LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint());
- String version = lwM2mClient.getSupportedObjectVersion(objectId).toString();
+ var version = lwM2mClient.getSupportedObjectVersion(objectId);
if (version != null) {
- return this.getObjectModelDynamic(objectId, version);
+ return this.getObjectModelDynamic(objectId, version.toString());
}
return null;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
deleted file mode 100644
index 4dd0633420..0000000000
--- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * 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.
- */
-package org.thingsboard.server.dao;
-
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-import org.springframework.data.repository.config.BootstrapMode;
-import org.springframework.transaction.annotation.EnableTransactionManagement;
-import org.thingsboard.server.dao.util.TbAutoConfiguration;
-
-/**
- * @author Valerii Sosliuk
- */
-@Configuration
-@TbAutoConfiguration
-@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"})
-@EnableJpaRepositories(value = "org.thingsboard.server.dao.sql", bootstrapMode = BootstrapMode.LAZY)
-@EntityScan("org.thingsboard.server.dao.model.sql")
-@EnableTransactionManagement
-public class JpaDaoConfig {
-
-}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 29b097bfab..c5ff7132cb 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -408,8 +408,12 @@ public class AuditLogServiceImpl implements AuditLogService {
}
return executor.submit(() -> {
- AuditLog auditLog = auditLogDao.save(tenantId, auditLogEntry);
- auditLogSink.logAction(auditLog);
+ try {
+ AuditLog auditLog = auditLogDao.save(tenantId, auditLogEntry);
+ auditLogSink.logAction(auditLog);
+ } catch (Throwable e) {
+ log.error("[{}] Failed to save audit log: {}", tenantId, auditLogEntry, e);
+ }
return null;
});
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsDataSource.java b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsDataSource.java
new file mode 100644
index 0000000000..3ca6a20d44
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsDataSource.java
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@ConditionalOnProperty(value = "spring.datasource.events.enabled", havingValue = "true")
+public @interface DedicatedEventsDataSource {
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java
new file mode 100644
index 0000000000..e4a0b36100
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/DedicatedEventsJpaDaoConfig.java
@@ -0,0 +1,91 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.config;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+import org.thingsboard.server.dao.model.sql.ErrorEventEntity;
+import org.thingsboard.server.dao.model.sql.LifecycleEventEntity;
+import org.thingsboard.server.dao.model.sql.RuleChainDebugEventEntity;
+import org.thingsboard.server.dao.model.sql.RuleNodeDebugEventEntity;
+import org.thingsboard.server.dao.model.sql.StatisticsEventEntity;
+
+import javax.sql.DataSource;
+import java.util.Objects;
+
+@DedicatedEventsDataSource
+@Configuration
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sql.event", "org.thingsboard.server.dao.sql.audit"},
+ bootstrapMode = BootstrapMode.LAZY,
+ entityManagerFactoryRef = "eventsEntityManagerFactory", transactionManagerRef = "eventsTransactionManager")
+public class DedicatedEventsJpaDaoConfig {
+
+ public static final String EVENTS_PERSISTENCE_UNIT = "events";
+ public static final String EVENTS_DATA_SOURCE = EVENTS_PERSISTENCE_UNIT + "DataSource";
+ public static final String EVENTS_TRANSACTION_MANAGER = EVENTS_PERSISTENCE_UNIT + "TransactionManager";
+ public static final String EVENTS_TRANSACTION_TEMPLATE = EVENTS_PERSISTENCE_UNIT + "TransactionTemplate";
+ public static final String EVENTS_JDBC_TEMPLATE = EVENTS_PERSISTENCE_UNIT + "JdbcTemplate";
+
+ @Bean
+ @ConfigurationProperties("spring.datasource.events")
+ public DataSourceProperties eventsDataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ @ConfigurationProperties(prefix = "spring.datasource.events.hikari")
+ @Bean(EVENTS_DATA_SOURCE)
+ public DataSource eventsDataSource(@Qualifier("eventsDataSourceProperties") DataSourceProperties eventsDataSourceProperties) {
+ return eventsDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean eventsEntityManagerFactory(@Qualifier(EVENTS_DATA_SOURCE) DataSource eventsDataSource,
+ EntityManagerFactoryBuilder builder) {
+ return builder
+ .dataSource(eventsDataSource)
+ .packages(LifecycleEventEntity.class, StatisticsEventEntity.class, ErrorEventEntity.class, RuleNodeDebugEventEntity.class, RuleChainDebugEventEntity.class, AuditLogEntity.class)
+ .persistenceUnit(EVENTS_PERSISTENCE_UNIT)
+ .build();
+ }
+
+ @Bean(EVENTS_TRANSACTION_MANAGER)
+ public JpaTransactionManager eventsTransactionManager(@Qualifier("eventsEntityManagerFactory") LocalContainerEntityManagerFactoryBean eventsEntityManagerFactory) {
+ return new JpaTransactionManager(Objects.requireNonNull(eventsEntityManagerFactory.getObject()));
+ }
+
+ @Bean(EVENTS_TRANSACTION_TEMPLATE)
+ public TransactionTemplate eventsTransactionTemplate(@Qualifier(EVENTS_TRANSACTION_MANAGER) JpaTransactionManager eventsTransactionManager) {
+ return new TransactionTemplate(eventsTransactionManager);
+ }
+
+ @Bean(EVENTS_JDBC_TEMPLATE)
+ public JdbcTemplate eventsJdbcTemplate(@Qualifier(EVENTS_DATA_SOURCE) DataSource eventsDataSource) {
+ return new JdbcTemplate(eventsDataSource);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/DefaultDataSource.java b/dao/src/main/java/org/thingsboard/server/dao/config/DefaultDataSource.java
new file mode 100644
index 0000000000..ad6c398ea6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/DefaultDataSource.java
@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@ConditionalOnProperty(value = "spring.datasource.events.enabled", havingValue = "false", matchIfMissing = true)
+public @interface DefaultDataSource {
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/DefaultDedicatedJpaDaoConfig.java
similarity index 56%
rename from dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java
rename to dao/src/main/java/org/thingsboard/server/dao/config/DefaultDedicatedJpaDaoConfig.java
index 09553fb973..fd28735b79 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/DefaultDedicatedJpaDaoConfig.java
@@ -13,22 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao;
+package org.thingsboard.server.dao.config;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.config.BootstrapMode;
-import org.springframework.transaction.annotation.EnableTransactionManagement;
-import org.thingsboard.server.dao.util.TbAutoConfiguration;
+@DefaultDataSource
@Configuration
-@TbAutoConfiguration
-@ComponentScan({"org.thingsboard.server.dao.sqlts.dictionary"})
-@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.dictionary"}, bootstrapMode = BootstrapMode.LAZY)
-@EntityScan({"org.thingsboard.server.dao.model.sqlts.dictionary"})
-@EnableTransactionManagement
-public class SqlTimeseriesDaoConfig {
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sql.event", "org.thingsboard.server.dao.sql.audit"}, bootstrapMode = BootstrapMode.LAZY)
+public class DefaultDedicatedJpaDaoConfig {
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java
new file mode 100644
index 0000000000..86c0ed4ea3
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/JpaDaoConfig.java
@@ -0,0 +1,121 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.config;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.data.repository.config.BootstrapMode;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.thingsboard.server.dao.sql.audit.AuditLogRepository;
+import org.thingsboard.server.dao.sql.event.EventRepository;
+import org.thingsboard.server.dao.util.TbAutoConfiguration;
+
+import javax.sql.DataSource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+@Configuration
+@TbAutoConfiguration
+@ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.sqlts.dictionary", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"})
+@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.sqlts.dictionary"},
+ excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {EventRepository.class, AuditLogRepository.class}),
+ bootstrapMode = BootstrapMode.LAZY)
+public class JpaDaoConfig {
+
+ @Bean
+ @ConfigurationProperties("spring.datasource")
+ public DataSourceProperties dataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ @Primary
+ @ConfigurationProperties(prefix = "spring.datasource.hikari")
+ @Bean
+ public DataSource dataSource(@Qualifier("dataSourceProperties") DataSourceProperties dataSourceProperties) {
+ return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
+ }
+
+ @Primary
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("dataSource") DataSource dataSource,
+ EntityManagerFactoryBuilder builder,
+ @Autowired(required = false) SqlTsLatestDaoConfig tsLatestDaoConfig,
+ @Autowired(required = false) SqlTsDaoConfig tsDaoConfig,
+ @Autowired(required = false) TimescaleDaoConfig timescaleDaoConfig,
+ @Autowired(required = false) TimescaleTsLatestDaoConfig timescaleTsLatestDaoConfig) {
+ List packages = new ArrayList<>();
+ packages.add("org.thingsboard.server.dao.model.sql");
+ packages.add("org.thingsboard.server.dao.model.sqlts.dictionary");
+ if (tsLatestDaoConfig != null) {
+ packages.add("org.thingsboard.server.dao.model.sqlts.latest");
+ }
+ if (tsDaoConfig != null) {
+ packages.add("org.thingsboard.server.dao.model.sqlts.ts");
+ }
+ if (timescaleDaoConfig != null) {
+ packages.add("org.thingsboard.server.dao.model.sqlts.timescale");
+ }
+ if (timescaleTsLatestDaoConfig != null) {
+ packages.add("org.thingsboard.server.dao.model.sqlts.latest");
+ }
+ return builder
+ .dataSource(dataSource)
+ .packages(packages.toArray(String[]::new))
+ .persistenceUnit("default")
+ .build();
+ }
+
+ @Primary
+ @Bean
+ public JpaTransactionManager transactionManager(@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
+ return new JpaTransactionManager(Objects.requireNonNull(entityManagerFactory.getObject()));
+ }
+
+ @Primary
+ @Bean
+ public TransactionTemplate transactionTemplate(@Qualifier("transactionManager") JpaTransactionManager transactionManager) {
+ return new TransactionTemplate(transactionManager);
+ }
+
+ @Primary
+ @Bean
+ public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Primary
+ @Bean
+ public NamedParameterJdbcTemplate namedParameterJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
+ return new NamedParameterJdbcTemplate(dataSource);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/SqlTsDaoConfig.java
similarity index 89%
rename from dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java
rename to dao/src/main/java/org/thingsboard/server/dao/config/SqlTsDaoConfig.java
index bbd31846db..478dbde5d1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/SqlTsDaoConfig.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao;
+package org.thingsboard.server.dao.config;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@@ -28,7 +27,6 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.sql", "org.thingsboard.server.dao.sqlts.insert.sql"})
@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.sql"}, bootstrapMode = BootstrapMode.LAZY)
-@EntityScan({"org.thingsboard.server.dao.model.sqlts.ts"})
@EnableTransactionManagement
@SqlTsDao
public class SqlTsDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/SqlTsLatestDaoConfig.java
similarity index 89%
rename from dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java
rename to dao/src/main/java/org/thingsboard/server/dao/config/SqlTsLatestDaoConfig.java
index 6fcf21a0a8..49ff2afdb4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/SqlTsLatestDaoConfig.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao;
+package org.thingsboard.server.dao.config;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@@ -28,7 +27,6 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration;
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.sql"})
@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY)
-@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"})
@EnableTransactionManagement
@SqlTsLatestDao
public class SqlTsLatestDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/TimescaleDaoConfig.java
similarity index 89%
rename from dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java
rename to dao/src/main/java/org/thingsboard/server/dao/config/TimescaleDaoConfig.java
index c134b88897..3c910778a2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/TimescaleDaoConfig.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao;
+package org.thingsboard.server.dao.config;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@@ -28,7 +27,6 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao;
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"})
@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"}, bootstrapMode = BootstrapMode.LAZY)
-@EntityScan({"org.thingsboard.server.dao.model.sqlts.timescale"})
@EnableTransactionManagement
@TimescaleDBTsDao
public class TimescaleDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/config/TimescaleTsLatestDaoConfig.java
similarity index 89%
rename from dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java
rename to dao/src/main/java/org/thingsboard/server/dao/config/TimescaleTsLatestDaoConfig.java
index f0e74f1c3d..f6d1e49a6e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/config/TimescaleTsLatestDaoConfig.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao;
+package org.thingsboard.server.dao.config;
-import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@@ -28,7 +27,6 @@ import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao;
@TbAutoConfiguration
@ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"})
@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY)
-@EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"})
@EnableTransactionManagement
@TimescaleDBTsLatestDao
public class TimescaleTsLatestDaoConfig {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
index 6b3febf502..52335d87ec 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
@@ -101,5 +101,4 @@ public interface EventDao {
*/
void removeEvents(UUID tenantId, UUID entityId, EventFilter eventFilter, Long startTime, Long endTime);
- void migrateEvents(long regularEventTs, long debugEventTs);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 43f8ce507b..27f1019a68 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -304,6 +304,7 @@ public class ModelConstants {
public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY;
public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image";
+ public static final String WIDGETS_BUNDLE_SCADA_PROPERTY = "scada";
public static final String WIDGETS_BUNDLE_DESCRIPTION = "description";
public static final String WIDGETS_BUNDLE_ORDER = "widgets_bundle_order";
@@ -322,6 +323,8 @@ public class ModelConstants {
public static final String WIDGET_TYPE_DEPRECATED_PROPERTY = "deprecated";
+ public static final String WIDGET_TYPE_SCADA_PROPERTY = "scada";
+
public static final String WIDGET_TYPE_WIDGET_TYPE_PROPERTY = "widget_type";
public static final String WIDGET_TYPE_INFO_VIEW_TABLE_NAME = "widget_type_info_view";
@@ -500,6 +503,7 @@ public class ModelConstants {
public static final String RESOURCE_TABLE_NAME = "resource";
public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String RESOURCE_TYPE_COLUMN = "resource_type";
+ public static final String RESOURCE_SUB_TYPE_COLUMN = "resource_sub_type";
public static final String RESOURCE_KEY_COLUMN = "resource_key";
public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY;
public static final String RESOURCE_FILE_NAME_COLUMN = "file_name";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java
index 6e6ee4fed8..d649d1e31a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java
@@ -44,6 +44,9 @@ public abstract class AbstractWidgetTypeEntity extends
@Column(name = ModelConstants.WIDGET_TYPE_DEPRECATED_PROPERTY)
private boolean deprecated;
+ @Column(name = ModelConstants.WIDGET_TYPE_SCADA_PROPERTY)
+ private boolean scada;
+
public AbstractWidgetTypeEntity() {
super();
}
@@ -56,6 +59,7 @@ public abstract class AbstractWidgetTypeEntity extends
this.fqn = widgetType.getFqn();
this.name = widgetType.getName();
this.deprecated = widgetType.isDeprecated();
+ this.scada = widgetType.isScada();
}
public AbstractWidgetTypeEntity(AbstractWidgetTypeEntity widgetTypeEntity) {
@@ -64,6 +68,7 @@ public abstract class AbstractWidgetTypeEntity extends
this.fqn = widgetTypeEntity.getFqn();
this.name = widgetTypeEntity.getName();
this.deprecated = widgetTypeEntity.isDeprecated();
+ this.scada = widgetTypeEntity.isScada();
}
protected BaseWidgetType toBaseWidgetType() {
@@ -76,6 +81,7 @@ public abstract class AbstractWidgetTypeEntity extends
widgetType.setFqn(fqn);
widgetType.setName(name);
widgetType.setDeprecated(deprecated);
+ widgetType.setScada(scada);
return widgetType;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
index 1e878e9dc5..85e697bb2b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -149,4 +149,5 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit
auditLog.setActionFailureDetails(this.actionFailureDetails);
return auditLog;
}
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java
index 02922c742a..0fa74b4c9f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java
@@ -22,6 +22,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@@ -40,6 +41,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
@@ -61,6 +63,9 @@ public class TbResourceEntity extends BaseSqlEntity {
@Column(name = RESOURCE_TYPE_COLUMN)
private String resourceType;
+ @Column(name = RESOURCE_SUB_TYPE_COLUMN)
+ private String resourceSubType;
+
@Column(name = RESOURCE_KEY_COLUMN)
private String resourceKey;
@@ -105,6 +110,9 @@ public class TbResourceEntity extends BaseSqlEntity {
}
this.title = resource.getTitle();
this.resourceType = resource.getResourceType().name();
+ if (resource.getResourceSubType() != null) {
+ this.resourceSubType = resource.getResourceSubType().name();
+ }
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.fileName = resource.getFileName();
@@ -124,6 +132,7 @@ public class TbResourceEntity extends BaseSqlEntity {
resource.setTenantId(TenantId.fromUUID(tenantId));
resource.setTitle(title);
resource.setResourceType(ResourceType.valueOf(resourceType));
+ resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null);
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setFileName(fileName);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java
index 7d65d70a37..328bf38978 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java
@@ -22,6 +22,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
@@ -39,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLU
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_SUB_TYPE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
@@ -60,6 +62,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen
@Column(name = RESOURCE_TYPE_COLUMN)
private String resourceType;
+ @Column(name = RESOURCE_SUB_TYPE_COLUMN)
+ private String resourceSubType;
+
@Column(name = RESOURCE_KEY_COLUMN)
private String resourceKey;
@@ -96,6 +101,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen
this.tenantId = resource.getTenantId().getId();
this.title = resource.getTitle();
this.resourceType = resource.getResourceType().name();
+ if (resource.getResourceSubType() != null) {
+ this.resourceSubType = resource.getResourceSubType().name();
+ }
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.etag = resource.getEtag();
@@ -113,6 +121,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen
resource.setTenantId(TenantId.fromUUID(tenantId));
resource.setTitle(title);
resource.setResourceType(ResourceType.valueOf(resourceType));
+ resource.setResourceSubType(resourceSubType != null ? ResourceSubType.valueOf(resourceSubType) : null);
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setEtag(etag);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java
index 5e2766b98d..43e2bfd457 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java
@@ -47,6 +47,9 @@ public final class WidgetsBundleEntity extends BaseVersionedEntity getImagesByTenantId(TenantId tenantId, PageLink pageLink) {
+ public PageData getImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) {
log.trace("Executing getImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
+ .resourceSubTypes(Set.of(imageSubType))
.build();
return findTenantResourcesByTenantId(filter, pageLink);
}
@Override
- public PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) {
+ public PageData getAllImagesByTenantId(TenantId tenantId, ResourceSubType imageSubType, PageLink pageLink) {
log.trace("Executing getAllImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
+ .resourceSubTypes(Set.of(imageSubType))
.build();
return findAllTenantResourcesByTenantId(filter, pageLink);
}
@@ -280,6 +287,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return result.success(success).build();
}
+ @Override
+ public String calculateImageEtag(byte[] imageData) {
+ return calculateEtag(imageData);
+ }
+
@Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag);
@@ -419,7 +431,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return base64ToImageUrl(tenantId, name, data, false);
}
- private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:(.*):(.*);data:(.*);.*");
+ private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^:]*):([^:]*):?([^:]*)?;data:(.*);.*");
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) {
if (StringUtils.isBlank(data)) {
@@ -429,11 +441,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic
boolean matches = matcher.matches();
String mdResourceKey = null;
String mdResourceName = null;
+ String mdResourceSubType = null;
String mdMediaType;
if (matches) {
mdResourceKey = new String(Base64.getDecoder().decode(matcher.group(1)), StandardCharsets.UTF_8);
mdResourceName = new String(Base64.getDecoder().decode(matcher.group(2)), StandardCharsets.UTF_8);
- mdMediaType = matcher.group(3);
+ if (StringUtils.isNotBlank(matcher.group(3))) {
+ mdResourceSubType = new String(Base64.getDecoder().decode(matcher.group(3)), StandardCharsets.UTF_8);
+ };
+ mdMediaType = matcher.group(4);
} else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) {
mdMediaType = StringUtils.substringBetween(data, "data:", ";base64");
} else {
@@ -462,6 +478,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic
} else {
fileName = mdResourceKey;
}
+ if (StringUtils.isBlank(mdResourceSubType)) {
+ image.setResourceSubType(ResourceSubType.IMAGE);
+ } else {
+ image.setResourceSubType(ResourceSubType.valueOf(mdResourceSubType));
+ }
image.setFileName(fileName);
image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType));
image.setData(imageData);
@@ -623,7 +644,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String tbImagePrefix = "";
if (addTbImagePrefix) {
tbImagePrefix = "tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":"
- + Base64.getEncoder().encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ";";
+ + Base64.getEncoder().encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ":"
+ + Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes(StandardCharsets.UTF_8)) + ";";
}
return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64.getEncoder().encodeToString(data);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
index bc641bb1fc..fcace55c93 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
@@ -187,7 +187,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType resourceType, String[] objectIds) {
log.trace("Executing findTenantResourcesByResourceTypeAndObjectIds [{}][{}][{}]", tenantId, resourceType, objectIds);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
- return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, objectIds, null);
+ return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, objectIds, null);
}
@Override
@@ -201,7 +201,7 @@ public class BaseResourceService extends AbstractCachedEntityService findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType resourceType, PageLink pageLink) {
log.trace("Executing findTenantResourcesByResourceTypeAndPageLink [{}][{}][{}]", tenantId, resourceType, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
- return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, pageLink);
+ return resourceDao.findResourcesByTenantIdAndResourceType(tenantId, resourceType, null, pageLink);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
index b776b49cc9..c236a4a5d5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.resource;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@@ -35,10 +36,12 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao,
PageData findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
+ ResourceSubType resourceSubType,
PageLink pageLink);
List findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
+ ResourceSubType resourceSubType,
String[] objectIds,
String searchText);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
index 66dba55d36..8002730686 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
@@ -55,10 +55,6 @@ public abstract class JpaAbstractDao, D>
@PersistenceContext
private EntityManager entityManager;
- protected abstract Class getEntityClass();
-
- protected abstract JpaRepository getRepository();
-
@Override
@Transactional
public D save(TenantId tenantId, D domain) {
@@ -85,6 +81,7 @@ public abstract class JpaAbstractDao, D>
}
protected E doSave(E entity, boolean isNew) {
+ EntityManager entityManager = getEntityManager();
if (isNew) {
if (entity instanceof HasVersion versionedEntity) {
versionedEntity.setVersion(1L);
@@ -176,11 +173,23 @@ public abstract class JpaAbstractDao, D>
}
query += " ORDER BY id LIMIT ?";
- return jdbcTemplate.queryForList(query, UUID.class, params);
+ return getJdbcTemplate().queryForList(query, UUID.class, params);
}
protected String getTenantIdColumn() {
return ModelConstants.TENANT_ID_COLUMN;
}
+ protected EntityManager getEntityManager() {
+ return entityManager;
+ }
+
+ protected JdbcTemplate getJdbcTemplate() {
+ return jdbcTemplate;
+ }
+
+ protected abstract Class getEntityClass();
+
+ protected abstract JpaRepository getRepository();
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/DedicatedJpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/DedicatedJpaAuditLogDao.java
new file mode 100644
index 0000000000..d40e1e5474
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/DedicatedJpaAuditLogDao.java
@@ -0,0 +1,87 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.config.DedicatedEventsDataSource;
+import org.thingsboard.server.dao.sqlts.insert.sql.DedicatedEventsSqlPartitioningRepository;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import java.util.Collection;
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_JDBC_TEMPLATE;
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_PERSISTENCE_UNIT;
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_TRANSACTION_MANAGER;
+
+@DedicatedEventsDataSource
+@Component
+@SqlDao
+public class DedicatedJpaAuditLogDao extends JpaAuditLogDao {
+
+ @Autowired
+ @Qualifier(EVENTS_JDBC_TEMPLATE)
+ private JdbcTemplate jdbcTemplate;
+ @PersistenceContext(unitName = EVENTS_PERSISTENCE_UNIT)
+ private EntityManager entityManager;
+
+ public DedicatedJpaAuditLogDao(AuditLogRepository auditLogRepository, DedicatedEventsSqlPartitioningRepository partitioningRepository) {
+ super(auditLogRepository, partitioningRepository);
+ }
+
+ @Transactional(transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public AuditLog save(TenantId tenantId, AuditLog domain) {
+ return super.save(tenantId, domain);
+ }
+
+ @Transactional(transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public AuditLog saveAndFlush(TenantId tenantId, AuditLog domain) {
+ return super.saveAndFlush(tenantId, domain);
+ }
+
+ @Transactional(transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public void removeById(TenantId tenantId, UUID id) {
+ super.removeById(tenantId, id);
+ }
+
+ @Transactional(transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public void removeAllByIds(Collection ids) {
+ super.removeAllByIds(ids);
+ }
+
+ @Override
+ protected EntityManager getEntityManager() {
+ return entityManager;
+ }
+
+ @Override
+ protected JdbcTemplate getJdbcTemplate() {
+ return jdbcTemplate;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
index f73cb95c27..7e859d1e8c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
@@ -30,7 +29,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.audit.AuditLogDao;
-import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.config.DefaultDataSource;
import org.thingsboard.server.dao.model.sql.AuditLogEntity;
import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
@@ -40,6 +39,9 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_TABLE_NAME;
+
+@DefaultDataSource
@Component
@SqlDao
@RequiredArgsConstructor
@@ -48,24 +50,9 @@ public class JpaAuditLogDao extends JpaPartitionedAbstractDao getEntityClass() {
- return AuditLogEntity.class;
- }
-
- @Override
- protected JpaRepository getRepository() {
- return auditLogRepository;
- }
@Override
public PageData findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) {
@@ -124,12 +111,22 @@ public class JpaAuditLogDao extends JpaPartitionedAbstractDao getEntityClass() {
+ return AuditLogEntity.class;
+ }
+
+ @Override
+ protected JpaRepository getRepository() {
+ return auditLogRepository;
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedEventInsertRepository.java
new file mode 100644
index 0000000000..65169dcbc7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedEventInsertRepository.java
@@ -0,0 +1,36 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.sql.event;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.thingsboard.server.dao.config.DedicatedEventsDataSource;
+
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_JDBC_TEMPLATE;
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_TRANSACTION_TEMPLATE;
+
+@DedicatedEventsDataSource
+@Repository
+public class DedicatedEventInsertRepository extends EventInsertRepository {
+
+ public DedicatedEventInsertRepository(@Qualifier(EVENTS_JDBC_TEMPLATE) JdbcTemplate jdbcTemplate,
+ @Qualifier(EVENTS_TRANSACTION_TEMPLATE) TransactionTemplate transactionTemplate) {
+ super(jdbcTemplate, transactionTemplate);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedJpaEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedJpaEventDao.java
new file mode 100644
index 0000000000..9b7af5e7f7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/DedicatedJpaEventDao.java
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.sql.event;
+
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.stats.StatsFactory;
+import org.thingsboard.server.dao.config.DedicatedEventsDataSource;
+import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
+import org.thingsboard.server.dao.sqlts.insert.sql.DedicatedEventsSqlPartitioningRepository;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@DedicatedEventsDataSource
+@Component
+@SqlDao
+public class DedicatedJpaEventDao extends JpaBaseEventDao {
+
+ public DedicatedJpaEventDao(EventPartitionConfiguration partitionConfiguration,
+ DedicatedEventsSqlPartitioningRepository partitioningRepository,
+ LifecycleEventRepository lcEventRepository,
+ StatisticsEventRepository statsEventRepository,
+ ErrorEventRepository errorEventRepository,
+ DedicatedEventInsertRepository eventInsertRepository,
+ RuleNodeDebugEventRepository ruleNodeDebugEventRepository,
+ RuleChainDebugEventRepository ruleChainDebugEventRepository,
+ ScheduledLogExecutorComponent logExecutor,
+ StatsFactory statsFactory) {
+ super(partitionConfiguration, partitioningRepository, lcEventRepository, statsEventRepository,
+ errorEventRepository, eventInsertRepository, ruleNodeDebugEventRepository,
+ ruleChainDebugEventRepository, logExecutor, statsFactory);
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
index faa5eb6562..962be57892 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
@@ -16,7 +16,7 @@
package org.thingsboard.server.dao.sql.event;
import jakarta.annotation.PostConstruct;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.event.RuleChainDebugEvent;
import org.thingsboard.server.common.data.event.RuleNodeDebugEvent;
import org.thingsboard.server.common.data.event.StatisticsEvent;
+import org.thingsboard.server.dao.config.DefaultDataSource;
import org.thingsboard.server.dao.util.SqlDao;
import java.sql.PreparedStatement;
@@ -44,9 +45,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+@DefaultDataSource
@Repository
@Transactional
@SqlDao
+@RequiredArgsConstructor
public class EventInsertRepository {
private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
@@ -55,11 +58,8 @@ public class EventInsertRepository {
private final Map insertStmtMap = new ConcurrentHashMap<>();
- @Autowired
- protected JdbcTemplate jdbcTemplate;
-
- @Autowired
- private TransactionTemplate transactionTemplate;
+ private final JdbcTemplate jdbcTemplate;
+ private final TransactionTemplate transactionTemplate;
@Value("${sql.remove_null_chars:true}")
private boolean removeNullChars;
@@ -236,4 +236,5 @@ public class EventInsertRepository {
}
return strValue;
}
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
index 33e7567664..b8d5083402 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
@@ -19,8 +19,8 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.config.DefaultDataSource;
import org.thingsboard.server.dao.event.EventDao;
import org.thingsboard.server.dao.model.sql.EventEntity;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
@@ -54,46 +55,23 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
-/**
- * Created by Valerii Sosliuk on 5/3/2017.
- */
-@Slf4j
+@DefaultDataSource
@Component
@SqlDao
+@RequiredArgsConstructor
+@Slf4j
public class JpaBaseEventDao implements EventDao {
- @Autowired
- private EventPartitionConfiguration partitionConfiguration;
-
- @Autowired
- private SqlPartitioningRepository partitioningRepository;
-
- @Autowired
- private LifecycleEventRepository lcEventRepository;
-
- @Autowired
- private StatisticsEventRepository statsEventRepository;
-
- @Autowired
- private ErrorEventRepository errorEventRepository;
-
- @Autowired
- private EventInsertRepository eventInsertRepository;
-
- @Autowired
- private EventCleanupRepository eventCleanupRepository;
-
- @Autowired
- private RuleNodeDebugEventRepository ruleNodeDebugEventRepository;
-
- @Autowired
- private RuleChainDebugEventRepository ruleChainDebugEventRepository;
-
- @Autowired
- ScheduledLogExecutorComponent logExecutor;
-
- @Autowired
- private StatsFactory statsFactory;
+ private final EventPartitionConfiguration partitionConfiguration;
+ private final SqlPartitioningRepository partitioningRepository;
+ private final LifecycleEventRepository lcEventRepository;
+ private final StatisticsEventRepository statsEventRepository;
+ private final ErrorEventRepository errorEventRepository;
+ private final EventInsertRepository eventInsertRepository;
+ private final RuleNodeDebugEventRepository ruleNodeDebugEventRepository;
+ private final RuleChainDebugEventRepository ruleChainDebugEventRepository;
+ private final ScheduledLogExecutorComponent logExecutor;
+ private final StatsFactory statsFactory;
@Value("${sql.events.batch_size:10000}")
private int batchSize;
@@ -223,11 +201,6 @@ public class JpaBaseEventDao implements EventDao {
}
}
- @Override
- public void migrateEvents(long regularEventTs, long debugEventTs) {
- eventCleanupRepository.migrateEvents(regularEventTs, debugEventTs);
- }
-
private PageData extends Event> findEventByFilter(UUID tenantId, UUID entityId, RuleChainDebugEventFilter eventFilter, TimePageLink pageLink) {
return DaoUtil.toPageData(
ruleChainDebugEventRepository.findEvents(
@@ -402,7 +375,7 @@ public class JpaBaseEventDao implements EventDao {
if (regularEventExpTs > 0) {
log.info("Going to cleanup regular events with exp time: {}", regularEventExpTs);
if (cleanupDb) {
- eventCleanupRepository.cleanupEvents(regularEventExpTs, false);
+ cleanupEvents(regularEventExpTs, false);
} else {
cleanupPartitionsCache(regularEventExpTs, false);
}
@@ -410,13 +383,25 @@ public class JpaBaseEventDao implements EventDao {
if (debugEventExpTs > 0) {
log.info("Going to cleanup debug events with exp time: {}", debugEventExpTs);
if (cleanupDb) {
- eventCleanupRepository.cleanupEvents(debugEventExpTs, true);
+ cleanupEvents(debugEventExpTs, true);
} else {
cleanupPartitionsCache(debugEventExpTs, true);
}
}
}
+ private void cleanupEvents(long eventExpTime, boolean debug) {
+ for (EventType eventType : EventType.values()) {
+ if (eventType.isDebug() == debug) {
+ cleanupPartitions(eventType, eventExpTime);
+ }
+ }
+ }
+
+ private void cleanupPartitions(EventType eventType, long eventExpTime) {
+ partitioningRepository.dropPartitionsBefore(eventType.getTable(), eventExpTime, partitionConfiguration.getPartitionSizeInMs(eventType));
+ }
+
private void cleanupPartitionsCache(long expTime, boolean isDebug) {
for (EventType eventType : EventType.values()) {
if (eventType.isDebug() == isDebug) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/SqlEventCleanupRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/SqlEventCleanupRepository.java
deleted file mode 100644
index d1e8aa380f..0000000000
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/SqlEventCleanupRepository.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * 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.
- */
-package org.thingsboard.server.dao.sql.event;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.dao.DataAccessException;
-import org.springframework.stereotype.Repository;
-import org.thingsboard.server.common.data.event.EventType;
-import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
-import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
-
-import java.util.concurrent.TimeUnit;
-
-
-@Slf4j
-@Repository
-public class SqlEventCleanupRepository extends JpaAbstractDaoListeningExecutorService implements EventCleanupRepository {
-
- @Autowired
- private EventPartitionConfiguration partitionConfiguration;
- @Autowired
- private SqlPartitioningRepository partitioningRepository;
-
- @Override
- public void cleanupEvents(long eventExpTime, boolean debug) {
- for (EventType eventType : EventType.values()) {
- if (eventType.isDebug() == debug) {
- cleanupEvents(eventType, eventExpTime);
- }
- }
- }
-
- @Override
- public void migrateEvents(long regularEventTs, long debugEventTs) {
- regularEventTs = Math.max(regularEventTs, 1480982400000L);
- debugEventTs = Math.max(debugEventTs, 1480982400000L);
-
- callMigrateFunctionByPartitions("regular", "migrate_regular_events", regularEventTs, partitionConfiguration.getRegularPartitionSizeInHours());
- callMigrateFunctionByPartitions("debug", "migrate_debug_events", debugEventTs, partitionConfiguration.getDebugPartitionSizeInHours());
-
- try {
- jdbcTemplate.execute("DROP PROCEDURE IF EXISTS migrate_regular_events(bigint, bigint, int)");
- jdbcTemplate.execute("DROP PROCEDURE IF EXISTS migrate_debug_events(bigint, bigint, int)");
- jdbcTemplate.execute("DROP TABLE IF EXISTS event");
- } catch (DataAccessException e) {
- log.error("Error occurred during drop of the `events` table", e);
- throw e;
- }
- }
-
- private void callMigrateFunctionByPartitions(String logTag, String functionName, long startTs, int partitionSizeInHours) {
- long currentTs = System.currentTimeMillis();
- var regularPartitionStepInMs = TimeUnit.HOURS.toMillis(partitionSizeInHours);
- long numberOfPartitions = (currentTs - startTs) / regularPartitionStepInMs;
- if (numberOfPartitions > 1000) {
- log.error("Please adjust your {} events partitioning configuration. " +
- "Configuration with partition size of {} hours and corresponding TTL will use {} (>1000) partitions which is not recommended!",
- logTag, partitionSizeInHours, numberOfPartitions);
- throw new RuntimeException("Please adjust your " + logTag + " events partitioning configuration. " +
- "Configuration with partition size of " + partitionSizeInHours + " hours and corresponding TTL will use " +
- +numberOfPartitions + " (>1000) partitions which is not recommended!");
- }
- while (startTs < currentTs) {
- var endTs = startTs + regularPartitionStepInMs;
- log.info("Migrate {} events for time period: [{},{}]", logTag, startTs, endTs);
- callMigrateFunction(functionName, startTs, startTs + regularPartitionStepInMs, partitionSizeInHours);
- startTs = endTs;
- }
- log.info("Migrate {} events done.", logTag);
- }
-
- private void callMigrateFunction(String functionName, long startTs, long endTs, int partitionSizeInHours) {
- try {
- jdbcTemplate.update("CALL " + functionName + "(?, ?, ?)", startTs, endTs, partitionSizeInHours);
- } catch (DataAccessException e) {
- if (e.getMessage() == null || !e.getMessage().contains("relation \"event\" does not exist")) {
- log.error("[{}] SQLException occurred during execution of {} with parameters {} and {}", functionName, startTs, partitionSizeInHours, e);
- throw new RuntimeException(e);
- }
- }
- }
-
- private void cleanupEvents(EventType eventType, long eventExpTime) {
- partitioningRepository.dropPartitionsBefore(eventType.getTable(), eventExpTime, partitionConfiguration.getPartitionSizeInMs(eventType));
- }
-
-}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java
index cb2ab5aa42..f8927aaa81 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
@@ -68,11 +69,13 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId,
ResourceType resourceType,
+ ResourceSubType resourceSubType,
PageLink pageLink) {
return DaoUtil.toPageData(resourceRepository.findResourcesPage(
tenantId.getId(),
TenantId.SYS_TENANT_ID.getId(),
resourceType.name(),
+ resourceSubType != null ? resourceSubType.name() : null,
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)
));
@@ -80,6 +83,7 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourcesByTenantIdAndResourceType(TenantId tenantId, ResourceType resourceType,
+ ResourceSubType resourceSubType,
String[] objectIds,
String searchText) {
return objectIds == null ?
@@ -87,6 +91,7 @@ public class JpaTbResourceDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes();
return DaoUtil.toPageData(resourceInfoRepository
.findAllTenantResourcesByTenantId(
filter.getTenantId().getId(), TenantId.NULL_UUID,
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
+ CollectionsUtil.isEmpty(resourceSubTypes) ? null :
+ resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@@ -77,10 +81,13 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao resourceSubTypes = filter.getResourceSubTypes();
return DaoUtil.toPageData(resourceInfoRepository
.findTenantResourcesByTenantId(
filter.getTenantId().getId(),
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
+ CollectionsUtil.isEmpty(resourceSubTypes) ? null :
+ resourceSubTypes.stream().map(Enum::name).collect(Collectors.toList()),
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)));
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java
index c092ea3292..00c51b50a2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java
@@ -37,19 +37,23 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("systemTenantId") UUID systemTenantId,
@Param("resourceTypes") List resourceTypes,
+ @Param("resourceSubTypes") List resourceSubTypes,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
"ri.tenantId = :tenantId " +
"AND ri.resourceType IN :resourceTypes " +
+ "AND (:resourceSubTypes IS NULL OR ri.resourceSubType IN :resourceSubTypes) " +
"AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)")
Page findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("resourceTypes") List resourceTypes,
+ @Param("resourceSubTypes") List resourceSubTypes,
@Param("searchText") String searchText,
Pageable pageable);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java
index f5a831ff64..ce2e6b890b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java
@@ -34,6 +34,7 @@ public interface TbResourceRepository extends JpaRepository findResources(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysAdminId,
@Param("resourceType") String resourceType,
+ @Param("resourceSubType") String resourceSubType,
@Param("searchText") String searchText);
@Query("SELECT tr FROM TbResourceEntity tr " +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
index 0adfe1f2be..6fb4bcb85d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.DaoUtil;
@@ -84,57 +85,60 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
- boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
- boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
+ public PageData findSystemWidgetTypes(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty();
return DaoUtil.toPageData(
widgetTypeInfoRepository
.findSystemWidgetTypes(
NULL_UUID,
pageLink.getTextSearch(),
- fullSearch,
+ widgetTypeFilter.isFullSearch(),
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
- widgetTypes == null ? Collections.emptyList() : widgetTypes,
+ widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(),
+ widgetTypeFilter.isScadaFirst(),
DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP)));
}
@Override
- public PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
- boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
- boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
+ public PageData findAllTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty();
return DaoUtil.toPageData(
widgetTypeInfoRepository
.findAllTenantWidgetTypesByTenantId(
- tenantId,
+ widgetTypeFilter.getTenantId().getId(),
NULL_UUID,
pageLink.getTextSearch(),
- fullSearch,
+ widgetTypeFilter.isFullSearch(),
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
- widgetTypes == null ? Collections.emptyList() : widgetTypes,
+ widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(),
+ widgetTypeFilter.isScadaFirst(),
DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP)));
}
@Override
- public PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
- boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
- boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
+ public PageData findTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(widgetTypeFilter.getDeprecatedFilter());
+ boolean widgetTypesEmpty = widgetTypeFilter.getWidgetTypes() == null || widgetTypeFilter.getWidgetTypes().isEmpty();
return DaoUtil.toPageData(
widgetTypeInfoRepository
.findTenantWidgetTypesByTenantId(
- tenantId,
+ widgetTypeFilter.getTenantId().getId(),
pageLink.getTextSearch(),
- fullSearch,
+ widgetTypeFilter.isFullSearch(),
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
- widgetTypes == null ? Collections.emptyList() : widgetTypes,
+ widgetTypeFilter.getWidgetTypes() == null ? Collections.emptyList() : widgetTypeFilter.getWidgetTypes(),
+ widgetTypeFilter.isScadaFirst(),
DaoUtil.toPageable(pageLink, WidgetTypeInfoEntity.SEARCH_COLUMNS_MAP)));
}
@@ -176,6 +180,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java
index 668eb0bcbf..290e949c38 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
@@ -64,8 +65,8 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
- if (fullSearch) {
+ public PageData findSystemWidgetsBundles(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ if (widgetsBundleFilter.isFullSearch()) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findSystemWidgetsBundlesFullSearch(
@@ -93,13 +94,13 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) {
- return findTenantWidgetsBundlesByTenantIds(Arrays.asList(tenantId, NULL_UUID), fullSearch, pageLink);
+ public PageData findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ return findTenantWidgetsBundlesByTenantIds(Arrays.asList(widgetsBundleFilter.getTenantId().getId(), NULL_UUID), widgetsBundleFilter, pageLink);
}
@Override
- public PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) {
- return findTenantWidgetsBundlesByTenantIds(Collections.singletonList(tenantId), fullSearch, pageLink);
+ public PageData findTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ return findTenantWidgetsBundlesByTenantIds(Collections.singletonList(widgetsBundleFilter.getTenantId().getId()), widgetsBundleFilter, pageLink);
}
@Override
@@ -107,13 +108,14 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findTenantWidgetsBundlesByTenantIds(List tenantIds, boolean fullSearch, PageLink pageLink) {
- if (fullSearch) {
+ private PageData findTenantWidgetsBundlesByTenantIds(List tenantIds, WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ if (widgetsBundleFilter.isFullSearch()) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findAllTenantWidgetsBundlesByTenantIdsFullSearch(
tenantIds,
pageLink.getTextSearch(),
+ widgetsBundleFilter.isScadaFirst(),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.toPageData(
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
index 85079bb7a8..4b85fcb7dc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
@@ -41,7 +41,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes,
+ @Param("scadaFirst") boolean scadaFirst,
Pageable pageable);
@Query(nativeQuery = true,
@@ -80,7 +82,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes,
+ @Param("scadaFirst") boolean scadaFirst,
Pageable pageable);
@Query(nativeQuery = true,
@@ -120,7 +124,8 @@ public interface WidgetTypeInfoRepository extends JpaRepository widgetTypes,
+ @Param("scadaFirst") boolean scadaFirst,
Pageable pageable);
@Query("SELECT wti FROM WidgetTypeInfoEntity wti, WidgetsBundleWidgetEntity wbw " +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
index 396e82561c..a87a96ac36 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
@@ -67,6 +67,8 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java
index 26e13b1650..8de78986ae 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java
@@ -106,7 +106,7 @@ public interface WidgetsBundleRepository extends JpaRepository findAllTenantWidgetsBundlesByTenantIdsFullSearch(@Param("tenantIds") List tenantIds,
- @Param("textSearch") String textSearch,
- Pageable pageable);
+ @Param("textSearch") String textSearch,
+ @Param("scadaFirst") boolean scadaFirst,
+ Pageable pageable);
WidgetsBundleEntity findFirstByTenantIdAndTitle(UUID tenantId, String title);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/DedicatedEventsSqlPartitioningRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/DedicatedEventsSqlPartitioningRepository.java
new file mode 100644
index 0000000000..7e1be6bbd9
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/DedicatedEventsSqlPartitioningRepository.java
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.sqlts.insert.sql;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.server.dao.config.DedicatedEventsDataSource;
+import org.thingsboard.server.dao.timeseries.SqlPartition;
+
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_JDBC_TEMPLATE;
+import static org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig.EVENTS_TRANSACTION_MANAGER;
+
+@DedicatedEventsDataSource
+@Repository
+public class DedicatedEventsSqlPartitioningRepository extends SqlPartitioningRepository {
+
+ @Autowired
+ @Qualifier(EVENTS_JDBC_TEMPLATE)
+ private JdbcTemplate jdbcTemplate;
+
+ @Transactional(propagation = Propagation.NOT_SUPPORTED, transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public void save(SqlPartition partition) {
+ super.save(partition);
+ }
+
+ @Transactional(propagation = Propagation.NOT_SUPPORTED, transactionManager = EVENTS_TRANSACTION_MANAGER)
+ @Override
+ public void createPartitionIfNotExists(String table, long entityTs, long partitionDurationMs) {
+ super.createPartitionIfNotExists(table, entityTs, partitionDurationMs);
+ }
+
+ @Override
+ protected JdbcTemplate getJdbcTemplate() {
+ return jdbcTemplate;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
index 83a7415780..0f3922e070 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Primary;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@@ -32,6 +33,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
+@Primary
@Repository
@Slf4j
public class SqlPartitioningRepository {
@@ -49,7 +51,7 @@ public class SqlPartitioningRepository {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void save(SqlPartition partition) {
- jdbcTemplate.execute(partition.getQuery());
+ getJdbcTemplate().execute(partition.getQuery());
}
@Transactional(propagation = Propagation.NOT_SUPPORTED) // executing non-transactionally, so that parent transaction is not aborted on partition save error
@@ -119,8 +121,8 @@ public class SqlPartitioningRepository {
String dropStmtStr = "DROP TABLE " + tablePartition;
try {
- jdbcTemplate.execute(detachPsqlStmtStr);
- jdbcTemplate.execute(dropStmtStr);
+ getJdbcTemplate().execute(detachPsqlStmtStr);
+ getJdbcTemplate().execute(dropStmtStr);
return true;
} catch (DataAccessException e) {
log.error("[{}] Error occurred trying to detach and drop the partition {} ", table, partitionTs, e);
@@ -134,7 +136,7 @@ public class SqlPartitioningRepository {
public List fetchPartitions(String table) {
List partitions = new ArrayList<>();
- List partitionsTables = jdbcTemplate.queryForList(SELECT_PARTITIONS_STMT, String.class, table);
+ List partitionsTables = getJdbcTemplate().queryForList(SELECT_PARTITIONS_STMT, String.class, table);
for (String partitionTableName : partitionsTables) {
String partitionTsStr = partitionTableName.substring(table.length() + 1);
try {
@@ -153,7 +155,7 @@ public class SqlPartitioningRepository {
private synchronized int getCurrentServerVersion() {
if (currentServerVersion == null) {
try {
- currentServerVersion = jdbcTemplate.queryForObject("SELECT current_setting('server_version_num')", Integer.class);
+ currentServerVersion = getJdbcTemplate().queryForObject("SELECT current_setting('server_version_num')", Integer.class);
} catch (Exception e) {
log.warn("Error occurred during fetch of the server version", e);
}
@@ -164,4 +166,8 @@ public class SqlPartitioningRepository {
return currentServerVersion;
}
+ protected JdbcTemplate getJdbcTemplate() {
+ return jdbcTemplate;
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
index db312d7605..e92adba01d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
@@ -19,6 +19,8 @@ import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -39,6 +41,7 @@ import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.w3c.dom.Document;
@@ -234,6 +237,17 @@ public class ImageUtils {
return originalImage;
}
+ public static ScadaSymbolMetadataInfo processScadaSymbolMetadata(String fileName, byte[] data) throws Exception {
+ SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
+ Document document = factory.createDocument(null, new ByteArrayInputStream(data));
+ var metaElements = document.getElementsByTagName("tb:metadata");
+ JsonNode metaData = null;
+ if (metaElements.getLength() > 0) {
+ metaData = JacksonUtil.toJsonNode(metaElements.item(0).getTextContent());
+ }
+ return new ScadaSymbolMetadataInfo(fileName, metaData);
+ }
+
private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) {
if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) {
return new int[]{originalWidth, originalHeight};
@@ -264,4 +278,45 @@ public class ImageUtils {
private ProcessedImage preview;
}
+ @Data
+ public static class ScadaSymbolMetadataInfo {
+ private String title;
+ private String description;
+ private String[] searchTags;
+ private int widgetSizeX;
+ private int widgetSizeY;
+
+ public ScadaSymbolMetadataInfo(String fileName, JsonNode metaData) {
+ if (metaData != null && metaData.has("title")) {
+ title = metaData.get("title").asText();
+ } else {
+ title = fileName;
+ }
+ if (metaData != null && metaData.has("description")) {
+ description = metaData.get("description").asText();
+ } else {
+ description = "";
+ }
+ if (metaData != null && metaData.has("searchTags") && metaData.get("searchTags").isArray()) {
+ var tagsNode = (ArrayNode) metaData.get("searchTags");
+ searchTags = new String[tagsNode.size()];
+ for (int i = 0; i < tagsNode.size(); i++) {
+ searchTags[i] = tagsNode.get(i).asText();
+ }
+ } else {
+ searchTags = new String[0];
+ }
+ if (metaData != null && metaData.has("widgetSizeX")) {
+ widgetSizeX = metaData.get("widgetSizeX").asInt();
+ } else {
+ widgetSizeX = 3;
+ }
+ if (metaData != null && metaData.has("widgetSizeY")) {
+ widgetSizeY = metaData.get("widgetSizeY").asInt();
+ } else {
+ widgetSizeY = 3;
+ }
+ }
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
index 0a7cabb457..d90c105aae 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.Dao;
@@ -55,11 +56,11 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD
boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId);
- PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findSystemWidgetTypes(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
- PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findAllTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
- PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink);
+ PageData findTenantWidgetTypesByTenantId(WidgetTypeFilter widgetTypeFilter, PageLink pageLink);
/**
* Find widget types by widgetsBundleId.
@@ -92,6 +93,8 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD
*/
WidgetType findByTenantIdAndFqn(UUID tenantId, String fqn);
+ WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn);
+
/**
* Find widget types infos by tenantId and resourceId in descriptor.
*
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
index f2506a2d9b..3e2fe0bf35 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
@@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
@@ -121,28 +122,30 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
}
@Override
- public PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ public PageData findSystemWidgetTypesByPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ log.trace("Executing findSystemWidgetTypesByPageLink, pageLink [{}]", pageLink);
Validator.validatePageLink(pageLink);
- return widgetTypeDao.findSystemWidgetTypes(tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ return widgetTypeDao.findSystemWidgetTypes(widgetTypeFilter, pageLink);
}
@Override
- public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]",
- tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ TenantId tenantId = widgetTypeFilter.getTenantId();
+ log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]",
+ tenantId, pageLink);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Validator.validatePageLink(pageLink);
- return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ return widgetTypeDao.findAllTenantWidgetTypesByTenantId(widgetTypeFilter, pageLink);
}
@Override
- public PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink) {
- log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]",
- tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ public PageData findTenantWidgetTypesByTenantIdAndPageLink(WidgetTypeFilter widgetTypeFilter, PageLink pageLink) {
+ TenantId tenantId = widgetTypeFilter.getTenantId();
+ log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]",
+ tenantId, pageLink);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Validator.validatePageLink(pageLink);
- return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink);
+ return widgetTypeDao.findTenantWidgetTypesByTenantId(widgetTypeFilter, pageLink);
}
@Override
@@ -189,6 +192,15 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findByTenantIdAndFqn(tenantId.getId(), fqn);
}
+ @Override
+ public WidgetTypeDetails findWidgetTypeDetailsByTenantIdAndFqn(TenantId tenantId, String fqn) {
+ log.trace("Executing findWidgetTypeDetailsByTenantIdAndFqn, tenantId [{}], fqn [{}]", tenantId, fqn);
+ Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
+ Validator.validateString(fqn, f -> "Incorrect fqn " + f);
+ return widgetTypeDao.findDetailsByTenantIdAndFqn(tenantId.getId(), fqn);
+ }
+
+
@Override
public void updateWidgetsBundleWidgetTypes(TenantId tenantId, WidgetsBundleId widgetsBundleId, List widgetTypeIds) {
log.trace("Executing updateWidgetsBundleWidgetTypes, tenantId [{}], widgetsBundleId [{}], widgetTypeIds [{}]", tenantId, widgetsBundleId, widgetTypeIds);
@@ -251,7 +263,13 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
@Override
protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
- return widgetTypeDao.findTenantWidgetTypesByTenantId(id.getId(), false, DeprecatedFilter.ALL, null, pageLink);
+ return widgetTypeDao.findTenantWidgetTypesByTenantId(
+ WidgetTypeFilter.builder()
+ .tenantId(id)
+ .fullSearch(false)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ pageLink);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
index b54cd66499..a37149ae88 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ImageContainerDao;
@@ -55,7 +56,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa
* @param pageLink the page link
* @return the list of widgets bundles objects
*/
- PageData findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findSystemWidgetsBundles(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
/**
* Find tenant widgets bundles by tenantId and page link.
@@ -73,7 +74,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa
* @param pageLink the page link
* @return the list of widgets bundles objects
*/
- PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
/**
* Find all tenant widgets bundles (does not include system) by tenantId and page link.
@@ -82,7 +83,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa
* @param pageLink the page link
* @return the list of widgets bundles objects
*/
- PageData findTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink);
+ PageData findTenantWidgetsBundlesByTenantId(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink);
PageData findAllWidgetsBundles(PageLink pageLink);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
index ae53c70ec8..d7006e9e56 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@@ -118,10 +119,10 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
@Override
- public PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
- log.trace("Executing findSystemWidgetsBundles, fullSearch [{}], pageLink [{}]", fullSearch, pageLink);
+ public PageData findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ log.trace("Executing findSystemWidgetsBundles, widgetsBundleFilter [{}], pageLink [{}]", widgetsBundleFilter, pageLink);
Validator.validatePageLink(pageLink);
- return widgetsBundleDao.findSystemWidgetsBundles(tenantId, fullSearch, pageLink);
+ return widgetsBundleDao.findSystemWidgetsBundles(widgetsBundleFilter, pageLink);
}
@Override
@@ -131,7 +132,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT);
PageData pageData;
do {
- pageData = findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
+ pageData = findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
widgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -149,19 +150,21 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
@Override
- public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
- log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink);
+ public PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ TenantId tenantId = widgetsBundleFilter.getTenantId();
+ log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Validator.validatePageLink(pageLink);
- return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink);
+ return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(widgetsBundleFilter, pageLink);
}
@Override
- public PageData findTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
- log.trace("Executing findTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink);
+ public PageData findTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter widgetsBundleFilter, PageLink pageLink) {
+ TenantId tenantId = widgetsBundleFilter.getTenantId();
+ log.trace("Executing findTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Validator.validatePageLink(pageLink);
- return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink);
+ return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(widgetsBundleFilter, pageLink);
}
@Override
@@ -172,7 +175,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT);
PageData pageData;
do {
- pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
+ pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
widgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 9cd734d80b..66fe8dc1bb 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -506,6 +506,7 @@ CREATE TABLE IF NOT EXISTS widget_type (
name varchar(255),
tenant_id uuid,
image varchar(1000000),
+ scada boolean NOT NULL DEFAULT false,
deprecated boolean NOT NULL DEFAULT false,
description varchar(1024),
tags text[],
@@ -522,6 +523,7 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
tenant_id uuid,
title varchar(255),
image varchar(1000000),
+ scada boolean NOT NULL DEFAULT false,
description varchar(1024),
widgets_bundle_order int,
external_id uuid,
@@ -737,6 +739,7 @@ CREATE TABLE IF NOT EXISTS resource (
tenant_id uuid NOT NULL,
title varchar(255) NOT NULL,
resource_type varchar(32) NOT NULL,
+ resource_sub_type varchar(32),
resource_key varchar(255) NOT NULL,
search_text varchar(255),
file_name varchar(255) NOT NULL,
diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java
index efa529cb5e..c4bb8fb336 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractDaoServiceTest.java
@@ -25,10 +25,14 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.thingsboard.server.common.stats.StatsFactory;
+import org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig;
+import org.thingsboard.server.dao.config.JpaDaoConfig;
+import org.thingsboard.server.dao.config.SqlTsDaoConfig;
+import org.thingsboard.server.dao.config.SqlTsLatestDaoConfig;
import org.thingsboard.server.dao.service.DaoSqlTest;
@RunWith(SpringRunner.class)
-@ContextConfiguration(classes = {JpaDaoConfig.class, SqlTsDaoConfig.class, SqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class})
+@ContextConfiguration(classes = {JpaDaoConfig.class, SqlTsDaoConfig.class, SqlTsLatestDaoConfig.class, DedicatedEventsJpaDaoConfig.class})
@DaoSqlTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@TestExecutionListeners({
diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractJpaDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractJpaDaoTest.java
index 8682aa5968..f3ddda769d 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/AbstractJpaDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractJpaDaoTest.java
@@ -24,13 +24,18 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.thingsboard.server.common.stats.StatsFactory;
+import org.thingsboard.server.dao.config.DedicatedEventsJpaDaoConfig;
+import org.thingsboard.server.dao.config.DefaultDedicatedJpaDaoConfig;
+import org.thingsboard.server.dao.config.JpaDaoConfig;
+import org.thingsboard.server.dao.config.SqlTsDaoConfig;
+import org.thingsboard.server.dao.config.SqlTsLatestDaoConfig;
import org.thingsboard.server.dao.service.DaoSqlTest;
/**
* Created by Valerii Sosliuk on 4/22/2017.
*/
@RunWith(SpringRunner.class)
-@ContextConfiguration(classes = {JpaDaoConfig.class, SqlTsDaoConfig.class, SqlTsLatestDaoConfig.class, SqlTimeseriesDaoConfig.class})
+@ContextConfiguration(classes = {JpaDaoConfig.class, SqlTsDaoConfig.class, SqlTsLatestDaoConfig.class, DedicatedEventsJpaDaoConfig.class, DefaultDedicatedJpaDaoConfig.class})
@DaoSqlTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
diff --git a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
index 4feef028d4..80938b5724 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
@@ -63,4 +63,5 @@ public class PostgreSqlInitializer {
throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e);
}
}
+
}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java
index 314a361dd2..d4c00fdf39 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@@ -174,7 +175,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
PageLink pageLink = new PageLink(19);
PageData pageData = null;
do {
- pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
+ pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -297,7 +298,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
PageLink pageLink = new PageLink(17);
PageData pageData = null;
do {
- pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -314,7 +315,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
loadedWidgetsBundles.clear();
pageLink = new PageLink(14);
do {
- pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@@ -336,7 +337,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
loadedWidgetsBundles.clear();
pageLink = new PageLink(18);
do {
- pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
+ pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/sql/EventServiceSqlTest_DedicatedEventsDataSource.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/sql/EventServiceSqlTest_DedicatedEventsDataSource.java
new file mode 100644
index 0000000000..cb765c29d0
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/sql/EventServiceSqlTest_DedicatedEventsDataSource.java
@@ -0,0 +1,28 @@
+/**
+ * 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.
+ */
+package org.thingsboard.server.dao.service.event.sql;
+
+import org.springframework.test.context.TestPropertySource;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+@DaoSqlTest
+@TestPropertySource(properties = {
+ "spring.datasource.events.enabled=true",
+ "spring.datasource.events.url=${spring.datasource.url}",
+ "spring.datasource.events.driverClassName=${spring.datasource.driverClassName}"
+})
+public class EventServiceSqlTest_DedicatedEventsDataSource extends EventServiceSqlTest {
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java
index e8f3bba9db..8010eab4cd 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java
@@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
@@ -151,12 +152,22 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
@Test
public void testFindSystemWidgetTypes() {
- PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"),
+ PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes(
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(Collections.singletonList("static")).build(),
new PageLink(1024, 0, "TYPE_DESCRIPTION", new SortOrder("createdTime")));
assertEquals(1, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(1)), widgetTypes.getData().get(0));
- widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.emptyList(),
+ widgetTypes = widgetTypeDao.findSystemWidgetTypes(
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(Collections.emptyList()).build(),
new PageLink(1024, 0, "hfgfd tag2_2 ghg", new SortOrder("createdTime")));
assertEquals(1, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(2)), widgetTypes.getData().get(0));
@@ -175,12 +186,21 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
sameNameList.sort(Comparator.comparing(BaseWidgetType::getName).thenComparing((BaseWidgetType baseWidgetType) -> baseWidgetType.getId().getId()));
List expected = sameNameList.stream().map(WidgetTypeInfo::new).collect(Collectors.toList());
- PageData widgetTypesFirstPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"),
+ PageData widgetTypesFirstPage = widgetTypeDao.findSystemWidgetTypes(
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(Collections.singletonList("static")).build(),
new PageLink(10, 0, null, new SortOrder("name")));
assertEquals(10, widgetTypesFirstPage.getData().size());
assertThat(widgetTypesFirstPage.getData()).containsExactlyElementsOf(expected.subList(0, 10));
- PageData widgetTypesSecondPage = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"),
+ PageData widgetTypesSecondPage = widgetTypeDao.findSystemWidgetTypes(WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(Collections.singletonList("static")).build(),
new PageLink(10, 1, null, new SortOrder("name")));
assertEquals(10, widgetTypesSecondPage.getData().size());
assertThat(widgetTypesSecondPage.getData()).containsExactlyElementsOf(expected.subList(10, 20));
@@ -195,7 +215,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes(
- TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(1);
@@ -211,7 +236,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findSystemWidgetTypes(
- TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(0);
@@ -227,7 +257,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
var widgetType = createAndSaveWidgetType(new TenantId(tenantId), i);
widgetTypeList.add(widgetType);
}
- PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId, true, DeprecatedFilter.ALL, null,
+ PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(
+ WidgetTypeFilter.builder()
+ .tenantId(new TenantId(tenantId))
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
new PageLink(10, 0, "", new SortOrder("createdTime")));
assertEquals(WIDGET_TYPE_COUNT, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(3)), widgetTypes.getData().get(0));
@@ -244,7 +279,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(1);
@@ -260,7 +300,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(0);
@@ -278,7 +323,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findAllTenantWidgetTypesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(1);
@@ -294,7 +344,12 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
WidgetTypeDetails savedWidgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, WIDGET_TYPE_COUNT + 1, tags);
PageData widgetTypes = widgetTypeDao.findAllTenantWidgetTypesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, DeprecatedFilter.ALL, null, new PageLink(10, 0, searchText)
+ WidgetTypeFilter.builder()
+ .tenantId(TenantId.SYS_TENANT_ID)
+ .fullSearch(true)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(0);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
index e2221dcc93..5ec50189c4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
@@ -42,6 +43,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
@@ -113,11 +116,11 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
assertEquals(30, widgetsBundles.size());
// Get first page
PageLink pageLink = new PageLink(10, 0, "WB");
- PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink);
+ PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fromTenantId(TenantId.SYS_TENANT_ID), pageLink);
assertEquals(10, widgetsBundles1.getData().size());
// Get next page
pageLink = pageLink.nextPageLink();
- PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink);
+ PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fromTenantId(TenantId.SYS_TENANT_ID), pageLink);
assertEquals(10, widgetsBundles2.getData().size());
}
@@ -142,19 +145,19 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1));
PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title"));
- PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
+ PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink);
assertEquals(2, widgetsBundles1.getData().size());
assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles1.getData().get(1));
pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title"));
- PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
+ PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink);
assertEquals(2, widgetsBundles2.getData().size());
assertEquals(widgetsBundle2, widgetsBundles2.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles2.getData().get(1));
pageLink = new PageLink(10, 0, "ppp Fd v TAG1 tt", new SortOrder("title"));
- PageData widgetsBundles3 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
+ PageData widgetsBundles3 = widgetsBundleDao.findSystemWidgetsBundles(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), pageLink);
assertEquals(2, widgetsBundles3.getData().size());
assertEquals(widgetsBundle1, widgetsBundles3.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1));
@@ -171,7 +174,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0));
PageData widgetTypes = widgetsBundleDao.findSystemWidgetsBundles(
- TenantId.SYS_TENANT_ID, true, new PageLink(10, 0, searchText)
+ WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(1);
@@ -191,7 +194,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0));
PageData widgetTypes = widgetsBundleDao.findSystemWidgetsBundles(
- TenantId.SYS_TENANT_ID, true, new PageLink(10, 0, searchText)
+ WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(0);
@@ -245,19 +248,19 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
assertEquals(100, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size());
PageLink pageLink = new PageLink(30, 0, "WB");
- PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
+ PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(30, widgetsBundles1.getData().size());
pageLink = pageLink.nextPageLink();
- PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
+ PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(30, widgetsBundles2.getData().size());
pageLink = pageLink.nextPageLink();
- PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
+ PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(10, widgetsBundles3.getData().size());
pageLink = pageLink.nextPageLink();
- PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
+ PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(0, widgetsBundles4.getData().size());
}
@@ -287,29 +290,75 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1));
PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title"));
- PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
+ PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(1, widgetsBundles1.getData().size());
assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0));
pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title"));
- PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
+ PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(0, widgetsBundles2.getData().size());
- PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink);
+ PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId2)), pageLink);
assertEquals(2, widgetsBundles3.getData().size());
assertEquals(widgetsBundle2, widgetsBundles3.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1));
pageLink = new PageLink(10, 0, "ttt Tag2 ffff hhhh", new SortOrder("title"));
- PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
+ PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(1, widgetsBundles4.getData().size());
assertEquals(widgetsBundle1, widgetsBundles4.getData().get(0));
- PageData widgetsBundles5 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink);
+ PageData widgetsBundles5 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId2)), pageLink);
assertEquals(1, widgetsBundles5.getData().size());
assertEquals(widgetsBundle3, widgetsBundles5.getData().get(0));
}
+ @Test
+ public void testFindAllWidgetsBundlesByTenantIdFullSearchScadaFirst() {
+ UUID tenantId1 = Uuids.timeBased();
+ UUID tenantId2 = Uuids.timeBased();
+ for (int i = 0; i < 10; i++) {
+ createWidgetBundles(5, tenantId1, "WB1_" + i + "_");
+ createWidgetBundles(2, tenantId1, "WB1_SCADA_" + i + "_", true);
+ createWidgetBundles(3, tenantId2, "WB2_" + i + "_");
+ createWidgetBundles(3, tenantId2, "WB2_SCADA_" + i + "_", true);
+ createSystemWidgetBundles(2, "WB_SYS_" + i + "_");
+ createSystemWidgetBundles(1, "WB_SYS_SCADA_" + i + "_", true);
+ }
+ widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID).stream().sorted(Comparator.comparing(WidgetsBundle::getTitle)).collect(Collectors.toList());;
+ assertEquals(160, widgetsBundles.size());
+
+ PageLink pageLink = new PageLink(50, 0, "WB", new SortOrder("title"));
+ PageData widgetsBundles1 =
+ widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(
+ WidgetsBundleFilter.builder().tenantId(TenantId.fromUUID(tenantId1)).fullSearch(true).scadaFirst(true).build(), pageLink);
+
+ for (int i =0; i < 30; i++) {
+ var widgetsBundle = widgetsBundles1.getData().get(i);
+ assertTrue(widgetsBundle.isScada());
+ }
+
+ for (int i = 30; i < 50; i++) {
+ var widgetsBundle = widgetsBundles1.getData().get(i);
+ assertFalse(widgetsBundle.isScada());
+ }
+
+ pageLink = new PageLink(50, 0, "WB", new SortOrder("title"));
+ PageData widgetsBundles2 =
+ widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(
+ WidgetsBundleFilter.builder().tenantId(TenantId.fromUUID(tenantId2)).fullSearch(true).scadaFirst(true).build(), pageLink);
+
+ for (int i =0; i < 40; i++) {
+ var widgetsBundle = widgetsBundles2.getData().get(i);
+ assertTrue(widgetsBundle.isScada());
+ }
+
+ for (int i = 40; i < 50; i++) {
+ var widgetsBundle = widgetsBundles2.getData().get(i);
+ assertFalse(widgetsBundle.isScada());
+ }
+ }
+
@Test
public void testTagsSearchInFindAllWidgetsBundlesByTenantId() {
for (var entry : SHOULD_FIND_SEARCH_TO_TAGS_MAP.entrySet()) {
@@ -321,7 +370,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0));
PageData widgetTypes = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, new PageLink(10, 0, searchText)
+ WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(1);
@@ -341,7 +390,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(systemWidgetBundle.getId(), widgetType.getId(), 0));
PageData widgetTypes = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(
- TenantId.SYS_TENANT_ID.getId(), true, new PageLink(10, 0, searchText)
+ WidgetsBundleFilter.fullSearchFromTenantId(TenantId.SYS_TENANT_ID), new PageLink(10, 0, searchText)
);
assertThat(widgetTypes.getData()).hasSize(0);
@@ -378,7 +427,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
}).collect(Collectors.toList());;
assertEquals(20, widgetsBundles.size());
PageLink pageLink = new PageLink(100, 0, "", new SortOrder("title"));
- PageData widgetsBundlesData = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
+ PageData widgetsBundlesData = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fullSearchFromTenantId(TenantId.fromUUID(tenantId1)), pageLink);
assertEquals(20, widgetsBundlesData.getData().size());
assertEquals(widgetsBundles, widgetsBundlesData.getData());
}
@@ -391,19 +440,27 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID);
assertEquals(10, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size());
PageLink textPageLink = new PageLink(30, 0, "TEXT_NOT_FOUND");
- PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, false, textPageLink);
+ PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(WidgetsBundleFilter.fromTenantId(TenantId.fromUUID(tenantId)), textPageLink);
assertEquals(0, widgetsBundles4.getData().size());
}
private void createWidgetBundles(int count, UUID tenantId, String prefix) {
+ createWidgetBundles(count, tenantId, prefix, false);
+ }
+
+ private void createWidgetBundles(int count, UUID tenantId, String prefix, boolean scada) {
for (int i = 0; i < count; i++) {
- createWidgetsBundle(TenantId.fromUUID(tenantId), prefix + i, prefix + i, null);
+ createWidgetsBundle(TenantId.fromUUID(tenantId), prefix + i, prefix + i, null, scada);
}
}
private void createSystemWidgetBundles(int count, String prefix) {
+ createSystemWidgetBundles(count, prefix, false);
+ }
+
+ private void createSystemWidgetBundles(int count, String prefix, boolean scada) {
for (int i = 0; i < count; i++) {
- createWidgetsBundle(TenantId.SYS_TENANT_ID, prefix + i, prefix + i, null);
+ createWidgetsBundle(TenantId.SYS_TENANT_ID, prefix + i, prefix + i, null, scada);
}
}
@@ -412,12 +469,17 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
}
private WidgetsBundle createWidgetsBundle(TenantId tenantId, String alias, String title, Integer order) {
+ return createWidgetsBundle(tenantId, alias, title, order, false);
+ }
+
+ private WidgetsBundle createWidgetsBundle(TenantId tenantId, String alias, String title, Integer order, boolean scada) {
WidgetsBundle widgetsBundle = new WidgetsBundle();
widgetsBundle.setAlias(alias);
widgetsBundle.setTitle(title);
widgetsBundle.setTenantId(tenantId);
widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased()));
widgetsBundle.setOrder(order);
+ widgetsBundle.setScada(scada);
return widgetsBundleDao.save(TenantId.SYS_TENANT_ID, widgetsBundle);
}
diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties
index 4292db28d6..eb50e96e75 100644
--- a/dao/src/test/resources/sql-test.properties
+++ b/dao/src/test/resources/sql-test.properties
@@ -14,7 +14,7 @@ spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
spring.datasource.username=postgres
spring.datasource.password=postgres
-spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
+spring.datasource.url=jdbc:tc:postgresql:12.19:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.hikari.maximumPoolSize=16
diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
index 6bc4d82c5b..0aa51402c5 100644
--- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
+++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
@@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
@@ -3627,10 +3628,19 @@ public class RestClient implements Closeable {
}
public PageData getImages(PageLink pageLink, boolean includeSystemImages) {
+ return this.getImages(pageLink, null, includeSystemImages);
+ }
+
+ public PageData getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) {
Map params = new HashMap<>();
+ var url = baseURL + "/api/images?includeSystemImages={includeSystemImages}&";
addPageLinkToParam(params, pageLink);
params.put("includeSystemImages", String.valueOf(includeSystemImages));
- return restTemplate.exchange(baseURL + "/api/images?includeSystemImages={includeSystemImages}&" + getUrlParams(pageLink),
+ if (imageSubType != null) {
+ url += "imageSubType={imageSubType}&";
+ params.put("imageSubType", imageSubType.name());
+ }
+ return restTemplate.exchange(url + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference>() {},
diff --git a/ui-ngx/package.json b/ui-ngx/package.json
index f383054ece..916b72cc15 100644
--- a/ui-ngx/package.json
+++ b/ui-ngx/package.json
@@ -45,6 +45,7 @@
"@ngx-translate/core": "^14.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.0",
+ "@svgdotjs/svg.panzoom.js": "^2.1.2",
"@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",
diff --git a/ui-ngx/patches/angular-gridster2+15.0.4.patch b/ui-ngx/patches/angular-gridster2+15.0.4.patch
new file mode 100644
index 0000000000..a6642928c3
--- /dev/null
+++ b/ui-ngx/patches/angular-gridster2+15.0.4.patch
@@ -0,0 +1,44 @@
+diff --git a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
+index cf4e220..4275d11 100644
+--- a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
++++ b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
+@@ -208,6 +208,7 @@ const GridsterConfigService = {
+ useTransformPositioning: true,
+ scrollSensitivity: 10,
+ scrollSpeed: 20,
++ colWidthUpdateCallback: undefined,
+ initCallback: undefined,
+ destroyCallback: undefined,
+ gridSizeChangedCallback: undefined,
+@@ -1243,6 +1244,9 @@ class GridsterComponent {
+ this.renderer.setStyle(this.el, 'padding-right', this.$options.margin + 'px');
+ }
+ this.curColWidth = (this.curWidth - marginWidth) / this.columns;
++ if (this.options.colWidthUpdateCallback) {
++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth);
++ }
+ let marginHeight = -this.$options.margin;
+ if (this.$options.outerMarginTop !== null) {
+ marginHeight += this.$options.outerMarginTop;
+@@ -1266,6 +1270,9 @@ class GridsterComponent {
+ }
+ else {
+ this.curColWidth = (this.curWidth + this.$options.margin) / this.columns;
++ if (this.options.colWidthUpdateCallback) {
++ this.curColWidth = this.options.colWidthUpdateCallback(this.curColWidth);
++ }
+ this.curRowHeight =
+ ((this.curHeight + this.$options.margin) / this.rows) *
+ this.$options.rowHeightRatio;
+diff --git a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts
+index 1d7cdf0..a712b35 100644
+--- a/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts
++++ b/node_modules/angular-gridster2/lib/gridsterConfig.interface.d.ts
+@@ -73,6 +73,7 @@ export interface GridsterConfig {
+ useTransformPositioning?: boolean;
+ scrollSensitivity?: number | null;
+ scrollSpeed?: number;
++ colWidthUpdateCallback?: (colWidth: number) => number;
+ initCallback?: (gridster: GridsterComponentInterface) => void;
+ destroyCallback?: (gridster: GridsterComponentInterface) => void;
+ gridSizeChangedCallback?: (gridster: GridsterComponentInterface) => void;
diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts
index e18c909c9a..1ebd87c119 100644
--- a/ui-ngx/src/app/app.module.ts
+++ b/ui-ngx/src/app/app.module.ts
@@ -27,6 +27,23 @@ import { AppComponent } from './app.component';
import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module';
import { RouterModule, Routes } from '@angular/router';
+import { DefaultUrlSerializer, UrlSerializer, UrlTree } from '@angular/router';
+
+export default class TbUrlSerializer implements UrlSerializer {
+ private _defaultUrlSerializer: DefaultUrlSerializer = new DefaultUrlSerializer();
+
+ parse(url: string): UrlTree {
+ // Encode parentheses
+ url = url.replace(/\(/g, '%28').replace(/\)/g, '%29');
+ // Use the default serializer.
+ return this._defaultUrlSerializer.parse(url)
+ }
+
+ serialize(tree: UrlTree): string {
+ return this._defaultUrlSerializer.serialize(tree).replace(/%28/g, '(').replace(/%29/g, ')');
+ }
+}
+
const routes: Routes = [
{ path: '**',
redirectTo: 'home'
@@ -55,7 +72,9 @@ export class PageNotFoundRoutingModule { }
DashboardRoutingModule,
PageNotFoundRoutingModule
],
- providers: [],
+ providers: [
+ { provide: UrlSerializer, useClass: TbUrlSerializer }
+ ],
bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts
index 1fcd18bd93..0300c89d7f 100644
--- a/ui-ngx/src/app/core/api/widget-api.models.ts
+++ b/ui-ngx/src/app/core/api/widget-api.models.ts
@@ -23,7 +23,7 @@ import {
DatasourceType,
KeyInfo,
LegendConfig,
- LegendData, TargetDevice,
+ LegendData, TargetDevice, WidgetAction,
WidgetActionDescriptor,
widgetType
} from '@shared/models/widget.models';
@@ -95,6 +95,7 @@ export interface WidgetActionsApi {
getActionDescriptors: (actionSourceId: string) => Array;
handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
+ onWidgetAction: ($event: Event, action: WidgetAction) => void;
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;
diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts
index b5b2b0e796..7446d67601 100644
--- a/ui-ngx/src/app/core/http/entity.service.ts
+++ b/ui-ngx/src/app/core/http/entity.service.ts
@@ -427,11 +427,11 @@ export class EntityService {
break;
case EntityType.WIDGETS_BUNDLE:
pageLink.sortOrder.property = 'title';
- entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, config);
+ entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, false, config);
break;
case EntityType.WIDGET_TYPE:
pageLink.sortOrder.property = 'name';
- entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, DeprecatedFilter.ALL, null, config);
+ entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, false, DeprecatedFilter.ALL, null, config);
break;
case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name';
diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts
index 2339801e2b..eb15b658e5 100644
--- a/ui-ngx/src/app/core/http/image.service.ts
+++ b/ui-ngx/src/app/core/http/image.service.ts
@@ -25,11 +25,11 @@ import {
ImageResourceInfo,
imageResourceType,
ImageResourceType,
- IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix
+ IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix, ResourceSubType
} from '@shared/models/resource.models';
import { catchError, map, switchMap } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
-import { blobToBase64 } from '@core/utils';
+import { blobToBase64, blobToText } from '@core/utils';
import { ResourcesService } from '@core/services/resources.service';
@Injectable({
@@ -46,13 +46,14 @@ export class ImageService {
) {
}
- public uploadImage(file: File, title: string, config?: RequestConfig): Observable {
+ public uploadImage(file: File, title: string, imageSubType: ResourceSubType, config?: RequestConfig): Observable {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
+ formData.append('imageSubType', imageSubType);
return this.http.post('/api/image', formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
@@ -81,9 +82,10 @@ export class ImageService {
imageInfo, defaultHttpOptionsFromConfig(config));
}
- public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable> {
+ public getImages(pageLink: PageLink, imageSubType: ResourceSubType,
+ includeSystemImages = false, config?: RequestConfig): Observable> {
return this.http.get>(
- `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`,
+ `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`,
defaultHttpOptionsFromConfig(config));
}
@@ -130,6 +132,33 @@ export class ImageService {
);
}
+ public getImageString(imageUrl: string): Observable {
+ imageUrl = removeTbImagePrefix(imageUrl);
+ let request: ReplaySubject;
+ if (this.imagesLoading[imageUrl]) {
+ request = this.imagesLoading[imageUrl];
+ } else {
+ request = new ReplaySubject(1);
+ this.imagesLoading[imageUrl] = request;
+ const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
+ this.http.get(imageUrl, {...options, ...{ responseType: 'blob' } }).subscribe({
+ next: (value) => {
+ request.next(value);
+ request.complete();
+ },
+ error: err => {
+ request.error(err);
+ },
+ complete: () => {
+ delete this.imagesLoading[imageUrl];
+ }
+ });
+ }
+ return request.pipe(
+ switchMap(val => blobToText(val))
+ );
+ }
+
public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable {
imageUrl = removeTbImagePrefix(imageUrl);
if (isImageResourceUrl(imageUrl)) {
diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts
index 6f455189a7..b172b61a1d 100644
--- a/ui-ngx/src/app/core/http/widget.service.ts
+++ b/ui-ngx/src/app/core/http/widget.service.ts
@@ -86,9 +86,9 @@ export class WidgetService {
}
public getWidgetBundles(pageLink: PageLink, fullSearch = false,
- tenantOnly = false, config?: RequestConfig): Observable> {
+ tenantOnly = false, scadaFirst = false, config?: RequestConfig): Observable> {
return this.http.get>(
- `/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`,
+ `/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&scadaFirst=${scadaFirst}`,
defaultHttpOptionsFromConfig(config)
);
}
@@ -240,10 +240,13 @@ export class WidgetService {
}
public getWidgetTypes(pageLink: PageLink, tenantOnly = false,
- fullSearch = false, deprecatedFilter = DeprecatedFilter.ALL, widgetTypes: Array = null,
+ fullSearch = false, scadaFirst = false,
+ deprecatedFilter = DeprecatedFilter.ALL,
+ widgetTypes: Array = null,
config?: RequestConfig): Observable> {
let url =
- `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`;
+ `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}
+ &scadaFirst=${scadaFirst}&deprecatedFilter=${deprecatedFilter}`;
if (widgetTypes && widgetTypes.length) {
url += `&widgetTypeList=${widgetTypes.join(',')}`;
}
diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts
index ae4d2e5c41..12a7e687bd 100644
--- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts
+++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts
@@ -50,6 +50,7 @@ import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
+import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
@Injectable({
providedIn: 'root'
@@ -332,6 +333,27 @@ export class DashboardUtilsService {
return widgetConfig;
}
+ public prepareWidgetForScadaLayout(widget: Widget): Widget {
+ const config = widget.config;
+ config.showTitle = false;
+ config.dropShadow = false;
+ config.padding = '0';
+ config.margin = '0';
+ config.backgroundColor = 'rgba(0,0,0,0)';
+ const settings = config.settings || {};
+ settings.padding = '0';
+ const background = settings.background;
+ if (isBackgroundSettings(background)) {
+ background.type = BackgroundType.color;
+ background.color = 'rgba(0,0,0,0)';
+ background.overlay.enabled = false;
+ } else {
+ settings.background = colorBackground('rgba(0,0,0,0)');
+ }
+ config.settings = settings;
+ return widget;
+ }
+
public validateAndUpdateDatasources(datasources?: Datasource[]): Datasource[] {
if (!datasources) {
datasources = [];
@@ -662,7 +684,6 @@ export class DashboardUtilsService {
const columns = gridSettings.columns || 24;
const ratio = columns / prevColumns;
layout.gridSettings = gridSettings;
- let maxRow = 0;
for (const w of Object.keys(layout.widgets)) {
const widget = layout.widgets[w];
if (!widget.sizeX) {
@@ -671,23 +692,23 @@ export class DashboardUtilsService {
if (!widget.sizeY) {
widget.sizeY = 1;
}
- maxRow = Math.max(maxRow, widget.row + widget.sizeY);
}
- const newMaxRow = Math.round(maxRow * ratio);
for (const w of Object.keys(layout.widgets)) {
const widget = layout.widgets[w];
- if (widget.row + widget.sizeY === maxRow) {
- widget.row = Math.round(widget.row * ratio);
- widget.sizeY = newMaxRow - widget.row;
- } else {
- widget.row = Math.round(widget.row * ratio);
- widget.sizeY = Math.round(widget.sizeY * ratio);
- }
- widget.sizeX = Math.round(widget.sizeX * ratio);
+ widget.row = Math.round(widget.row * ratio);
widget.col = Math.round(widget.col * ratio);
- if (widget.col + widget.sizeX > columns) {
- widget.sizeX = columns - widget.col;
- }
+ widget.sizeX = Math.round(widget.sizeX * ratio);
+ widget.sizeY = Math.round(widget.sizeY * ratio);
+ }
+ }
+
+ public moveWidgets(layout: DashboardLayout, cols: number, rows: number) {
+ cols = isDefinedAndNotNull(cols) ? Math.round(cols) : 0;
+ rows = isDefinedAndNotNull(rows) ? Math.round(rows) : 0;
+ for (const w of Object.keys(layout.widgets)) {
+ const widget = layout.widgets[w];
+ widget.col = Math.max(0, widget.col + cols);
+ widget.row = Math.max(0, widget.row + rows);
}
}
diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts
index ffb5b5f0a6..309d82ebc0 100644
--- a/ui-ngx/src/app/core/services/menu.service.ts
+++ b/ui-ngx/src/app/core/services/menu.service.ts
@@ -150,6 +150,13 @@ export class MenuService {
path: '/resources/images',
icon: 'filter'
},
+ {
+ id: 'scada_symbols',
+ name: 'scada.symbols',
+ type: 'link',
+ path: '/resources/scada-symbols',
+ icon: 'view_in_ar'
+ },
{
id: 'resources_library',
name: 'resource.resources-library',
@@ -549,6 +556,13 @@ export class MenuService {
path: '/resources/images',
icon: 'filter'
},
+ {
+ id: 'scada_symbols',
+ name: 'scada.symbols',
+ type: 'link',
+ path: '/resources/scada-symbols',
+ icon: 'view_in_ar'
+ },
{
id: 'resources_library',
name: 'resource.resources-library',
diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts
index a534f157b7..3f27e8289d 100644
--- a/ui-ngx/src/app/core/utils.ts
+++ b/ui-ngx/src/app/core/utils.ts
@@ -217,6 +217,23 @@ export const blobToBase64 = (blob: Blob): Observable => from(new Promise
}
));
+export const blobToText = (blob: Blob): Observable => from(new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.readAsText(blob);
+ }
+));
+
+export const updateFileContent = (file: File, newContent: string): File => {
+ const blob = new Blob([newContent], { type: file.type });
+ return new File([blob], file.name, {type: file.type});
+};
+
+export const createFileFromContent = (content: string, name: string, type: string): File => {
+ const blob = new Blob([content], { type });
+ return new File([blob], name, { type });
+};
+
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
index c7f8980426..8baaa67ead 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
@@ -44,6 +44,7 @@
[dashboard]="dashboard"
[widget]="widget"
[hideHeader]="hideHeader"
+ [scada]="data.scada"
isAdd
formControlName="widgetConfig">
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
index 2089c03d5d..662739295b 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
@@ -37,6 +37,7 @@ export interface AddWidgetDialogData {
stateController: IStateController;
widget: Widget;
widgetInfo: WidgetInfo;
+ scada: boolean;
}
@Component({
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
index e923036571..21a99f70e9 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
@@ -380,6 +380,7 @@
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
+ [scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
@@ -442,6 +443,7 @@
*ngIf="isAddingWidget"
[aliasController]="dashboardCtx.aliasController"
[filterWidgetTypes]="filterWidgetTypes"
+ [scadaFirst]="layouts.main.layoutCtx.gridSettings.layoutType === LayoutType.scada"
(widgetSelected)="addWidgetFromType($event)">
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
index 70022fff1b..d16e8f360f 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
@@ -51,6 +51,7 @@ import {
DashboardStateLayouts,
GridSettings,
LayoutDimension,
+ LayoutType,
WidgetLayout
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
@@ -150,6 +151,10 @@ import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-pag
import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
+import {
+ MoveWidgetsDialogComponent,
+ MoveWidgetsDialogResult
+} from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
// @dynamic
@Component({
@@ -161,6 +166,8 @@ import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
})
export class DashboardPageComponent extends PageComponent implements IDashboardController, HasDirtyFlag, OnInit, AfterViewInit, OnDestroy {
+ LayoutType = LayoutType;
+
private forcePristine = false;
get isDirty(): boolean {
@@ -285,7 +292,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
gridSettings: {},
ignoreLoading: true,
ctrl: null,
- dashboardCtrl: this
+ dashboardCtrl: this,
+ displayGrid: 'onDrag&Resize'
}
},
right: {
@@ -297,7 +305,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
gridSettings: {},
ignoreLoading: true,
ctrl: null,
- dashboardCtrl: this
+ dashboardCtrl: this,
+ displayGrid: 'onDrag&Resize'
}
}
};
@@ -913,6 +922,28 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
});
}
+ private moveWidgets($event: Event, layoutId: DashboardLayoutId) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ this.layouts[layoutId].layoutCtx.displayGrid = 'always';
+ this.cd.markForCheck();
+ this.dialog.open(MoveWidgetsDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
+ }).afterClosed().subscribe((result) => {
+ this.layouts[layoutId].layoutCtx.displayGrid = 'onDrag&Resize';
+ if (result) {
+ const targetLayout = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts[layoutId];
+ this.dashboardUtils.moveWidgets(targetLayout, result.cols, result.rows);
+ this.updateLayouts();
+ } else {
+ this.cd.markForCheck();
+ }
+ });
+ }
+
private updateDashboardLayouts(newLayouts: DashboardStateLayouts) {
this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts);
this.updateLayouts();
@@ -1163,6 +1194,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.runChangeDetection();
}
+ private isAddingToScadaLayout(): boolean {
+ const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts;
+ let layoutIds: DashboardLayoutId[];
+ if (this.addingLayoutCtx?.id) {
+ layoutIds = [this.addingLayoutCtx?.id];
+ } else {
+ layoutIds = Object.keys(layouts) as DashboardLayoutId[];
+ }
+ return layoutIds.every(id => layouts[id].gridSettings.layoutType === LayoutType.scada);
+ }
+
private selectTargetLayout(): Observable {
const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts;
const layoutIds = Object.keys(layouts);
@@ -1208,6 +1250,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
col: 0
};
newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget);
+ const scada = this.isAddingToScadaLayout();
+ if (scada) {
+ newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget);
+ }
if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget);
} else {
@@ -1222,7 +1268,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
aliasController: this.dashboardCtx.aliasController,
stateController: this.dashboardCtx.stateController,
widget: newWidget,
- widgetInfo: widgetTypeInfo
+ widgetInfo: widgetTypeInfo,
+ scada
}
}).afterClosed().subscribe((addedWidget) => {
if (addedWidget) {
@@ -1355,6 +1402,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle);
}
+ dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) {
+ if (this.isEdit && !this.isEditingWidget) {
+ layoutCtx.ctrl.resetHighlight();
+ }
+ }
+
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
if (this.isEditingWidget) {
this.editWidget($event, layoutCtx, widget);
@@ -1408,6 +1461,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
shortcut: 'M-I'
}
);
+ dashboardContextActions.push(
+ {
+ action: ($event) => {
+ this.moveWidgets($event, layoutCtx.id);
+ },
+ enabled: true,
+ value: 'dashboard.move-all-widgets',
+ icon: 'open_with'
+ }
+ );
}
return dashboardContextActions;
}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
index b2ed609c29..0c43be1e91 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
@@ -21,6 +21,7 @@ import { IAliasController, IStateController } from '@core/api/widget-api.models'
import { ILayoutController } from './layout/layout.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs';
+import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
export declare type DashboardPageScope = 'tenant' | 'customer';
@@ -52,6 +53,7 @@ export interface IDashboardController {
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
+ dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array