Commit 79588a7b708b23dec7d3ad356a36a08563d5aed6

Authored by Igor Kulikov
1 parent b3534942

Update help assets

Showing 31 changed files with 1705 additions and 97 deletions
1 1 <ul>
2 2 <li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object.
3 3 </li>
4   - <li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object.
  4 + <li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value map, where both keys and values are strings.
5 5 </li>
6   - <li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values.
  6 + <li><b>msgType:</b> <code>string</code> - is a string containing Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values.
7 7 </li>
8 8 </ul>
  9 +
  10 +Enable 'debug mode' for your rule node to see the messages that arrive in near real-time.
  11 +See <a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/overview/#debugging" target="_blank">Debugging</a> for more information.
\ No newline at end of file
... ...
... ... @@ -5,7 +5,7 @@
5 5
6 6 *function Filter(msg, metadata, msgType): boolean*
7 7
8   -JavaScript function evaluating **true/false** condition on incoming Message.
  8 +JavaScript function defines a boolean expression based on the incoming Message and Metadata.
9 9
10 10 **Parameters:**
11 11
... ... @@ -13,19 +13,34 @@ JavaScript function evaluating **true/false** condition on incoming Message.
13 13
14 14 **Returns:**
15 15
16   -Should return `boolean` value. If `true` - send Message via **True** chain, otherwise **False** chain is used.
  16 +Must return a `boolean` value. If `true` - routes Message to subsequent rule nodes that are related via **True** link,
  17 +otherwise sends Message to rule nodes related via **False** link.
  18 +Uses 'Failure' link in case of any failures to evaluate the expression.
17 19
18 20 <div class="divider"></div>
19 21
20 22 ##### Examples
21 23
22   -* Forward all messages with `temperature` value greater than `20` to the **True** chain and all other messages to the **False** chain:
  24 +* Forward all messages with `temperature` value greater than `20` to the **True** link and all other messages to the **False** link.
  25 + Assumes that incoming messages always contain the 'temperature' field:
23 26
24 27 ```javascript
25 28 return msg.temperature > 20;
26 29 {:copy-code}
27 30 ```
28 31
  32 +
  33 +Example of the rule chain configuration:
  34 +
  35 +![image](${helpBaseUrl}/help/images/rulenode/examples/filter-node.png)
  36 +
  37 +* Same as above, but checks that the message has 'temperature' field to **avoid failures** on unexpected messages:
  38 +
  39 +```javascript
  40 +return typeof msg.temperature !== 'undefined' && msg.temperature > 20;
  41 +{:copy-code}
  42 +```
  43 +
29 44 * Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain:
30 45
31 46 ```javascript
... ...
... ... @@ -5,7 +5,7 @@
5 5
6 6 *function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}*
7 7
8   -JavaScript function generating new message using previous Message payload, Metadata and Message type as input arguments.
  8 +JavaScript function generating new Message using previous Message payload, Metadata and Message type as input arguments.
9 9
10 10 **Parameters:**
11 11
... ... @@ -24,13 +24,13 @@ Should return the object with the following structure:
24 24
25 25 ```javascript
26 26 {
27   - msg?: {[key: string]: any},
28   - metadata?: {[key: string]: string},
29   - msgType?: string
  27 + msg: {[key: string]: any},
  28 + metadata: {[key: string]: string},
  29 + msgType: string
30 30 }
31 31 ```
32 32
33   -All fields in resulting object are optional and will be taken from previously generated Message if not specified.
  33 +All fields in resulting object are mandatory.
34 34
35 35 <div class="divider"></div>
36 36
... ... @@ -39,14 +39,11 @@ All fields in resulting object are optional and will be taken from previously ge
39 39 * Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`:
40 40
41 41 ```javascript
42   -var temperature = 18 + Math.random() * 14;
  42 +var temperature = 18 + Math.random() * (32 - 18);
43 43 // Round to at most 2 decimal places (optional)
44 44 temperature = Math.round( temperature * 100 ) / 100;
45 45 var msg = { temperature: temperature };
46   -var metadata = {};
47   -var msgType = "POST_TELEMETRY_REQUEST";
48   -
49   -return { msg: msg, metadata: metadata, msgType: msgType };
  46 +return { msg: msg, metadata: {}, msgType: "POST_TELEMETRY_REQUEST" };
50 47 {:copy-code}
51 48 ```
52 49
... ... @@ -62,9 +59,7 @@ and <strong>metadata</strong> with field <code>data</code> having value <code>40
62 59 ```javascript
63 60 var msg = { temp: 42, humidity: 77 };
64 61 var metadata = { data: 40 };
65   -var msgType = "POST_TELEMETRY_REQUEST";
66   -
67   -return { msg: msg, metadata: metadata, msgType: msgType };
  62 +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" };
68 63 {:copy-code}
69 64 ```
70 65
... ... @@ -108,9 +103,8 @@ if (isDecrement === 'true') {
108 103
109 104 var msg = { temperature: temperature };
110 105 var metadata = { isDecrement: isDecrement };
111   -var msgType = "POST_TELEMETRY_REQUEST";
112 106
113   -return { msg: msg, metadata: metadata, msgType: msgType };
  107 +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" };
114 108 {:copy-code}
115 109 ```
116 110
... ...
... ... @@ -5,7 +5,7 @@
5 5
6 6 *function Switch(msg, metadata, msgType): string[]*
7 7
8   -JavaScript function computing **an array of next Relation names** for incoming Message.
  8 +JavaScript function computing **an array of Link names** to forward the incoming Message.
9 9
10 10 **Parameters:**
11 11
... ... @@ -13,8 +13,9 @@ JavaScript function computing **an array of next Relation names** for incoming M
13 13
14 14 **Returns:**
15 15
16   -Should return an array of `string` values presenting **next Relation names** where Message should be routed.<br>
17   -If returned array is empty - message will not be routed to any Node and discarded.
  16 +Should return an array of `string` values presenting **link names** that the Rule Engine should use to further route the incoming Message.<br>
  17 +If the result is an empty array - message will not be routed to any Node and will be immediately
  18 +<a href="https://thingsboard.io/docs/user-guide/rule-engine-2-0/overview/#message-processing-result" target="_blank">acknowledged</a>.
18 19
19 20 <div class="divider"></div>
20 21
... ... @@ -24,7 +25,7 @@ If returned array is empty - message will not be routed to any Node and discarde
24 25 <li>
25 26 Forward all messages with <code>temperature</code> value greater than <code>30</code> to the <strong>'High temperature'</strong> chain,<br>
26 27 with <code>temperature</code> value lower than <code>20</code> to the <strong>'Low temperature'</strong> chain and all other messages<br>
27   -to the <strong>'Normal temperature'</strong> chain:
  28 +to the <strong>'Other'</strong> chain:
28 29 </li>
29 30 </ul>
30 31
... ... @@ -34,11 +35,15 @@ if (msg.temperature > 30) {
34 35 } else if (msg.temperature < 20) {
35 36 return ['Low temperature'];
36 37 } else {
37   - return ['Normal temperature'];
  38 + return ['Other'];
38 39 }
39 40 {:copy-code}
40 41 ```
41 42
  43 +Example of the rule chain configuration:
  44 +
  45 +![image](${helpBaseUrl}/help/images/rulenode/examples/switch-node.png)
  46 +
42 47 <ul>
43 48 <li>
44 49 For messages with type <code>POST_TELEMETRY_REQUEST</code>:
... ...
... ... @@ -5,7 +5,7 @@
5 5
6 6 *function Transform(msg, metadata, msgType): {msg: object, metadata: object, msgType: string}*
7 7
8   -JavaScript function transforming input Message payload, Metadata or Message type.
  8 +The JavaScript function to transform input Message payload, Metadata and/or Message type to the output message.
9 9
10 10 **Parameters:**
11 11
... ... @@ -29,11 +29,29 @@ All fields in resulting object are optional and will be taken from original mess
29 29
30 30 ##### Examples
31 31
32   -* Change message type to `CUSTOM_REQUEST`:
  32 +* Add sum of two fields ('a' and 'b') as a new field ('sum') of existing message:
33 33
34 34 ```javascript
35   -return { msgType: 'CUSTOM_REQUEST' };
36   -{:copy-code}
  35 +if(typeof msg.a !== "undefined" && typeof msg.b !== "undefined"){
  36 + msg.sum = msg.a + msg.b;
  37 +}
  38 +return {msg: msg};
  39 +```
  40 +
  41 +* Transform value of the 'temperature' field from °F to °C:
  42 +
  43 +```javascript
  44 +msg.temperature = (msg.temperature - 32) * 5 / 9;
  45 +return {msg: msg};
  46 +```
  47 +
  48 +* Replace the incoming message with the new message that contains only one field - count of properties in the original message:
  49 +
  50 +```javascript
  51 +var newMsg = {
  52 + count: Object.keys(msg).length
  53 +};
  54 +return {msg: newMsg};
37 55 ```
38 56
39 57 <ul>
... ...
... ... @@ -17,67 +17,65 @@ A JavaScript function performing custom action.
17 17
18 18 ##### Examples
19 19
20   -* Display alert dialog with entity information:
21   -
22   -```javascript
23   -{:code-style="max-height: 300px;"}
24   -var title;
25   -var content;
26   -if (entityName) {
27   - title = entityName + ' details';
28   - content = '<b>Entity name</b>: ' + entityName;
29   - if (additionalParams && additionalParams.entity) {
30   - var entity = additionalParams.entity;
31   - if (entity.id) {
32   - content += '<br><b>Entity type</b>: ' + entity.id.entityType;
33   - }
34   - if (!isNaN(entity.temperature) && entity.temperature !== '') {
35   - content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C';
36   - }
37   - }
38   -} else {
39   - title = 'No entity information available';
40   - content = '<b>No entity information available</b>';
41   -}
42   -
43   -showAlertDialog(title, content);
44   -
45   -function showAlertDialog(title, content) {
46   - setTimeout(function() {
47   - widgetContext.dialogs.alert(title, content).subscribe();
48   - }, 100);
49   -}
50   -{:copy-code}
51   -```
52   -
53   -* Delete device after confirmation:
54   -
55   -```javascript
56   -{:code-style="max-height: 300px;"}
57   -var $injector = widgetContext.$scope.$injector;
58   -var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
59   -var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
60   -
61   -openDeleteDeviceDialog();
62   -
63   -function openDeleteDeviceDialog() {
64   - var title = 'Are you sure you want to delete the device ' + entityName + '?';
65   - var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!';
66   - dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(
67   - function(result) {
68   - if (result) {
69   - deleteDevice();
70   - }
71   - }
72   - );
73   -}
74   -
75   -function deleteDevice() {
76   - deviceService.deleteDevice(entityId.id).subscribe(
77   - function() {
78   - widgetContext.updateAliases();
79   - }
80   - );
81   -}
82   -{:copy-code}
83   -```
  20 +<br>
  21 +
  22 +<div style="padding-left: 32px;"
  23 + tb-help-popup="widget/action/examples_custom_action/custom_action_display_alert"
  24 + tb-help-popup-placement="top"
  25 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  26 + trigger-style="font-size: 16px;"
  27 + trigger-text="Display alert dialog with entity information">
  28 +</div>
  29 +
  30 +<br>
  31 +
  32 +<div style="padding-left: 32px;"
  33 + tb-help-popup="widget/action/examples_custom_action/custom_action_delete_device_confirm"
  34 + tb-help-popup-placement="top"
  35 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  36 + trigger-style="font-size: 16px;"
  37 + trigger-text="Delete device after confirmation">
  38 +</div>
  39 +
  40 +<br>
  41 +
  42 +<div style="padding-left: 32px;"
  43 + tb-help-popup="widget/action/examples_custom_action/custom_action_return_previous_state"
  44 + tb-help-popup-placement="top"
  45 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  46 + trigger-style="font-size: 16px;"
  47 + trigger-text="Return to the previous state">
  48 +</div>
  49 +
  50 +<br>
  51 +
  52 +<div style="padding-left: 32px;"
  53 + tb-help-popup="widget/action/examples_custom_action/custom_action_open_state_save_parameters"
  54 + tb-help-popup-placement="top"
  55 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  56 + trigger-style="font-size: 16px;"
  57 + trigger-text="Open state conditionally with saving particular state parameters">
  58 +</div>
  59 +
  60 +<br>
  61 +
  62 +<div style="padding-left: 32px;"
  63 + tb-help-popup="widget/action/examples_custom_action/custom_action_back_first_and_open_state"
  64 + tb-help-popup-placement="top"
  65 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  66 + trigger-style="font-size: 16px;"
  67 + trigger-text="Go back to the first state, after this go to the target state">
  68 +</div>
  69 +
  70 +<br>
  71 +
  72 +<div style="padding-left: 32px;"
  73 + tb-help-popup="widget/action/examples_custom_action/custom_action_copy_access_token"
  74 + tb-help-popup-placement="top"
  75 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  76 + trigger-style="font-size: 16px;"
  77 + trigger-text="Copy device access token to buffer">
  78 +</div>
  79 +
  80 +<br>
  81 +<br>
... ...
... ... @@ -42,7 +42,7 @@ A JavaScript function performing custom action with defined HTML template to ren
42 42 <br>
43 43
44 44 <div style="padding-left: 64px;"
45   - tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js"
  45 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_dialog_js"
46 46 tb-help-popup-placement="top"
47 47 [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
48 48 trigger-style="font-size: 16px;"
... ... @@ -52,7 +52,7 @@ A JavaScript function performing custom action with defined HTML template to ren
52 52 <br>
53 53
54 54 <div style="padding-left: 64px;"
55   - tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html"
  55 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_dialog_html"
56 56 tb-help-popup-placement="top"
57 57 [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
58 58 trigger-style="font-size: 16px;"
... ... @@ -64,7 +64,7 @@ A JavaScript function performing custom action with defined HTML template to ren
64 64 <br>
65 65
66 66 <div style="padding-left: 64px;"
67   - tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js"
  67 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js"
68 68 tb-help-popup-placement="top"
69 69 [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
70 70 trigger-style="font-size: 16px;"
... ... @@ -74,7 +74,73 @@ A JavaScript function performing custom action with defined HTML template to ren
74 74 <br>
75 75
76 76 <div style="padding-left: 64px;"
77   - tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html"
  77 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html"
  78 + tb-help-popup-placement="top"
  79 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  80 + trigger-style="font-size: 16px;"
  81 + trigger-text="HTML code">
  82 +</div>
  83 +
  84 +###### Display dialog to created new user
  85 +
  86 +<br>
  87 +
  88 +<div style="padding-left: 64px;"
  89 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_user_js"
  90 + tb-help-popup-placement="top"
  91 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  92 + trigger-style="font-size: 16px;"
  93 + trigger-text="JavaScript function">
  94 +</div>
  95 +
  96 +<br>
  97 +
  98 +<div style="padding-left: 64px;"
  99 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_create_user_html"
  100 + tb-help-popup-placement="top"
  101 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  102 + trigger-style="font-size: 16px;"
  103 + trigger-text="HTML code">
  104 +</div>
  105 +
  106 +###### Display dialog to add/edit image in entity attribute
  107 +
  108 +<br>
  109 +
  110 +<div style="padding-left: 64px;"
  111 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_image_js"
  112 + tb-help-popup-placement="top"
  113 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  114 + trigger-style="font-size: 16px;"
  115 + trigger-text="JavaScript function">
  116 +</div>
  117 +
  118 +<br>
  119 +
  120 +<div style="padding-left: 64px;"
  121 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_edit_image_html"
  122 + tb-help-popup-placement="top"
  123 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  124 + trigger-style="font-size: 16px;"
  125 + trigger-text="HTML code">
  126 +</div>
  127 +
  128 +###### Display dialog to clone device
  129 +
  130 +<br>
  131 +
  132 +<div style="padding-left: 64px;"
  133 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_clone_device_js"
  134 + tb-help-popup-placement="top"
  135 + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
  136 + trigger-style="font-size: 16px;"
  137 + trigger-text="JavaScript function">
  138 +</div>
  139 +
  140 +<br>
  141 +
  142 +<div style="padding-left: 64px;"
  143 + tb-help-popup="widget/action/examples_custom_pretty/custom_pretty_clone_device_html"
78 144 tb-help-popup-placement="top"
79 145 [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
80 146 trigger-style="font-size: 16px;"
... ...
  1 +#### Function go back to the first state, after this go to the target state
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +var stateIndex = widgetContext.stateController.getStateIndex();
  6 +while (stateIndex > 0) {
  7 + stateIndex -= 1;
  8 + backToPrevState(stateIndex);
  9 +}
  10 +openDashboardState('devices');
  11 +
  12 +function backToPrevState(stateIndex) {
  13 + widgetContext.stateController.navigatePrevState(stateIndex);
  14 +}
  15 +
  16 +function openDashboardState(statedId) {
  17 + var currentState = widgetContext.stateController.getStateId();
  18 + if (currentState !== statedId) {
  19 + var params = {};
  20 + widgetContext.stateController.updateState(statedId, params, false);
  21 + }
  22 +}
  23 +{:copy-code}
  24 +```
  25 +
  26 +<br>
  27 +<br>
... ...
  1 +#### Function copy device access token to buffer
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +var $injector = widgetContext.$scope.$injector;
  6 +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  7 +var $translate = $injector.get(widgetContext.servicesMap.get('translate'));
  8 +var $scope = widgetContext.$scope;
  9 +if (entityId.id && entityId.entityType === 'DEVICE') {
  10 + deviceService.getDeviceCredentials(entityId.id, true).subscribe(
  11 + (deviceCredentials) => {
  12 + var credentialsId = deviceCredentials.credentialsId;
  13 + if (copyToClipboard(credentialsId)) {
  14 + $scope.showSuccessToast($translate.instant('device.accessTokenCopiedMessage'), 750, "top", "left");
  15 + }
  16 + }
  17 + );
  18 +}
  19 +
  20 +function copyToClipboard(text) {
  21 + if (window.clipboardData && window.clipboardData.setData) {
  22 + return window.clipboardData.setData("Text", text);
  23 + }
  24 + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
  25 + var textarea = document.createElement("textarea");
  26 + textarea.textContent = text;
  27 + textarea.style.position = "fixed";
  28 + document.body.appendChild(textarea);
  29 + textarea.select();
  30 + try {
  31 + return document.execCommand("copy");
  32 + }
  33 + catch (ex) {
  34 + console.warn("Copy to clipboard failed.", ex);
  35 + return false;
  36 + }
  37 + document.body.removeChild(textarea);
  38 + }
  39 +}
  40 +{:copy-code}
  41 +```
  42 +
  43 +<br>
  44 +<br>
... ...
  1 +#### Function delete device after confirmation
  2 +
  3 +```javascript
  4 +var $injector = widgetContext.$scope.$injector;
  5 +var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
  6 +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  7 +
  8 +openDeleteDeviceDialog();
  9 +
  10 +function openDeleteDeviceDialog() {
  11 + var title = 'Are you sure you want to delete the device ' + entityName + '?';
  12 + var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!';
  13 + dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(
  14 + function(result) {
  15 + if (result) {
  16 + deleteDevice();
  17 + }
  18 + }
  19 + );
  20 +}
  21 +
  22 +function deleteDevice() {
  23 + deviceService.deleteDevice(entityId.id).subscribe(
  24 + function() {
  25 + widgetContext.updateAliases();
  26 + }
  27 + );
  28 +}
  29 +{:copy-code}
  30 +```
  31 +
  32 +<br>
  33 +<br>
... ...
  1 +#### Function display alert dialog with entity information
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +var title;
  6 +var content;
  7 +if (entityName) {
  8 + title = entityName + ' details';
  9 + content = '<b>Entity name</b>: ' + entityName;
  10 + if (additionalParams && additionalParams.entity) {
  11 + var entity = additionalParams.entity;
  12 + if (entity.id) {
  13 + content += '<br><b>Entity type</b>: ' + entity.id.entityType;
  14 + }
  15 + if (!isNaN(entity.temperature) && entity.temperature !== '') {
  16 + content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C';
  17 + }
  18 + }
  19 +} else {
  20 + title = 'No entity information available';
  21 + content = '<b>No entity information available</b>';
  22 +}
  23 +
  24 +showAlertDialog(title, content);
  25 +
  26 +function showAlertDialog(title, content) {
  27 + setTimeout(function() {
  28 + widgetContext.dialogs.alert(title, content).subscribe();
  29 + }, 100);
  30 +}
  31 +{:copy-code}
  32 +```
  33 +
  34 +<br>
  35 +<br>
... ...
  1 +#### Function open state conditionally with saving particular state parameters
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +var entitySubType;
  6 +var $injector = widgetContext.$scope.$injector;
  7 +$injector.get(widgetContext.servicesMap.get('entityService')).getEntity(entityId.entityType, entityId.id)
  8 + .subscribe(function(data) {
  9 + entitySubType = data.type;
  10 + if (entitySubType == 'energy meter') {
  11 + openDashboardStates('energy_meter_details_view');
  12 + } else if (entitySubType == 'thermometer') {
  13 + openDashboardStates('thermometer_details_view');
  14 + }
  15 + });
  16 +
  17 +function openDashboardStates(statedId) {
  18 + var stateParams = widgetContext.stateController.getStateParams();
  19 + var params = {
  20 + entityId: entityId,
  21 + entityName: entityName
  22 + };
  23 +
  24 + if (stateParams.city) {
  25 + params.city = stateParams.city;
  26 + }
  27 +
  28 + widgetContext.stateController.openState(statedId, params, false);
  29 +}
  30 +{:copy-code}
  31 +```
  32 +
  33 +<br>
  34 +<br>
... ...
  1 +#### Function return to the previous state
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +let stateIndex = widgetContext.stateController.getStateIndex();
  6 +if (stateIndex > 0) {
  7 + stateIndex -= 1;
  8 + backToPrevState(stateIndex);
  9 +}
  10 +
  11 +function backToPrevState(stateIndex) {
  12 + widgetContext.stateController.navigatePrevState(stateIndex);
  13 +}
  14 +{:copy-code}
  15 +```
  16 +
  17 +<br>
  18 +<br>
... ...
  1 +#### HTML template of dialog to clone device
  2 +
  3 +```html
  4 +{:code-style="max-height: 400px;"}
  5 +<form [formGroup]="cloneDeviceFormGroup" (ngSubmit)="save()" style="min-width:320px;">
  6 + <mat-toolbar fxLayout="row" color="primary">
  7 + <h2>Clone device: {{ deviceName }}</h2>
  8 + <span fxFlex></span>
  9 + <button mat-icon-button (click)="cancel()"
  10 + type="button">
  11 + <mat-icon class="material-icons">close
  12 + </mat-icon>
  13 + </button>
  14 + </mat-toolbar>
  15 + <mat-progress-bar color="warn" mode="indeterminate"
  16 + *ngIf="isLoading$ | async">
  17 + </mat-progress-bar>
  18 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  19 + <div mat-dialog-content fxLayout="column">
  20 + <mat-form-field fxFlex class="mat-block">
  21 + <mat-label>Clone device name</mat-label>
  22 + <input matInput formControlName="cloneName" required>
  23 + <mat-error *ngIf="cloneDeviceFormGroup.get('cloneName').hasError('required')">
  24 + Clone device name is required
  25 + </mat-error>
  26 + </mat-form-field>
  27 + </div>
  28 + <div mat-dialog-actions fxLayout="row"
  29 + fxLayoutAlign="end center">
  30 + <button mat-button color="primary" type="button"
  31 + [disabled]="(isLoading$ | async)"
  32 + (click)="cancel()" cdkFocusInitial>
  33 + Cancel
  34 + </button>
  35 + <button mat-button mat-raised-button color="primary"
  36 + type="submit"
  37 + [disabled]="(isLoading$ | async) || cloneDeviceFormGroup.invalid || !cloneDeviceFormGroup.dirty">
  38 + Save
  39 + </button>
  40 + </div>
  41 +</form>
  42 +{:copy-code}
  43 +```
  44 +
  45 +<br>
  46 +<br>
... ...
  1 +#### Function displaying dialog to clone device
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +const $injector = widgetContext.$scope.$injector;
  6 +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  7 +const attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  8 +const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  9 +const rxjs = widgetContext.rxjs;
  10 +
  11 +openCloneDeviceDialog();
  12 +
  13 +function openCloneDeviceDialog() {
  14 + customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe();
  15 +}
  16 +
  17 +function CloneDeviceDialogController(instance) {
  18 + let vm = instance;
  19 + vm.deviceName = entityName;
  20 +
  21 + vm.cloneDeviceFormGroup = vm.fb.group({
  22 + cloneName: ['', [vm.validators.required]]
  23 + });
  24 +
  25 + vm.save = function() {
  26 + deviceService.getDevice(entityId.id).pipe(
  27 + rxjs.mergeMap((origDevice) => {
  28 + let cloneDevice = {
  29 + name: vm.cloneDeviceFormGroup.get('cloneName').value,
  30 + type: origDevice.type
  31 + };
  32 + return deviceService.saveDevice(cloneDevice).pipe(
  33 + rxjs.mergeMap((newDevice) => {
  34 + return attributeService.getEntityAttributes(origDevice.id, 'SERVER_SCOPE').pipe(
  35 + rxjs.mergeMap((origAttributes) => {
  36 + return attributeService.saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes);
  37 + })
  38 + );
  39 + })
  40 + );
  41 + })
  42 + ).subscribe(() => {
  43 + widgetContext.updateAliases();
  44 + vm.dialogRef.close(null);
  45 + });
  46 + };
  47 +
  48 + vm.cancel = function() {
  49 + vm.dialogRef.close(null);
  50 + };
  51 +}
  52 +{:copy-code}
  53 +```
  54 +
  55 +<br>
  56 +<br>
... ...
  1 +#### HTML template of dialog to create a device or an asset
  2 +
  3 +```html
  4 +{:code-style="max-height: 400px;"}
  5 +<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"
  6 + (ngSubmit)="save()" class="add-entity-form">
  7 + <mat-toolbar fxLayout="row" color="primary">
  8 + <h2>Add entity</h2>
  9 + <span fxFlex></span>
  10 + <button mat-icon-button (click)="cancel()" type="button">
  11 + <mat-icon class="material-icons">close</mat-icon>
  12 + </button>
  13 + </mat-toolbar>
  14 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  15 + </mat-progress-bar>
  16 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  17 + <div mat-dialog-content fxLayout="column">
  18 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  19 + <mat-form-field fxFlex class="mat-block">
  20 + <mat-label>Entity Name</mat-label>
  21 + <input matInput formControlName="entityName" required>
  22 + <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">
  23 + Entity name is required.
  24 + </mat-error>
  25 + </mat-form-field>
  26 + <mat-form-field fxFlex class="mat-block">
  27 + <mat-label>Entity Label</mat-label>
  28 + <input matInput formControlName="entityLabel" >
  29 + </mat-form-field>
  30 + </div>
  31 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  32 + <tb-entity-type-select
  33 + class="mat-block"
  34 + formControlName="entityType"
  35 + [showLabel]="true"
  36 + [allowedEntityTypes]="allowedEntityTypes"
  37 + ></tb-entity-type-select>
  38 + <tb-entity-subtype-autocomplete
  39 + fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"
  40 + class="mat-block"
  41 + formControlName="type"
  42 + [required]="true"
  43 + [entityType]="'ASSET'"
  44 + ></tb-entity-subtype-autocomplete>
  45 + <tb-entity-subtype-autocomplete
  46 + fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"
  47 + class="mat-block"
  48 + formControlName="type"
  49 + [required]="true"
  50 + [entityType]="'DEVICE'"
  51 + ></tb-entity-subtype-autocomplete>
  52 + </div>
  53 + <div formGroupName="attributes" fxLayout="column">
  54 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  55 + <mat-form-field fxFlex class="mat-block">
  56 + <mat-label>Latitude</mat-label>
  57 + <input type="number" step="any" matInput formControlName="latitude">
  58 + </mat-form-field>
  59 + <mat-form-field fxFlex class="mat-block">
  60 + <mat-label>Longitude</mat-label>
  61 + <input type="number" step="any" matInput formControlName="longitude">
  62 + </mat-form-field>
  63 + </div>
  64 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  65 + <mat-form-field fxFlex class="mat-block">
  66 + <mat-label>Address</mat-label>
  67 + <input matInput formControlName="address">
  68 + </mat-form-field>
  69 + <mat-form-field fxFlex class="mat-block">
  70 + <mat-label>Owner</mat-label>
  71 + <input matInput formControlName="owner">
  72 + </mat-form-field>
  73 + </div>
  74 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  75 + <mat-form-field fxFlex class="mat-block">
  76 + <mat-label>Integer Value</mat-label>
  77 + <input type="number" step="1" matInput formControlName="number">
  78 + <mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')">
  79 + Invalid integer value.
  80 + </mat-error>
  81 + </mat-form-field>
  82 + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
  83 + <label class="checkbox-label">Boolean Value</label>
  84 + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
  85 +
  86 + </mat-checkbox>
  87 + </div>
  88 + </div>
  89 + </div>
  90 + <div class="relations-list">
  91 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>
  92 + <div class="body" [fxShow]="relations().length">
  93 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">
  94 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  95 + <div fxFlex fxLayout="column">
  96 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  97 + <mat-form-field class="mat-block" style="min-width: 100px;">
  98 + <mat-label>Direction</mat-label>
  99 + <mat-select formControlName="direction" name="direction">
  100 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  101 +
  102 + </mat-option>
  103 + </mat-select>
  104 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  105 + Relation direction is required.
  106 + </mat-error>
  107 + </mat-form-field>
  108 + <tb-relation-type-autocomplete
  109 + fxFlex class="mat-block"
  110 + formControlName="relationType"
  111 + [required]="true">
  112 + </tb-relation-type-autocomplete>
  113 + </div>
  114 + <div fxLayout="row" fxLayout.xs="column">
  115 + <tb-entity-select
  116 + fxFlex class="mat-block"
  117 + [required]="true"
  118 + formControlName="relatedEntity">
  119 + </tb-entity-select>
  120 + </div>
  121 + </div>
  122 + <div fxLayout="column" fxLayoutAlign="center center">
  123 + <button mat-icon-button color="primary"
  124 + aria-label="Remove"
  125 + type="button"
  126 + (click)="removeRelation(i)"
  127 + matTooltip="Remove relation"
  128 + matTooltipPosition="above">
  129 + <mat-icon>close</mat-icon>
  130 + </button>
  131 + </div>
  132 + </div>
  133 + </div>
  134 + </div>
  135 + <div>
  136 + <button mat-raised-button color="primary"
  137 + type="button"
  138 + (click)="addRelation()"
  139 + matTooltip="Add Relation"
  140 + matTooltipPosition="above">
  141 + Add
  142 + </button>
  143 + </div>
  144 + </div>
  145 + </div>
  146 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  147 + <button mat-button color="primary"
  148 + type="button"
  149 + [disabled]="(isLoading$ | async)"
  150 + (click)="cancel()" cdkFocusInitial>
  151 + Cancel
  152 + </button>
  153 + <button mat-button mat-raised-button color="primary"
  154 + type="submit"
  155 + [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">
  156 + Create
  157 + </button>
  158 + </div>
  159 +</form>
  160 +{:copy-code}
  161 +```
  162 +
  163 +<br>
  164 +<br>
... ...
  1 +#### Function displaying dialog to create a device or an asset
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +let $injector = widgetContext.$scope.$injector;
  6 +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  7 +let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
  8 +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  9 +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  10 +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
  11 +
  12 +openAddEntityDialog();
  13 +
  14 +function openAddEntityDialog() {
  15 + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
  16 +}
  17 +
  18 +function AddEntityDialogController(instance) {
  19 + let vm = instance;
  20 +
  21 + vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
  22 + vm.entitySearchDirection = {
  23 + from: "FROM",
  24 + to: "TO"
  25 + }
  26 +
  27 + vm.addEntityFormGroup = vm.fb.group({
  28 + entityName: ['', [vm.validators.required]],
  29 + entityType: ['DEVICE'],
  30 + entityLabel: [null],
  31 + type: ['', [vm.validators.required]],
  32 + attributes: vm.fb.group({
  33 + latitude: [null],
  34 + longitude: [null],
  35 + address: [null],
  36 + owner: [null],
  37 + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
  38 + booleanValue: [null]
  39 + }),
  40 + relations: vm.fb.array([])
  41 + });
  42 +
  43 + vm.cancel = function () {
  44 + vm.dialogRef.close(null);
  45 + };
  46 +
  47 + vm.relations = function () {
  48 + return vm.addEntityFormGroup.get('relations');
  49 + };
  50 +
  51 + vm.addRelation = function () {
  52 + vm.relations().push(vm.fb.group({
  53 + relatedEntity: [null, [vm.validators.required]],
  54 + relationType: [null, [vm.validators.required]],
  55 + direction: [null, [vm.validators.required]]
  56 + }));
  57 + };
  58 +
  59 + vm.removeRelation = function (index) {
  60 + vm.relations().removeAt(index);
  61 + vm.relations().markAsDirty();
  62 + };
  63 +
  64 + vm.save = function () {
  65 + vm.addEntityFormGroup.markAsPristine();
  66 + saveEntityObservable().subscribe(
  67 + function (entity) {
  68 + widgetContext.rxjs.forkJoin([
  69 + saveAttributes(entity.id),
  70 + saveRelations(entity.id)
  71 + ]).subscribe(
  72 + function () {
  73 + widgetContext.updateAliases();
  74 + vm.dialogRef.close(null);
  75 + }
  76 + );
  77 + }
  78 + );
  79 + };
  80 +
  81 + function saveEntityObservable() {
  82 + const formValues = vm.addEntityFormGroup.value;
  83 + let entity = {
  84 + name: formValues.entityName,
  85 + type: formValues.type,
  86 + label: formValues.entityLabel
  87 + };
  88 + if (formValues.entityType == 'ASSET') {
  89 + return assetService.saveAsset(entity);
  90 + } else if (formValues.entityType == 'DEVICE') {
  91 + return deviceService.saveDevice(entity);
  92 + }
  93 + }
  94 +
  95 + function saveAttributes(entityId) {
  96 + let attributes = vm.addEntityFormGroup.get('attributes').value;
  97 + let attributesArray = [];
  98 + for (let key in attributes) {
  99 + if (attributes[key] !== null) {
  100 + attributesArray.push({key: key, value: attributes[key]});
  101 + }
  102 + }
  103 + if (attributesArray.length > 0) {
  104 + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
  105 + }
  106 + return widgetContext.rxjs.of([]);
  107 + }
  108 +
  109 + function saveRelations(entityId) {
  110 + let relations = vm.addEntityFormGroup.get('relations').value;
  111 + let tasks = [];
  112 + for (let i = 0; i < relations.length; i++) {
  113 + let relation = {
  114 + type: relations[i].relationType,
  115 + typeGroup: 'COMMON'
  116 + };
  117 + if (relations[i].direction == 'FROM') {
  118 + relation.to = relations[i].relatedEntity;
  119 + relation.from = entityId;
  120 + } else {
  121 + relation.to = entityId;
  122 + relation.from = relations[i].relatedEntity;
  123 + }
  124 + tasks.push(entityRelationService.saveRelation(relation));
  125 + }
  126 + if (tasks.length > 0) {
  127 + return widgetContext.rxjs.forkJoin(tasks);
  128 + }
  129 + return widgetContext.rxjs.of([]);
  130 + }
  131 +}
  132 +{:copy-code}
  133 +```
  134 +
  135 +<br>
  136 +<br>
... ...
  1 +#### HTML template of dialog to create new user
  2 +
  3 +```html
  4 +{:code-style="max-height: 400px;"}
  5 +<form [formGroup]="addEntityFormGroup" (ngSubmit)="save()" style="min-width:480px;">
  6 + <mat-toolbar fxLayout="row" color="primary">
  7 + <h2>Add new User</h2>
  8 + <span fxFlex></span>
  9 + <button mat-icon-button (click)="cancel()" type="button">
  10 + <mat-icon class="material-icons">close</mat-icon>
  11 + </button>
  12 + </mat-toolbar>
  13 + <mat-progress-bar color="warn" mode="indeterminate"
  14 + *ngIf="isLoading$ | async">
  15 + </mat-progress-bar>
  16 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)">
  17 + </div>
  18 + <div mat-dialog-content fxLayout="column">
  19 + <div fxLayout="row" fxLayoutGap="8px"
  20 + fxLayout.xs="column" fxLayoutGap.xs="0">
  21 + <mat-form-field fxFlex class="mat-block">
  22 + <mat-label>Email</mat-label>
  23 + <input matInput formControlName="email" required
  24 + ng-pattern='/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/'>
  25 + <mat-error *ngIf="addEntityFormGroup.get('email').hasError('required')">
  26 + Email is required
  27 + </mat-error>
  28 + <mat-error *ngIf="addEntityFormGroup.get('email').hasError('pattern')">
  29 + Invalid email format
  30 + </mat-error>
  31 + </mat-form-field>
  32 + </div>
  33 + <div fxLayout="row" fxLayoutGap="8px"
  34 + fxLayout.xs="column" fxLayoutGap.xs="0">
  35 + <mat-form-field fxFlex class="mat-block">
  36 + <mat-label>First Name</mat-label>
  37 + <input matInput
  38 + formControlName="firstName">
  39 + </mat-form-field>
  40 + </div>
  41 + <div fxLayout="row" fxLayoutGap="8px"
  42 + fxLayout.xs="column" fxLayoutGap.xs="0">
  43 + <mat-form-field fxFlex class="mat-block">
  44 + <mat-label>Last Name</mat-label>
  45 + <input matInput
  46 + formControlName="lastName">
  47 + </mat-form-field>
  48 + </div>
  49 + <div fxLayout="row" fxLayoutGap="8px"
  50 + fxLayout.xs="column" fxLayoutGap.xs="0">
  51 + <mat-form-field fxFlex class="mat-block">
  52 + <mat-label>User activation method</mat-label>
  53 + <mat-select matInput formControlName="userActivationMethod">
  54 + <mat-option *ngFor="let activationMethod of activationMethods" [value]="activationMethod.value">
  55 + {{activationMethod.name}}
  56 + </mat-option>
  57 + </mat-select>
  58 + <mat-error *ngIf="addEntityFormGroup.get('userActivationMethod').hasError('required')">Please choose activation method</mat-error>
  59 + <mat-hint>e.g. Send activation email</mat-hint>
  60 + </mat-form-field>
  61 + </div>
  62 + </div>
  63 + <div mat-dialog-actions fxLayout="row"
  64 + fxLayoutAlign="end center">
  65 + <button mat-button color="primary" type="button"
  66 + [disabled]="(isLoading$ | async)"
  67 + (click)="cancel()" cdkFocusInitial>
  68 + Cancel
  69 + </button>
  70 + <button mat-button mat-raised-button color="primary"
  71 + type="submit"
  72 + [disabled]="(isLoading$ | async) || addEntityFormGroup.invalid || !addEntityFormGroup.dirty">
  73 + Create
  74 + </button>
  75 + </div>
  76 +</form>
  77 +{:copy-code}
  78 +```
  79 +
  80 +<br>
  81 +<br>
... ...
  1 +#### Function displaying dialog to create new user
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +const $injector = widgetContext.$scope.$injector;
  6 +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  7 +const userService = $injector.get(widgetContext.servicesMap.get('userService'));
  8 +const $scope = widgetContext.$scope;
  9 +const rxjs = widgetContext.rxjs;
  10 +
  11 +openAddUserDialog();
  12 +
  13 +function openAddUserDialog() {
  14 + customDialog.customDialog(htmlTemplate, AddUserDialogController).subscribe();
  15 +}
  16 +
  17 +function AddUserDialogController(instance) {
  18 + let vm = instance;
  19 +
  20 + vm.currentUser = widgetContext.currentUser;
  21 +
  22 + vm.addEntityFormGroup = vm.fb.group({
  23 + email: ['', [vm.validators.required, vm.validators.pattern(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/)]],
  24 + firstName: [''],
  25 + lastName: ['', ],
  26 + userActivationMethod: ['', [vm.validators.required]]
  27 + });
  28 +
  29 + vm.activationMethods = [
  30 + {
  31 + value: 'displayActivationLink',
  32 + name: 'Display activation link'
  33 + },
  34 + {
  35 + value: 'sendActivationMail',
  36 + name: 'Send activation email'
  37 + }
  38 + ];
  39 +
  40 + vm.cancel = function() {
  41 + vm.dialogRef.close(null);
  42 + };
  43 +
  44 + vm.save = function() {
  45 + let formObj = vm.addEntityFormGroup.getRawValue();
  46 + let attributes = [];
  47 + let sendActivationMail = false;
  48 + let newUser = {
  49 + email: formObj.email,
  50 + firstName: formObj.firstName,
  51 + lastName: formObj.lastName,
  52 + authority: 'TENANT_ADMIN'
  53 + };
  54 +
  55 + if (formObj.userActivationMethod === 'sendActivationMail') {
  56 + sendActivationMail = true;
  57 + }
  58 +
  59 + userService.saveUser(newUser, sendActivationMail).pipe(
  60 + rxjs.mergeMap((user) => {
  61 + let activationObs;
  62 + if (sendActivationMail) {
  63 + activationObs = rxjs.of(null);
  64 + } else {
  65 + activationObs = userService.getActivationLink(user.id.id);
  66 + }
  67 + return activationObs.pipe(
  68 + rxjs.mergeMap((activationLink) => {
  69 + return activationLink ? customDialog.customDialog(activationLinkDialogTemplate, ActivationLinkDialogController, {"activationLink": activationLink}) : rxjs.of(null);
  70 + })
  71 + );
  72 + })
  73 + ).subscribe(() => {
  74 + vm.dialogRef.close(null);
  75 + });
  76 + };
  77 +}
  78 +
  79 +function ActivationLinkDialogController(instance) {
  80 + let vm = instance;
  81 +
  82 + vm.activationLink = vm.data.activationLink;
  83 +
  84 + vm.onActivationLinkCopied = () => {
  85 + $scope.showSuccessToast("User activation link has been copied to clipboard", 1200, "bottom", "left", "activationLinkDialogContent");
  86 + };
  87 +
  88 + vm.close = () => {
  89 + vm.dialogRef.close(null);
  90 + };
  91 +}
  92 +
  93 +let activationLinkDialogTemplate = `<form style="min-width: 400px; position: relative;">
  94 + <mat-toolbar color="primary">
  95 + <h2 translate>user.activation-link</h2>
  96 + <span fxFlex></span>
  97 + <button mat-button mat-icon-button
  98 + (click)="close()"
  99 + type="button">
  100 + <mat-icon class="material-icons">close</mat-icon>
  101 + </button>
  102 + </mat-toolbar>
  103 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  104 + </mat-progress-bar>
  105 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  106 + <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
  107 + <div class="mat-content" fxLayout="column">
  108 + <span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span>
  109 + <div fxLayout="row" fxLayoutAlign="start center">
  110 + <pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre>
  111 + <button mat-icon-button
  112 + color="primary"
  113 + ngxClipboard
  114 + cbContent="{{ activationLink }}"
  115 + (cbOnSuccess)="onActivationLinkCopied()"
  116 + matTooltip="{{ 'user.copy-activation-link' | translate }}"
  117 + matTooltipPosition="above">
  118 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  119 + </button>
  120 + </div>
  121 + </div>
  122 + </div>
  123 + <div mat-dialog-actions fxLayoutAlign="end center">
  124 + <button mat-button color="primary"
  125 + type="button"
  126 + cdkFocusInitial
  127 + [disabled]="(isLoading$ | async)"
  128 + (click)="close()">
  129 + {{ 'action.ok' | translate }}
  130 + </button>
  131 + </div>
  132 +</form>`;
  133 +{:copy-code}
  134 +```
  135 +
  136 +<br>
  137 +<br>
... ...
  1 +#### HTML template of dialog to edit a device or an asset
  2 +
  3 +```html
  4 +{:code-style="max-height: 400px;"}
  5 +<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
  6 + (ngSubmit)="save()" class="edit-entity-form">
  7 + <mat-toolbar fxLayout="row" color="primary">
  8 + <h2>Edit </h2>
  9 + <span fxFlex></span>
  10 + <button mat-icon-button (click)="cancel()" type="button">
  11 + <mat-icon class="material-icons">close</mat-icon>
  12 + </button>
  13 + </mat-toolbar>
  14 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  15 + </mat-progress-bar>
  16 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  17 + <div mat-dialog-content fxLayout="column">
  18 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  19 + <mat-form-field fxFlex class="mat-block">
  20 + <mat-label>Entity Name</mat-label>
  21 + <input matInput formControlName="entityName" required readonly="">
  22 + </mat-form-field>
  23 + <mat-form-field fxFlex class="mat-block">
  24 + <mat-label>Entity Label</mat-label>
  25 + <input matInput formControlName="entityLabel">
  26 + </mat-form-field>
  27 + </div>
  28 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  29 + <mat-form-field fxFlex class="mat-block">
  30 + <mat-label>Entity Type</mat-label>
  31 + <input matInput formControlName="entityType" readonly>
  32 + </mat-form-field>
  33 + <mat-form-field fxFlex class="mat-block">
  34 + <mat-label>Type</mat-label>
  35 + <input matInput formControlName="type" readonly>
  36 + </mat-form-field>
  37 + </div>
  38 + <div formGroupName="attributes" fxLayout="column">
  39 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  40 + <mat-form-field fxFlex class="mat-block">
  41 + <mat-label>Latitude</mat-label>
  42 + <input type="number" step="any" matInput formControlName="latitude">
  43 + </mat-form-field>
  44 + <mat-form-field fxFlex class="mat-block">
  45 + <mat-label>Longitude</mat-label>
  46 + <input type="number" step="any" matInput formControlName="longitude">
  47 + </mat-form-field>
  48 + </div>
  49 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  50 + <mat-form-field fxFlex class="mat-block">
  51 + <mat-label>Address</mat-label>
  52 + <input matInput formControlName="address">
  53 + </mat-form-field>
  54 + <mat-form-field fxFlex class="mat-block">
  55 + <mat-label>Owner</mat-label>
  56 + <input matInput formControlName="owner">
  57 + </mat-form-field>
  58 + </div>
  59 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  60 + <mat-form-field fxFlex class="mat-block">
  61 + <mat-label>Integer Value</mat-label>
  62 + <input type="number" step="1" matInput formControlName="number">
  63 + <mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')">
  64 + Invalid integer value.
  65 + </mat-error>
  66 + </mat-form-field>
  67 + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
  68 + <label class="checkbox-label">Boolean Value</label>
  69 + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
  70 +
  71 + </mat-checkbox>
  72 + </div>
  73 + </div>
  74 + </div>
  75 + <div class="relations-list old-relations">
  76 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>
  77 + <div class="body" [fxShow]="oldRelations().length">
  78 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations"
  79 + *ngFor="let relation of oldRelations().controls; let i = index;">
  80 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  81 + <div fxFlex fxLayout="column">
  82 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  83 + <mat-form-field class="mat-block" style="min-width: 100px;">
  84 + <mat-label>Direction</mat-label>
  85 + <mat-select formControlName="direction" name="direction">
  86 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  87 +
  88 + </mat-option>
  89 + </mat-select>
  90 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  91 + Relation direction is required.
  92 + </mat-error>
  93 + </mat-form-field>
  94 + <tb-relation-type-autocomplete
  95 + fxFlex class="mat-block"
  96 + formControlName="relationType"
  97 + required="true">
  98 + </tb-relation-type-autocomplete>
  99 + </div>
  100 + <div fxLayout="row" fxLayout.xs="column">
  101 + <tb-entity-select
  102 + fxFlex class="mat-block"
  103 + required="true"
  104 + formControlName="relatedEntity">
  105 + </tb-entity-select>
  106 + </div>
  107 + </div>
  108 + <div fxLayout="column" fxLayoutAlign="center center">
  109 + <button mat-icon-button color="primary"
  110 + aria-label="Remove"
  111 + type="button"
  112 + (click)="removeOldRelation(i)"
  113 + matTooltip="Remove relation"
  114 + matTooltipPosition="above">
  115 + <mat-icon>close</mat-icon>
  116 + </button>
  117 + </div>
  118 + </div>
  119 + </div>
  120 + </div>
  121 + </div>
  122 + <div class="relations-list">
  123 + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>
  124 + <div class="body" [fxShow]="relations().length">
  125 + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">
  126 + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">
  127 + <div fxFlex fxLayout="column">
  128 + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">
  129 + <mat-form-field class="mat-block" style="min-width: 100px;">
  130 + <mat-label>Direction</mat-label>
  131 + <mat-select formControlName="direction" name="direction">
  132 + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
  133 +
  134 + </mat-option>
  135 + </mat-select>
  136 + <mat-error *ngIf="relation.get('direction').hasError('required')">
  137 + Relation direction is required.
  138 + </mat-error>
  139 + </mat-form-field>
  140 + <tb-relation-type-autocomplete
  141 + fxFlex class="mat-block"
  142 + formControlName="relationType"
  143 + [required]="true">
  144 + </tb-relation-type-autocomplete>
  145 + </div>
  146 + <div fxLayout="row" fxLayout.xs="column">
  147 + <tb-entity-select
  148 + fxFlex class="mat-block"
  149 + [required]="true"
  150 + formControlName="relatedEntity">
  151 + </tb-entity-select>
  152 + </div>
  153 + </div>
  154 + <div fxLayout="column" fxLayoutAlign="center center">
  155 + <button mat-icon-button color="primary"
  156 + aria-label="Remove"
  157 + type="button"
  158 + (click)="removeRelation(i)"
  159 + matTooltip="Remove relation"
  160 + matTooltipPosition="above">
  161 + <mat-icon>close</mat-icon>
  162 + </button>
  163 + </div>
  164 + </div>
  165 + </div>
  166 + </div>
  167 + <div>
  168 + <button mat-raised-button color="primary"
  169 + type="button"
  170 + (click)="addRelation()"
  171 + matTooltip="Add Relation"
  172 + matTooltipPosition="above">
  173 + Add
  174 + </button>
  175 + </div>
  176 + </div>
  177 + </div>
  178 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  179 + <button mat-button color="primary"
  180 + type="button"
  181 + [disabled]="(isLoading$ | async)"
  182 + (click)="cancel()" cdkFocusInitial>
  183 + Cancel
  184 + </button>
  185 + <button mat-button mat-raised-button color="primary"
  186 + type="submit"
  187 + [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">
  188 + Save
  189 + </button>
  190 + </div>
  191 +</form>
  192 +{:copy-code}
  193 +```
  194 +
  195 +<br>
  196 +<br>
... ...
  1 +#### Function displaying dialog to edit a device or an asset
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +let $injector = widgetContext.$scope.$injector;
  6 +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  7 +let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
  8 +let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
  9 +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
  10 +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  11 +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));
  12 +
  13 +openEditEntityDialog();
  14 +
  15 +function openEditEntityDialog() {
  16 + customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();
  17 +}
  18 +
  19 +function EditEntityDialogController(instance) {
  20 + let vm = instance;
  21 +
  22 + vm.entityName = entityName;
  23 + vm.entityType = entityId.entityType;
  24 + vm.entitySearchDirection = {
  25 + from: "FROM",
  26 + to: "TO"
  27 + };
  28 + vm.attributes = {};
  29 + vm.oldRelationsData = [];
  30 + vm.relationsToDelete = [];
  31 + vm.entity = {};
  32 +
  33 + vm.editEntityFormGroup = vm.fb.group({
  34 + entityName: ['', [vm.validators.required]],
  35 + entityType: [null],
  36 + entityLabel: [null],
  37 + type: ['', [vm.validators.required]],
  38 + attributes: vm.fb.group({
  39 + latitude: [null],
  40 + longitude: [null],
  41 + address: [null],
  42 + owner: [null],
  43 + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]],
  44 + booleanValue: [false]
  45 + }),
  46 + oldRelations: vm.fb.array([]),
  47 + relations: vm.fb.array([])
  48 + });
  49 +
  50 + getEntityInfo();
  51 +
  52 + vm.cancel = function() {
  53 + vm.dialogRef.close(null);
  54 + };
  55 +
  56 + vm.relations = function() {
  57 + return vm.editEntityFormGroup.get('relations');
  58 + };
  59 +
  60 + vm.oldRelations = function() {
  61 + return vm.editEntityFormGroup.get('oldRelations');
  62 + };
  63 +
  64 + vm.addRelation = function() {
  65 + vm.relations().push(vm.fb.group({
  66 + relatedEntity: [null, [vm.validators.required]],
  67 + relationType: [null, [vm.validators.required]],
  68 + direction: [null, [vm.validators.required]]
  69 + }));
  70 + };
  71 +
  72 + function addOldRelation() {
  73 + vm.oldRelations().push(vm.fb.group({
  74 + relatedEntity: [{value: null, disabled: true}, [vm.validators.required]],
  75 + relationType: [{value: null, disabled: true}, [vm.validators.required]],
  76 + direction: [{value: null, disabled: true}, [vm.validators.required]]
  77 + }));
  78 + }
  79 +
  80 + vm.removeRelation = function(index) {
  81 + vm.relations().removeAt(index);
  82 + vm.relations().markAsDirty();
  83 + };
  84 +
  85 + vm.removeOldRelation = function(index) {
  86 + vm.oldRelations().removeAt(index);
  87 + vm.relationsToDelete.push(vm.oldRelationsData[index]);
  88 + vm.oldRelations().markAsDirty();
  89 + };
  90 +
  91 + vm.save = function() {
  92 + vm.editEntityFormGroup.markAsPristine();
  93 + widgetContext.rxjs.forkJoin([
  94 + saveAttributes(entityId),
  95 + saveRelations(entityId),
  96 + saveEntity()
  97 + ]).subscribe(
  98 + function () {
  99 + widgetContext.updateAliases();
  100 + vm.dialogRef.close(null);
  101 + }
  102 + );
  103 + };
  104 +
  105 + function getEntityAttributes(attributes) {
  106 + for (var i = 0; i < attributes.length; i++) {
  107 + vm.attributes[attributes[i].key] = attributes[i].value;
  108 + }
  109 + }
  110 +
  111 + function getEntityRelations(relations) {
  112 + let relationsFrom = relations[0];
  113 + let relationsTo = relations[1];
  114 + for (let i=0; i < relationsFrom.length; i++) {
  115 + let relation = {
  116 + direction: 'FROM',
  117 + relationType: relationsFrom[i].type,
  118 + relatedEntity: relationsFrom[i].to
  119 + };
  120 + vm.oldRelationsData.push(relation);
  121 + addOldRelation();
  122 + }
  123 + for (let i=0; i < relationsTo.length; i++) {
  124 + let relation = {
  125 + direction: 'TO',
  126 + relationType: relationsTo[i].type,
  127 + relatedEntity: relationsTo[i].from
  128 + };
  129 + vm.oldRelationsData.push(relation);
  130 + addOldRelation();
  131 + }
  132 + }
  133 +
  134 + function getEntityInfo() {
  135 + widgetContext.rxjs.forkJoin([
  136 + entityRelationService.findInfoByFrom(entityId),
  137 + entityRelationService.findInfoByTo(entityId),
  138 + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'),
  139 + entityService.getEntity(entityId.entityType, entityId.id)
  140 + ]).subscribe(
  141 + function (data) {
  142 + getEntityRelations(data.slice(0,2));
  143 + getEntityAttributes(data[2]);
  144 + vm.entity = data[3];
  145 + vm.editEntityFormGroup.patchValue({
  146 + entityName: vm.entity.name,
  147 + entityType: vm.entityType,
  148 + entityLabel: vm.entity.label,
  149 + type: vm.entity.type,
  150 + attributes: vm.attributes,
  151 + oldRelations: vm.oldRelationsData
  152 + }, {emitEvent: false});
  153 + }
  154 + );
  155 + }
  156 +
  157 + function saveEntity() {
  158 + const formValues = vm.editEntityFormGroup.value;
  159 + if (vm.entity.label !== formValues.entityLabel){
  160 + vm.entity.label = formValues.entityLabel;
  161 + if (formValues.entityType == 'ASSET') {
  162 + return assetService.saveAsset(vm.entity);
  163 + } else if (formValues.entityType == 'DEVICE') {
  164 + return deviceService.saveDevice(vm.entity);
  165 + }
  166 + }
  167 + return widgetContext.rxjs.of([]);
  168 + }
  169 +
  170 + function saveAttributes(entityId) {
  171 + let attributes = vm.editEntityFormGroup.get('attributes').value;
  172 + let attributesArray = [];
  173 + for (let key in attributes) {
  174 + if (attributes[key] !== vm.attributes[key]) {
  175 + attributesArray.push({key: key, value: attributes[key]});
  176 + }
  177 + }
  178 + if (attributesArray.length > 0) {
  179 + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
  180 + }
  181 + return widgetContext.rxjs.of([]);
  182 + }
  183 +
  184 + function saveRelations(entityId) {
  185 + let relations = vm.editEntityFormGroup.get('relations').value;
  186 + let tasks = [];
  187 + for(let i=0; i < relations.length; i++) {
  188 + let relation = {
  189 + type: relations[i].relationType,
  190 + typeGroup: 'COMMON'
  191 + };
  192 + if (relations[i].direction == 'FROM') {
  193 + relation.to = relations[i].relatedEntity;
  194 + relation.from = entityId;
  195 + } else {
  196 + relation.to = entityId;
  197 + relation.from = relations[i].relatedEntity;
  198 + }
  199 + tasks.push(entityRelationService.saveRelation(relation));
  200 + }
  201 + for (let i=0; i < vm.relationsToDelete.length; i++) {
  202 + let relation = {
  203 + type: vm.relationsToDelete[i].relationType
  204 + };
  205 + if (vm.relationsToDelete[i].direction == 'FROM') {
  206 + relation.to = vm.relationsToDelete[i].relatedEntity;
  207 + relation.from = entityId;
  208 + } else {
  209 + relation.to = entityId;
  210 + relation.from = vm.relationsToDelete[i].relatedEntity;
  211 + }
  212 + tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to));
  213 + }
  214 + if (tasks.length > 0) {
  215 + return widgetContext.rxjs.forkJoin(tasks);
  216 + }
  217 + return widgetContext.rxjs.of([]);
  218 + }
  219 +}
  220 +{:copy-code}
  221 +```
  222 +
  223 +<br>
  224 +<br>
... ...
  1 +#### HTML template of dialog to add/edit image in entity attribute
  2 +
  3 +```html
  4 +{:code-style="max-height: 400px;"}
  5 +<form [formGroup]="editEntity" (ngSubmit)="save()" class="edit-entity-form">
  6 + <mat-toolbar fxLayout="row" color="primary">
  7 + <h2>Edit {{entityName}} image</h2>
  8 + <span fxFlex></span>
  9 + <button mat-icon-button (click)="cancel()" type="button">
  10 + <mat-icon class="material-icons">close</mat-icon>
  11 + </button>
  12 + </mat-toolbar>
  13 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async) || loading">
  14 + </mat-progress-bar>
  15 + <div style="height: 4px;" *ngIf="!(isLoading$ | async) && !loading"></div>
  16 + <div mat-dialog-content fxLayout="column">
  17 + <div formGroupName="attributes" fxLayout="column">
  18 + <tb-image-input
  19 + label="Entity image"
  20 + formControlName="image"
  21 + ></tb-image-input>
  22 + </div>
  23 + </div>
  24 + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
  25 + <button mat-button mat-raised-button color="primary"
  26 + type="submit"
  27 + [disabled]="(isLoading$ | async) || editEntity.invalid || !editEntity.dirty">
  28 + Save
  29 + </button>
  30 + <button mat-button color="primary"
  31 + type="button"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="cancel()" cdkFocusInitial>
  34 + Cancel
  35 + </button>
  36 + </div>
  37 +</form>
  38 +{:copy-code}
  39 +```
  40 +
  41 +<br>
  42 +<br>
... ...
  1 +#### Function displaying dialog to add/edit image in entity attribute
  2 +
  3 +```javascript
  4 +{:code-style="max-height: 400px;"}
  5 +let $injector = widgetContext.$scope.$injector;
  6 +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
  7 +let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
  8 +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
  9 +let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
  10 +
  11 +openAddEntityDialog();
  12 +
  13 +function openAddEntityDialog() {
  14 + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(() => {});
  15 +}
  16 +
  17 +function AddEntityDialogController(instance) {
  18 + let vm = instance;
  19 +
  20 + vm.entityName = entityName;
  21 +
  22 + vm.attributes = {};
  23 +
  24 + vm.editEntity = vm.fb.group({
  25 + attributes: vm.fb.group({
  26 + image: [null]
  27 + })
  28 + });
  29 +
  30 + getEntityInfo();
  31 +
  32 + vm.cancel = function() {
  33 + vm.dialogRef.close(null);
  34 + };
  35 +
  36 + vm.save = function() {
  37 + vm.loading = true;
  38 + saveAttributes(entityId).subscribe(
  39 + () => {
  40 + vm.dialogRef.close(null);
  41 + }, () =>{
  42 + vm.loading = false;
  43 + }
  44 + );
  45 + };
  46 +
  47 + function getEntityAttributes(attributes) {
  48 + for (var i = 0; i < attributes.length; i++) {
  49 + vm.attributes[attributes[i].key] = attributes[i].value;
  50 + }
  51 + }
  52 +
  53 + function getEntityInfo() {
  54 + vm.loading = true;
  55 + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(
  56 + function (data) {
  57 + getEntityAttributes(data);
  58 +
  59 + vm.editEntity.patchValue({
  60 + attributes: vm.attributes
  61 + }, {emitEvent: false});
  62 + vm.loading = false;
  63 + }
  64 + );
  65 + }
  66 +
  67 + function saveAttributes(entityId) {
  68 + let attributes = vm.editEntity.get('attributes').value;
  69 + let attributesArray = [];
  70 + for (let key in attributes) {
  71 + if (attributes[key] !== vm.attributes[key]) {
  72 + attributesArray.push({key: key, value: attributes[key]});
  73 + }
  74 + }
  75 + if (attributesArray.length > 0) {
  76 + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
  77 + }
  78 + return widgetContext.rxjs.of([]);
  79 + }
  80 +}
  81 +{:copy-code}
  82 +```
  83 +
  84 +<br>
  85 +<br>
... ...
... ... @@ -54,6 +54,33 @@ if (prevOrigValue) {
54 54 }
55 55 {:copy-code}
56 56 ```
  57 +* Formatting data to time format
  58 +
  59 +```javascript
  60 +if (value) {
  61 + return moment(value).format("DD/MM/YYYY HH:mm:ss");
  62 +}
  63 +return '';
  64 +{:copy-code}
  65 +```
  66 +
  67 +* Creates line-breaks for 0 values, when used in line chart
  68 +
  69 +```javascript
  70 +if (value === 0) {
  71 + return null;
  72 +} else {
  73 + return value;
  74 +}
  75 +{:copy-code}
  76 +```
  77 +
  78 +* Display data point of the HTML value card under the condition
  79 +
  80 +```javascript
  81 +return value ? '<div class="info"><b>Temperature: </b>'+value+' °C</div>' : '';
  82 +{:copy-code}
  83 +```
57 84
58 85 <br>
59 86 <br>
... ...
... ... @@ -57,5 +57,87 @@ return '<div style="border: 2px solid #0072ff; ' +
57 57 {:copy-code}
58 58 ```
59 59
  60 +* Colored circles instead of boolean value:
  61 +
  62 +```javascript
  63 +var color;
  64 +var active = value;
  65 +if (active == 'true') { // all key values here are strings
  66 + color = '#27AE60';
  67 +} else {
  68 + color = '#EB5757';
  69 +}
  70 +return '<span style="font-size: 18px; color: ' + color + '">&#11044;</span>';
  71 +{:copy-code}
  72 +```
  73 +
  74 +* Decimal value format (1196 => 1,196.0):
  75 +
  76 +```javascript
  77 +var value = value / 1;
  78 +function numberWithCommas(x) {
  79 + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  80 +}
  81 +return value ? numberWithCommas(value.toFixed(1)) : '';
  82 +{:copy-code}
  83 +```
  84 +
  85 +* Show device status and icon this :
  86 +
  87 +```javascript
  88 +{:code-style="max-height: 200px; max-width: 850px;"}
  89 +function getIcon(value) {
  90 + if (value == 'QUEUED') {
  91 + return '<mat-icon class="mat-icon material-icons mat-icon-no-color" data-mat-icon-type="font" style="color: #000;">' +
  92 + '<svg style="width:24px;height:24px" viewBox="0 0 24 24">' +
  93 + '<path fill="currentColor" d="M6,2V8H6V8L10,12L6,16V16H6V22H18V16H18V16L14,12L18,8V8H18V2H6M16,16.5V20H8V16.5L12,12.5L16,16.5M12,11.5L8,7.5V4H16V7.5L12,11.5Z" />' +
  94 + '</svg>' +
  95 + '</mat-icon>';
  96 + }
  97 + if (value == 'UPDATED' ) {
  98 + return '<mat-icon class="mat-icon notranslate material-icons mat-icon-no-color" data-mat-icon-type="font" style="color: #000">' +
  99 + 'update' +
  100 + '</mat-icon>';
  101 + }
  102 + return '';
  103 +}
  104 +function capitalize (s) {
  105 + if (typeof s !== 'string') return '';
  106 + return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
  107 +}
  108 +var status = value;
  109 +return getIcon(status) + '<span style="vertical-align: super;padding-left: 8px;">' + capitalize(status) + '</span>';
  110 +{:copy-code}
  111 +```
  112 +
  113 +* Display device attribute value on progress bar:
  114 +
  115 +```javascript
  116 +{:code-style="max-height: 200px; max-width: 850px;"}
  117 +var progress = value;
  118 +if (value !== '') {
  119 + return `<mat-progress-bar style="height: 8px; padding-right: 30px" ` +
  120 + `role="progressbar" aria-valuemin="0" aria-valuemax="100" ` +
  121 + `tabindex="-1" mode="determinate" value="${progress}" ` +
  122 + `class="mat-progress-bar mat-primary" aria-valuenow="${progress}">` +
  123 + `<div aria-hidden="true">` +
  124 + `<svg width="100%" height="8" focusable="false" ` +
  125 + `class="mat-progress-bar-background mat-progress-bar-element">` +
  126 + `<defs>` +
  127 + `<pattern x="4" y="0" width="8" height="4" patternUnits="userSpaceOnUse" id="mat-progress-bar-0">` +
  128 + `<circle cx="2" cy="2" r="2"></circle>` +
  129 + `</pattern>` +
  130 + `</defs>` +
  131 + `<rect width="100%" height="100%" fill="url("/components/progress-bar/overview#mat-progress-bar-0")"></rect>` +
  132 + `</svg>` +
  133 + `<div class="mat-progress-bar-buffer mat-progress-bar-element"></div>` +
  134 + `<div class="mat-progress-bar-primary mat-progress-bar-fill mat-progress-bar-element" style="transform: scale3d(${progress / 100}, 1, 1);"></div>` +
  135 + `<div class="mat-progress-bar-secondary mat-progress-bar-fill mat-progress-bar-element"></div>` +
  136 + `</div>` +
  137 + `</mat-progress-bar>`;
  138 +}
  139 +{:copy-code}
  140 +```
  141 +
60 142 <br>
61 143 <br>
... ...
... ... @@ -29,6 +29,16 @@ Should return key/value object presenting style attributes.
29 29
30 30 ##### Examples
31 31
  32 +* Set color and font-weight table cell content:
  33 +
  34 +```javascript
  35 +return {
  36 + color:'rgb(0, 132, 214)',
  37 + fontWeight: 600
  38 +}
  39 +{:copy-code}
  40 +```
  41 +
32 42 * Set color depending on device temperature value:
33 43
34 44 ```javascript
... ...
... ... @@ -27,6 +27,16 @@ Should return key/value object presenting style attributes.
27 27
28 28 ##### Examples
29 29
  30 +* Set color and font-weight table row:
  31 +
  32 +```javascript
  33 +return {
  34 + color:'rgb(0, 132, 214)',
  35 + fontWeight: 600
  36 +}
  37 +{:copy-code}
  38 +```
  39 +
30 40 * Set row background color depending on device type:
31 41
32 42 ```javascript
... ...
... ... @@ -43,6 +43,17 @@ return '';
43 43 {:copy-code}
44 44 ```
45 45
  46 +* Present ticks in decimal format (1196 => 1,196.0):
  47 +
  48 +```javascript
  49 +var value = value / 1;
  50 +function numberWithCommas(x) {
  51 + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  52 +}
  53 +return value ? numberWithCommas(value.toFixed(1)) : '';
  54 +{:copy-code}
  55 +```
  56 +
46 57 <ul>
47 58 <li>
48 59 To present axis ticks for true / false or 1 / 0 data.<br>
... ...
... ... @@ -36,5 +36,16 @@ return value.toFixed(2) + ' A';
36 36 {:copy-code}
37 37 ```
38 38
  39 +* Present the datapoint value in decimal format (1196 => 1,196.0):
  40 +
  41 +```javascript
  42 +var value = value / 1;
  43 +function numberWithCommas(x) {
  44 + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  45 +}
  46 +return value ? numberWithCommas(value.toFixed(1)) : '';
  47 +{:copy-code}
  48 +```
  49 +
39 50 <br>
40 51 <br>
... ...