Commit 700293d5f9c51f5062cb28bd9d1d37afbc83cb4c

Authored by Igor Kulikov
1 parent d29a8731

Implement Widget Editor. Dashboard page initial implementation.

Showing 55 changed files with 3915 additions and 128 deletions

Too many changes to show.

To preserve performance only 55 of 74 files are displayed.

@@ -47,6 +47,10 @@ @@ -47,6 +47,10 @@
47 "node_modules/flot/src/plugins/jquery.flot.stack.js", 47 "node_modules/flot/src/plugins/jquery.flot.stack.js",
48 "node_modules/flot.curvedlines/curvedLines.js", 48 "node_modules/flot.curvedlines/curvedLines.js",
49 "node_modules/tinycolor2/dist/tinycolor-min.js", 49 "node_modules/tinycolor2/dist/tinycolor-min.js",
  50 + "node_modules/split.js/dist/split.js",
  51 + "node_modules/js-beautify/js/lib/beautify.js",
  52 + "node_modules/js-beautify/js/lib/beautify-css.js",
  53 + "node_modules/js-beautify/js/lib/beautify-html.js",
50 "node_modules/ace-builds/src-min/ace.js", 54 "node_modules/ace-builds/src-min/ace.js",
51 "node_modules/ace-builds/src-min/ext-language_tools.js", 55 "node_modules/ace-builds/src-min/ext-language_tools.js",
52 "node_modules/ace-builds/src-min/ext-searchbox.js", 56 "node_modules/ace-builds/src-min/ext-searchbox.js",
@@ -1170,12 +1170,23 @@ @@ -1170,12 +1170,23 @@
1170 "@types/sizzle": "*" 1170 "@types/sizzle": "*"
1171 } 1171 }
1172 }, 1172 },
  1173 + "@types/js-beautify": {
  1174 + "version": "1.8.1",
  1175 + "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.8.1.tgz",
  1176 + "integrity": "sha512-B1Br8yE27obcYvFx5ECZswT/947aAFNb9lHqnkUOhtOfvJqaa6Axibo4T+5G6iQlUfjgSd8am9R/9j9UBfRlrw==",
  1177 + "dev": true
  1178 + },
1173 "@types/minimatch": { 1179 "@types/minimatch": {
1174 "version": "3.0.3", 1180 "version": "3.0.3",
1175 "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", 1181 "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
1176 "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", 1182 "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
1177 "dev": true 1183 "dev": true
1178 }, 1184 },
  1185 + "@types/mousetrap": {
  1186 + "version": "1.6.3",
  1187 + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz",
  1188 + "integrity": "sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew=="
  1189 + },
1179 "@types/node": { 1190 "@types/node": {
1180 "version": "10.14.15", 1191 "version": "10.14.15",
1181 "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", 1192 "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz",
@@ -1434,6 +1445,11 @@ @@ -1434,6 +1445,11 @@
1434 "through": ">=2.2.7 <3" 1445 "through": ">=2.2.7 <3"
1435 } 1446 }
1436 }, 1447 },
  1448 + "abbrev": {
  1449 + "version": "1.1.1",
  1450 + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
  1451 + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
  1452 + },
1437 "accepts": { 1453 "accepts": {
1438 "version": "1.3.7", 1454 "version": "1.3.7",
1439 "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 1455 "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -1523,6 +1539,15 @@ @@ -1523,6 +1539,15 @@
1523 "tslib": "^1.9.0" 1539 "tslib": "^1.9.0"
1524 } 1540 }
1525 }, 1541 },
  1542 + "angular2-hotkeys": {
  1543 + "version": "2.1.5",
  1544 + "resolved": "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz",
  1545 + "integrity": "sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA==",
  1546 + "requires": {
  1547 + "@types/mousetrap": "^1.6.0",
  1548 + "mousetrap": "^1.6.0"
  1549 + }
  1550 + },
1526 "ansi-colors": { 1551 "ansi-colors": {
1527 "version": "3.2.4", 1552 "version": "3.2.4",
1528 "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", 1553 "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@@ -1955,8 +1980,7 @@ @@ -1955,8 +1980,7 @@
1955 "balanced-match": { 1980 "balanced-match": {
1956 "version": "1.0.0", 1981 "version": "1.0.0",
1957 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 1982 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
1958 - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",  
1959 - "dev": true 1983 + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
1960 }, 1984 },
1961 "base": { 1985 "base": {
1962 "version": "0.11.2", 1986 "version": "0.11.2",
@@ -2143,7 +2167,6 @@ @@ -2143,7 +2167,6 @@
2143 "version": "1.1.11", 2167 "version": "1.1.11",
2144 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 2168 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
2145 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 2169 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
2146 - "dev": true,  
2147 "requires": { 2170 "requires": {
2148 "balanced-match": "^1.0.0", 2171 "balanced-match": "^1.0.0",
2149 "concat-map": "0.0.1" 2172 "concat-map": "0.0.1"
@@ -2854,8 +2877,7 @@ @@ -2854,8 +2877,7 @@
2854 "commander": { 2877 "commander": {
2855 "version": "2.20.0", 2878 "version": "2.20.0",
2856 "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", 2879 "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
2857 - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",  
2858 - "dev": true 2880 + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
2859 }, 2881 },
2860 "commondir": { 2882 "commondir": {
2861 "version": "1.0.1", 2883 "version": "1.0.1",
@@ -3107,8 +3129,7 @@ @@ -3107,8 +3129,7 @@
3107 "concat-map": { 3129 "concat-map": {
3108 "version": "0.0.1", 3130 "version": "0.0.1",
3109 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 3131 "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
3110 - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",  
3111 - "dev": true 3132 + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
3112 }, 3133 },
3113 "concat-stream": { 3134 "concat-stream": {
3114 "version": "1.6.2", 3135 "version": "1.6.2",
@@ -3122,6 +3143,15 @@ @@ -3122,6 +3143,15 @@
3122 "typedarray": "^0.0.6" 3143 "typedarray": "^0.0.6"
3123 } 3144 }
3124 }, 3145 },
  3146 + "config-chain": {
  3147 + "version": "1.1.12",
  3148 + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
  3149 + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
  3150 + "requires": {
  3151 + "ini": "^1.3.4",
  3152 + "proto-list": "~1.2.1"
  3153 + }
  3154 + },
3125 "connect": { 3155 "connect": {
3126 "version": "3.7.0", 3156 "version": "3.7.0",
3127 "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", 3157 "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
@@ -3762,6 +3792,17 @@ @@ -3762,6 +3792,17 @@
3762 "safer-buffer": "^2.1.0" 3792 "safer-buffer": "^2.1.0"
3763 } 3793 }
3764 }, 3794 },
  3795 + "editorconfig": {
  3796 + "version": "0.15.3",
  3797 + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
  3798 + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
  3799 + "requires": {
  3800 + "commander": "^2.19.0",
  3801 + "lru-cache": "^4.1.5",
  3802 + "semver": "^5.6.0",
  3803 + "sigmund": "^1.0.1"
  3804 + }
  3805 + },
3765 "ee-first": { 3806 "ee-first": {
3766 "version": "1.1.1", 3807 "version": "1.1.1",
3767 "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 3808 "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -4574,8 +4615,7 @@ @@ -4574,8 +4615,7 @@
4574 "fs.realpath": { 4615 "fs.realpath": {
4575 "version": "1.0.0", 4616 "version": "1.0.0",
4576 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 4617 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
4577 - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",  
4578 - "dev": true 4618 + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
4579 }, 4619 },
4580 "fsevents": { 4620 "fsevents": {
4581 "version": "1.2.9", 4621 "version": "1.2.9",
@@ -5183,7 +5223,6 @@ @@ -5183,7 +5223,6 @@
5183 "version": "7.1.3", 5223 "version": "7.1.3",
5184 "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 5224 "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
5185 "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 5225 "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
5186 - "dev": true,  
5187 "requires": { 5226 "requires": {
5188 "fs.realpath": "^1.0.0", 5227 "fs.realpath": "^1.0.0",
5189 "inflight": "^1.0.4", 5228 "inflight": "^1.0.4",
@@ -5709,7 +5748,6 @@ @@ -5709,7 +5748,6 @@
5709 "version": "1.0.6", 5748 "version": "1.0.6",
5710 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 5749 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
5711 "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 5750 "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
5712 - "dev": true,  
5713 "requires": { 5751 "requires": {
5714 "once": "^1.3.0", 5752 "once": "^1.3.0",
5715 "wrappy": "1" 5753 "wrappy": "1"
@@ -5718,14 +5756,12 @@ @@ -5718,14 +5756,12 @@
5718 "inherits": { 5756 "inherits": {
5719 "version": "2.0.4", 5757 "version": "2.0.4",
5720 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 5758 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
5721 - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",  
5722 - "dev": true 5759 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
5723 }, 5760 },
5724 "ini": { 5761 "ini": {
5725 "version": "1.3.5", 5762 "version": "1.3.5",
5726 "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 5763 "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
5727 - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",  
5728 - "dev": true 5764 + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
5729 }, 5765 },
5730 "inquirer": { 5766 "inquirer": {
5731 "version": "6.5.0", 5767 "version": "6.5.0",
@@ -6444,6 +6480,18 @@ @@ -6444,6 +6480,18 @@
6444 } 6480 }
6445 } 6481 }
6446 }, 6482 },
  6483 + "js-beautify": {
  6484 + "version": "1.10.2",
  6485 + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz",
  6486 + "integrity": "sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==",
  6487 + "requires": {
  6488 + "config-chain": "^1.1.12",
  6489 + "editorconfig": "^0.15.3",
  6490 + "glob": "^7.1.3",
  6491 + "mkdirp": "~0.5.1",
  6492 + "nopt": "~4.0.1"
  6493 + }
  6494 + },
6447 "js-tokens": { 6495 "js-tokens": {
6448 "version": "3.0.2", 6496 "version": "3.0.2",
6449 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 6497 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -6918,7 +6966,6 @@ @@ -6918,7 +6966,6 @@
6918 "version": "4.1.5", 6966 "version": "4.1.5",
6919 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 6967 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
6920 "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 6968 "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
6921 - "dev": true,  
6922 "requires": { 6969 "requires": {
6923 "pseudomap": "^1.0.2", 6970 "pseudomap": "^1.0.2",
6924 "yallist": "^2.1.2" 6971 "yallist": "^2.1.2"
@@ -7258,7 +7305,6 @@ @@ -7258,7 +7305,6 @@
7258 "version": "3.0.4", 7305 "version": "3.0.4",
7259 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 7306 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
7260 "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 7307 "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
7261 - "dev": true,  
7262 "requires": { 7308 "requires": {
7263 "brace-expansion": "^1.1.7" 7309 "brace-expansion": "^1.1.7"
7264 } 7310 }
@@ -7368,7 +7414,6 @@ @@ -7368,7 +7414,6 @@
7368 "version": "0.5.1", 7414 "version": "0.5.1",
7369 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 7415 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
7370 "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 7416 "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
7371 - "dev": true,  
7372 "requires": { 7417 "requires": {
7373 "minimist": "0.0.8" 7418 "minimist": "0.0.8"
7374 }, 7419 },
@@ -7376,8 +7421,7 @@ @@ -7376,8 +7421,7 @@
7376 "minimist": { 7421 "minimist": {
7377 "version": "0.0.8", 7422 "version": "0.0.8",
7378 "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 7423 "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
7379 - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",  
7380 - "dev": true 7424 + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
7381 } 7425 }
7382 } 7426 }
7383 }, 7427 },
@@ -7386,6 +7430,11 @@ @@ -7386,6 +7430,11 @@
7386 "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", 7430 "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
7387 "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" 7431 "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
7388 }, 7432 },
  7433 + "mousetrap": {
  7434 + "version": "1.6.3",
  7435 + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz",
  7436 + "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA=="
  7437 + },
7389 "move-concurrently": { 7438 "move-concurrently": {
7390 "version": "1.0.1", 7439 "version": "1.0.1",
7391 "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", 7440 "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -7563,6 +7612,15 @@ @@ -7563,6 +7612,15 @@
7563 "semver": "^5.3.0" 7612 "semver": "^5.3.0"
7564 } 7613 }
7565 }, 7614 },
  7615 + "nopt": {
  7616 + "version": "4.0.1",
  7617 + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
  7618 + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
  7619 + "requires": {
  7620 + "abbrev": "1",
  7621 + "osenv": "^0.1.4"
  7622 + }
  7623 + },
7566 "normalize-package-data": { 7624 "normalize-package-data": {
7567 "version": "2.5.0", 7625 "version": "2.5.0",
7568 "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 7626 "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -7801,7 +7859,6 @@ @@ -7801,7 +7859,6 @@
7801 "version": "1.4.0", 7859 "version": "1.4.0",
7802 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 7860 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
7803 "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 7861 "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
7804 - "dev": true,  
7805 "requires": { 7862 "requires": {
7806 "wrappy": "1" 7863 "wrappy": "1"
7807 } 7864 }
@@ -7877,8 +7934,7 @@ @@ -7877,8 +7934,7 @@
7877 "os-homedir": { 7934 "os-homedir": {
7878 "version": "1.0.2", 7935 "version": "1.0.2",
7879 "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 7936 "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
7880 - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",  
7881 - "dev": true 7937 + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
7882 }, 7938 },
7883 "os-locale": { 7939 "os-locale": {
7884 "version": "3.1.0", 7940 "version": "3.1.0",
@@ -7894,14 +7950,12 @@ @@ -7894,14 +7950,12 @@
7894 "os-tmpdir": { 7950 "os-tmpdir": {
7895 "version": "1.0.2", 7951 "version": "1.0.2",
7896 "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 7952 "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
7897 - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",  
7898 - "dev": true 7953 + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
7899 }, 7954 },
7900 "osenv": { 7955 "osenv": {
7901 "version": "0.1.5", 7956 "version": "0.1.5",
7902 "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 7957 "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
7903 "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 7958 "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
7904 - "dev": true,  
7905 "requires": { 7959 "requires": {
7906 "os-homedir": "^1.0.0", 7960 "os-homedir": "^1.0.0",
7907 "os-tmpdir": "^1.0.0" 7961 "os-tmpdir": "^1.0.0"
@@ -8222,8 +8276,7 @@ @@ -8222,8 +8276,7 @@
8222 "path-is-absolute": { 8276 "path-is-absolute": {
8223 "version": "1.0.1", 8277 "version": "1.0.1",
8224 "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 8278 "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
8225 - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",  
8226 - "dev": true 8279 + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
8227 }, 8280 },
8228 "path-is-inside": { 8281 "path-is-inside": {
8229 "version": "1.0.2", 8282 "version": "1.0.2",
@@ -8465,6 +8518,11 @@ @@ -8465,6 +8518,11 @@
8465 "retry": "^0.10.0" 8518 "retry": "^0.10.0"
8466 } 8519 }
8467 }, 8520 },
  8521 + "proto-list": {
  8522 + "version": "1.2.4",
  8523 + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
  8524 + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk="
  8525 + },
8468 "protoduck": { 8526 "protoduck": {
8469 "version": "5.0.1", 8527 "version": "5.0.1",
8470 "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", 8528 "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz",
@@ -8612,8 +8670,7 @@ @@ -8612,8 +8670,7 @@
8612 "pseudomap": { 8670 "pseudomap": {
8613 "version": "1.0.2", 8671 "version": "1.0.2",
8614 "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 8672 "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
8615 - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",  
8616 - "dev": true 8673 + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
8617 }, 8674 },
8618 "psl": { 8675 "psl": {
8619 "version": "1.3.0", 8676 "version": "1.3.0",
@@ -9208,8 +9265,7 @@ @@ -9208,8 +9265,7 @@
9208 "semver": { 9265 "semver": {
9209 "version": "5.6.0", 9266 "version": "5.6.0",
9210 "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 9267 "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
9211 - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",  
9212 - "dev": true 9268 + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
9213 }, 9269 },
9214 "semver-dsl": { 9270 "semver-dsl": {
9215 "version": "1.0.1", 9271 "version": "1.0.1",
@@ -9408,6 +9464,11 @@ @@ -9408,6 +9464,11 @@
9408 "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 9464 "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
9409 "dev": true 9465 "dev": true
9410 }, 9466 },
  9467 + "sigmund": {
  9468 + "version": "1.0.1",
  9469 + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
  9470 + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
  9471 + },
9411 "signal-exit": { 9472 "signal-exit": {
9412 "version": "3.0.2", 9473 "version": "3.0.2",
9413 "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 9474 "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@@ -9920,6 +9981,11 @@ @@ -9920,6 +9981,11 @@
9920 "extend-shallow": "^3.0.0" 9981 "extend-shallow": "^3.0.0"
9921 } 9982 }
9922 }, 9983 },
  9984 + "split.js": {
  9985 + "version": "1.5.11",
  9986 + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.5.11.tgz",
  9987 + "integrity": "sha512-ec0sAbWnaMGpNHWo1ZgIlF3Mx7GzSyaO0GlcEBZGIFZQwYPPkbDV6JRpDmpzIshVig7USREuEPudy0ygQaskXg=="
  9988 + },
9923 "sprintf-js": { 9989 "sprintf-js": {
9924 "version": "1.0.3", 9990 "version": "1.0.3",
9925 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 9991 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -11121,8 +11187,7 @@ @@ -11121,8 +11187,7 @@
11121 "wrappy": { 11187 "wrappy": {
11122 "version": "1.0.2", 11188 "version": "1.0.2",
11123 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 11189 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
11124 - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",  
11125 - "dev": true 11190 + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
11126 }, 11191 },
11127 "ws": { 11192 "ws": {
11128 "version": "3.3.3", 11193 "version": "3.3.3",
@@ -11180,8 +11245,7 @@ @@ -11180,8 +11245,7 @@
11180 "yallist": { 11245 "yallist": {
11181 "version": "2.1.2", 11246 "version": "2.1.2",
11182 "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 11247 "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
11183 - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",  
11184 - "dev": true 11248 + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
11185 }, 11249 },
11186 "yargs": { 11250 "yargs": {
11187 "version": "12.0.5", 11251 "version": "12.0.5",
@@ -33,6 +33,7 @@ @@ -33,6 +33,7 @@
33 "@ngx-translate/http-loader": "^4.0.0", 33 "@ngx-translate/http-loader": "^4.0.0",
34 "ace-builds": "^1.4.5", 34 "ace-builds": "^1.4.5",
35 "angular-gridster2": "^8.1.0", 35 "angular-gridster2": "^8.1.0",
  36 + "angular2-hotkeys": "^2.1.5",
36 "base64-js": "^1.3.1", 37 "base64-js": "^1.3.1",
37 "compass-sass-mixins": "^0.12.7", 38 "compass-sass-mixins": "^0.12.7",
38 "core-js": "^3.1.4", 39 "core-js": "^3.1.4",
@@ -44,6 +45,7 @@ @@ -44,6 +45,7 @@
44 "javascript-detect-element-resize": "^0.5.3", 45 "javascript-detect-element-resize": "^0.5.3",
45 "jquery": "^3.4.1", 46 "jquery": "^3.4.1",
46 "jquery.terminal": "^2.8.0", 47 "jquery.terminal": "^2.8.0",
  48 + "js-beautify": "^1.10.2",
47 "material-design-icons": "^3.0.1", 49 "material-design-icons": "^3.0.1",
48 "messageformat": "^2.3.0", 50 "messageformat": "^2.3.0",
49 "moment": "^2.24.0", 51 "moment": "^2.24.0",
@@ -51,6 +53,7 @@ @@ -51,6 +53,7 @@
51 "ngx-translate-messageformat-compiler": "^4.5.0", 53 "ngx-translate-messageformat-compiler": "^4.5.0",
52 "rxjs": "~6.5.2", 54 "rxjs": "~6.5.2",
53 "screenfull": "^4.2.1", 55 "screenfull": "^4.2.1",
  56 + "split.js": "^1.5.11",
54 "tinycolor2": "^1.4.1", 57 "tinycolor2": "^1.4.1",
55 "tslib": "^1.10.0", 58 "tslib": "^1.10.0",
56 "typeface-roboto": "^0.0.75", 59 "typeface-roboto": "^0.0.75",
@@ -66,6 +69,7 @@ @@ -66,6 +69,7 @@
66 "@types/jasmine": "~3.4.0", 69 "@types/jasmine": "~3.4.0",
67 "@types/jasminewd2": "~2.0.6", 70 "@types/jasminewd2": "~2.0.6",
68 "@types/jquery": "^3.3.31", 71 "@types/jquery": "^3.3.31",
  72 + "@types/js-beautify": "^1.8.1",
69 "@types/node": "~10.14.15", 73 "@types/node": "~10.14.15",
70 "@types/tinycolor2": "^1.4.2", 74 "@types/tinycolor2": "^1.4.2",
71 "codelyzer": "~5.1.0", 75 "codelyzer": "~5.1.0",
@@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module'; @@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module';
24 import { HomeModule } from './modules/home/home.module'; 24 import { HomeModule } from './modules/home/home.module';
25 25
26 import { AppComponent } from './app.component'; 26 import { AppComponent } from './app.component';
  27 +import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module';
27 28
28 @NgModule({ 29 @NgModule({
29 declarations: [ 30 declarations: [
@@ -35,7 +36,8 @@ import { AppComponent } from './app.component'; @@ -35,7 +36,8 @@ import { AppComponent } from './app.component';
35 AppRoutingModule, 36 AppRoutingModule,
36 CoreModule, 37 CoreModule,
37 LoginModule, 38 LoginModule,
38 - HomeModule 39 + HomeModule,
  40 + DashboardRoutingModule
39 ], 41 ],
40 providers: [], 42 providers: [],
41 bootstrap: [AppComponent] 43 bootstrap: [AppComponent]
@@ -14,19 +14,26 @@ @@ -14,19 +14,26 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { IAliasController, AliasInfo } from '@core/api/widget-api.models'; 17 +import { IAliasController, AliasInfo, IStateController } from '@core/api/widget-api.models';
18 import { Observable, of, Subject } from 'rxjs'; 18 import { Observable, of, Subject } from 'rxjs';
19 import { Datasource } from '@app/shared/models/widget.models'; 19 import { Datasource } from '@app/shared/models/widget.models';
20 import { deepClone } from '@core/utils'; 20 import { deepClone } from '@core/utils';
  21 +import { EntityService } from '@core/http/entity.service';
  22 +import { UtilsService } from '@core/services/utils.service';
  23 +import { EntityAliases } from '@shared/models/alias.models';
  24 +import { EntityInfo } from '@shared/models/entity.models';
  25 +import * as equal from 'deep-equal';
21 26
22 export class DummyAliasController implements IAliasController { 27 export class DummyAliasController implements IAliasController {
23 28
24 entityAliasesChanged: Observable<Array<string>>; 29 entityAliasesChanged: Observable<Array<string>>;
  30 + entityAliasResolved: Observable<string>;
25 31
26 [key: string]: any | null; 32 [key: string]: any | null;
27 33
28 constructor() { 34 constructor() {
29 this.entityAliasesChanged = new Subject<Array<string>>().asObservable(); 35 this.entityAliasesChanged = new Subject<Array<string>>().asObservable();
  36 + this.entityAliasResolved = new Subject<string>().asObservable();
30 } 37 }
31 38
32 getAliasInfo(aliasId): Observable<AliasInfo> { 39 getAliasInfo(aliasId): Observable<AliasInfo> {
@@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController { @@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController {
36 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { 43 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
37 return of(deepClone(datasources)); 44 return of(deepClone(datasources));
38 } 45 }
  46 +
  47 + getEntityAliases(): EntityAliases {
  48 + return undefined;
  49 + }
  50 +
  51 + getInstantAliasInfo(aliasId: string): AliasInfo {
  52 + return undefined;
  53 + }
  54 +
  55 + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) {
  56 + }
  57 +
  58 + updateEntityAliases(entityAliases: EntityAliases) {
  59 + }
  60 +
  61 +}
  62 +
  63 +export class AliasController implements IAliasController {
  64 +
  65 + private entityAliasesChangedSubject = new Subject<Array<string>>();
  66 + entityAliasesChanged: Observable<Array<string>> = this.entityAliasesChangedSubject.asObservable();
  67 +
  68 + private entityAliasResolvedSubject = new Subject<string>();
  69 + entityAliasResolved: Observable<string> = this.entityAliasResolvedSubject.asObservable();
  70 +
  71 + entityAliases: EntityAliases;
  72 +
  73 + resolvedAliases: {[aliasId: string]: AliasInfo} = {};
  74 +
  75 + [key: string]: any | null;
  76 +
  77 + constructor(private utils: UtilsService,
  78 + private entityService: EntityService,
  79 + private stateController: IStateController,
  80 + private origEntityAliases: EntityAliases) {
  81 + this.entityAliases = deepClone(this.origEntityAliases);
  82 + }
  83 +
  84 + getAliasInfo(aliasId: string): Observable<AliasInfo> {
  85 + return of(null);
  86 + }
  87 +
  88 + resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
  89 + return of(deepClone(datasources));
  90 + }
  91 +
  92 + getEntityAliases(): EntityAliases {
  93 + return this.entityAliases;
  94 + }
  95 +
  96 + getInstantAliasInfo(aliasId: string): AliasInfo {
  97 + return this.resolvedAliases[aliasId];
  98 + }
  99 +
  100 + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) {
  101 + const aliasInfo = this.resolvedAliases[aliasId];
  102 + if (aliasInfo) {
  103 + const prevCurrentEntity = aliasInfo.currentEntity;
  104 + if (!equal(currentEntity, prevCurrentEntity)) {
  105 + aliasInfo.currentEntity = currentEntity;
  106 + this.entityAliasesChangedSubject.next([aliasId]);
  107 + }
  108 + }
  109 + }
  110 +
  111 + updateEntityAliases(entityAliases: EntityAliases) {
  112 + }
  113 +
39 } 114 }
@@ -34,6 +34,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; @@ -34,6 +34,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models';
34 import { HttpErrorResponse } from '@angular/common/http'; 34 import { HttpErrorResponse } from '@angular/common/http';
35 import { DatasourceService } from '@core/api/datasource.service'; 35 import { DatasourceService } from '@core/api/datasource.service';
36 import { RafService } from '@core/services/raf.service'; 36 import { RafService } from '@core/services/raf.service';
  37 +import { EntityAliases } from '@shared/models/alias.models';
  38 +import { EntityInfo } from '@app/shared/models/entity.models';
37 39
38 export interface TimewindowFunctions { 40 export interface TimewindowFunctions {
39 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; 41 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@@ -66,20 +68,24 @@ export interface WidgetActionsApi { @@ -66,20 +68,24 @@ export interface WidgetActionsApi {
66 } 68 }
67 69
68 export interface AliasInfo { 70 export interface AliasInfo {
  71 + alias?: string;
69 stateEntity?: boolean; 72 stateEntity?: boolean;
70 - currentEntity?: {  
71 - id: string;  
72 - entityType: EntityType;  
73 - name?: string;  
74 - }; 73 + currentEntity?: EntityInfo;
  74 + selectedId?: string;
  75 + resolvedEntities?: Array<EntityInfo>;
75 [key: string]: any | null; 76 [key: string]: any | null;
76 // TODO: 77 // TODO:
77 } 78 }
78 79
79 export interface IAliasController { 80 export interface IAliasController {
80 entityAliasesChanged: Observable<Array<string>>; 81 entityAliasesChanged: Observable<Array<string>>;
81 - getAliasInfo(aliasId): Observable<AliasInfo>; 82 + entityAliasResolved: Observable<string>;
  83 + getAliasInfo(aliasId: string): Observable<AliasInfo>;
  84 + getInstantAliasInfo(aliasId: string): AliasInfo;
82 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; 85 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>;
  86 + getEntityAliases(): EntityAliases;
  87 + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
  88 + updateEntityAliases(entityAliases: EntityAliases);
83 [key: string]: any | null; 89 [key: string]: any | null;
84 // TODO: 90 // TODO:
85 } 91 }
@@ -97,17 +103,14 @@ export interface StateParams { @@ -97,17 +103,14 @@ export interface StateParams {
97 } 103 }
98 104
99 export interface IStateController { 105 export interface IStateController {
100 - getStateParams: () => StateParams;  
101 - openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void;  
102 - updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; 106 + getStateParams?: () => StateParams;
  107 + openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void;
  108 + updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void;
  109 + openRightLayout: () => void;
  110 + preserveState?: () => void;
103 // TODO: 111 // TODO:
104 } 112 }
105 113
106 -export interface EntityInfo {  
107 - entityId: EntityId;  
108 - entityName: string;  
109 -}  
110 -  
111 export interface SubscriptionInfo { 114 export interface SubscriptionInfo {
112 type: DatasourceType; 115 type: DatasourceType;
113 name?: string; 116 name?: string;
@@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions { @@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions {
171 // TODO: 174 // TODO:
172 } 175 }
173 176
  177 +export interface SubscriptionEntityInfo {
  178 + entityId: EntityId;
  179 + entityName: string;
  180 +}
  181 +
174 export interface IWidgetSubscription { 182 export interface IWidgetSubscription {
175 183
176 id: string; 184 id: string;
@@ -201,7 +209,7 @@ export interface IWidgetSubscription { @@ -201,7 +209,7 @@ export interface IWidgetSubscription {
201 rpcErrorText?: string; 209 rpcErrorText?: string;
202 rpcRejection?: HttpErrorResponse; 210 rpcRejection?: HttpErrorResponse;
203 211
204 - getFirstEntityInfo(): EntityInfo; 212 + getFirstEntityInfo(): SubscriptionEntityInfo;
205 213
206 onAliasesChanged(aliasIds: Array<string>): boolean; 214 onAliasesChanged(aliasIds: Array<string>): boolean;
207 215
@@ -15,8 +15,7 @@ @@ -15,8 +15,7 @@
15 /// 15 ///
16 16
17 import { 17 import {
18 - EntityInfo,  
19 - IWidgetSubscription, 18 + IWidgetSubscription, SubscriptionEntityInfo,
20 WidgetSubscriptionCallbacks, 19 WidgetSubscriptionCallbacks,
21 WidgetSubscriptionContext, 20 WidgetSubscriptionContext,
22 WidgetSubscriptionOptions 21 WidgetSubscriptionOptions
@@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription {
339 this.onDataUpdated(); 338 this.onDataUpdated();
340 } 339 }
341 340
342 - getFirstEntityInfo(): EntityInfo { 341 + getFirstEntityInfo(): SubscriptionEntityInfo {
343 return undefined; 342 return undefined;
344 } 343 }
345 344
@@ -42,6 +42,8 @@ import {TimeService} from '@core/services/time.service'; @@ -42,6 +42,8 @@ import {TimeService} from '@core/services/time.service';
42 }) 42 })
43 export class AuthService { 43 export class AuthService {
44 44
  45 + forceFullscreen = false; // TODO:
  46 +
45 constructor( 47 constructor(
46 private store: Store<AppState>, 48 private store: Store<AppState>,
47 private http: HttpClient, 49 private http: HttpClient,
@@ -39,6 +39,7 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp @@ -39,6 +39,7 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
39 import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; 39 import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
40 import { WINDOW_PROVIDERS } from '@core/services/window.service'; 40 import { WINDOW_PROVIDERS } from '@core/services/window.service';
41 import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component"; 41 import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
  42 +import { HotkeyModule } from 'angular2-hotkeys';
42 43
43 export function HttpLoaderFactory(http: HttpClient) { 44 export function HttpLoaderFactory(http: HttpClient) {
44 return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); 45 return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
@@ -79,6 +80,7 @@ export function HttpLoaderFactory(http: HttpClient) { @@ -79,6 +80,7 @@ export function HttpLoaderFactory(http: HttpClient) {
79 useClass: TranslateDefaultCompiler 80 useClass: TranslateDefaultCompiler
80 } 81 }
81 }), 82 }),
  83 + HotkeyModule.forRoot(),
82 84
83 // ngrx 85 // ngrx
84 StoreModule.forRoot(reducers, { metaReducers }), 86 StoreModule.forRoot(reducers, { metaReducers }),
@@ -28,21 +28,26 @@ import { selectAuth } from '@core/auth/auth.selectors'; @@ -28,21 +28,26 @@ import { selectAuth } from '@core/auth/auth.selectors';
28 import { take } from 'rxjs/operators'; 28 import { take } from 'rxjs/operators';
29 import { DialogService } from '@core/services/dialog.service'; 29 import { DialogService } from '@core/services/dialog.service';
30 import { TranslateService } from '@ngx-translate/core'; 30 import { TranslateService } from '@ngx-translate/core';
  31 +import { isDefined } from '../utils';
31 32
32 export interface HasConfirmForm { 33 export interface HasConfirmForm {
33 confirmForm(): FormGroup; 34 confirmForm(): FormGroup;
34 } 35 }
35 36
  37 +export interface HasDirtyFlag {
  38 + isDirty: boolean;
  39 +}
  40 +
36 @Injectable({ 41 @Injectable({
37 providedIn: 'root' 42 providedIn: 'root'
38 }) 43 })
39 -export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> { 44 +export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm & HasDirtyFlag> {
40 45
41 constructor(private store: Store<AppState>, 46 constructor(private store: Store<AppState>,
42 private dialogService: DialogService, 47 private dialogService: DialogService,
43 private translate: TranslateService) { } 48 private translate: TranslateService) { }
44 49
45 - canDeactivate(component: HasConfirmForm, 50 + canDeactivate(component: HasConfirmForm & HasDirtyFlag,
46 route: ActivatedRouteSnapshot, 51 route: ActivatedRouteSnapshot,
47 state: RouterStateSnapshot) { 52 state: RouterStateSnapshot) {
48 53
@@ -54,9 +59,17 @@ export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> { @@ -54,9 +59,17 @@ export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> {
54 } 59 }
55 ); 60 );
56 61
57 - if (component.confirmForm && auth && auth.isAuthenticated) {  
58 - const confirmForm = component.confirmForm();  
59 - if (confirmForm && confirmForm.dirty) { 62 + if (auth && auth.isAuthenticated) {
  63 + let isDirty = false;
  64 + if (component.confirmForm) {
  65 + const confirmForm = component.confirmForm();
  66 + if (confirmForm) {
  67 + isDirty = confirmForm.dirty;
  68 + }
  69 + } else if (isDefined(component.isDirty)) {
  70 + isDirty = component.isDirty;
  71 + }
  72 + if (isDirty) {
60 return this.dialogService.confirm( 73 return this.dialogService.confirm(
61 this.translate.instant('confirm-on-exit.title'), 74 this.translate.instant('confirm-on-exit.title'),
62 this.translate.instant('confirm-on-exit.html-message') 75 this.translate.instant('confirm-on-exit.html-message')
@@ -21,10 +21,12 @@ import { HttpClient } from '@angular/common/http'; @@ -21,10 +21,12 @@ import { HttpClient } from '@angular/common/http';
21 import { PageLink } from '@shared/models/page/page-link'; 21 import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 -import { WidgetType } from '@shared/models/widget.models'; 24 +import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared/models/widget.models';
25 import { UtilsService } from '@core/services/utils.service'; 25 import { UtilsService } from '@core/services/utils.service';
26 import { TranslateService } from '@ngx-translate/core'; 26 import { TranslateService } from '@ngx-translate/core';
27 import { ResourcesService } from '../services/resources.service'; 27 import { ResourcesService } from '../services/resources.service';
  28 +import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models';
  29 +import { map } from 'rxjs/operators';
28 30
29 @Injectable({ 31 @Injectable({
30 providedIn: 'root' 32 providedIn: 'root'
@@ -70,4 +72,23 @@ export class WidgetService { @@ -70,4 +72,23 @@ export class WidgetService {
70 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, 72 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
71 defaultHttpOptions(ignoreLoading, ignoreErrors)); 73 defaultHttpOptions(ignoreLoading, ignoreErrors));
72 } 74 }
  75 +
  76 + public getWidgetTypeById(widgetTypeId: string,
  77 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
  78 + return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
  79 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  80 + }
  81 +
  82 + public getWidgetTemplate(widgetTypeParam: widgetType,
  83 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetInfo> {
  84 + const templateWidgetType = widgetTypesData.get(widgetTypeParam);
  85 + return this.getWidgetType(templateWidgetType.template.bundleAlias, templateWidgetType.template.alias, true,
  86 + ignoreErrors, ignoreLoading).pipe(
  87 + map((result) => {
  88 + const widgetInfo = toWidgetInfo(result);
  89 + widgetInfo.alias = undefined;
  90 + return widgetInfo;
  91 + })
  92 + );
  93 + }
73 } 94 }
@@ -14,10 +14,13 @@ @@ -14,10 +14,13 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { createFeatureSelector, createSelector } from '@ngrx/store'; 17 +import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store';
18 18
19 import { AppState } from '../core.state'; 19 import { AppState } from '../core.state';
20 import { LoadState } from './load.models'; 20 import { LoadState } from './load.models';
  21 +import { AuthUser } from '@shared/models/user.model';
  22 +import { take } from 'rxjs/operators';
  23 +import { selectAuthUser } from '@core/auth/auth.selectors';
21 24
22 export const selectLoadState = createFeatureSelector<AppState, LoadState>( 25 export const selectLoadState = createFeatureSelector<AppState, LoadState>(
23 'load' 26 'load'
@@ -32,3 +35,11 @@ export const selectIsLoading = createSelector( @@ -32,3 +35,11 @@ export const selectIsLoading = createSelector(
32 selectLoadState, 35 selectLoadState,
33 (state: LoadState) => state.isLoading 36 (state: LoadState) => state.isLoading
34 ); 37 );
  38 +
  39 +export function getCurrentIsLoading(store: Store<AppState>): boolean {
  40 + let isLoading: boolean;
  41 + store.pipe(select(selectIsLoading), take(1)).subscribe(
  42 + val => isLoading = val
  43 + );
  44 + return isLoading;
  45 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { UtilsService } from '@core/services/utils.service';
  19 +import { TimeService } from '@core/services/time.service';
  20 +import {
  21 + Dashboard,
  22 + DashboardLayout,
  23 + DashboardStateLayouts,
  24 + DashboardState,
  25 + DashboardConfiguration
  26 +} from '@shared/models/dashboard.models';
  27 +import { isUndefined, isDefined, isString } from '@core/utils';
  28 +import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models';
  29 +import { EntityType } from '@shared/models/entity-type.models';
  30 +import { EntityAlias, AliasFilterType } from '@app/shared/models/alias.models';
  31 +
  32 +@Injectable({
  33 + providedIn: 'root'
  34 +})
  35 +export class DashboardUtilsService {
  36 +
  37 + constructor(private utils: UtilsService,
  38 + private timeService: TimeService) {
  39 + }
  40 +
  41 + public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
  42 + if (!dashboard.configuration) {
  43 + dashboard.configuration = {};
  44 + }
  45 + if (isUndefined(dashboard.configuration.widgets)) {
  46 + dashboard.configuration.widgets = {};
  47 + } else if (Array.isArray(dashboard.configuration.widgets)) {
  48 + const widgetsMap: {[id: string]: Widget} = {};
  49 + dashboard.configuration.widgets.forEach((widget) => {
  50 + if (!widget.id) {
  51 + widget.id = this.utils.guid();
  52 + }
  53 + widgetsMap[widget.id] = widget;
  54 + });
  55 + dashboard.configuration.widgets = widgetsMap;
  56 + }
  57 + for (const id of Object.keys(dashboard.configuration.widgets)) {
  58 + const widget = dashboard.configuration.widgets[id];
  59 + dashboard.configuration.widgets[id] = this.validateAndUpdateWidget(widget);
  60 + }
  61 + if (isUndefined(dashboard.configuration.states)) {
  62 + dashboard.configuration.states = {
  63 + default: this.createDefaultState(dashboard.title, true)
  64 + };
  65 +
  66 + const mainLayout = dashboard.configuration.states.default.layouts.main;
  67 + for (const id of Object.keys(dashboard.configuration.widgets)) {
  68 + const widget = dashboard.configuration.widgets[id];
  69 + mainLayout.widgets[id] = {
  70 + sizeX: widget.sizeX,
  71 + sizeY: widget.sizeY,
  72 + row: widget.row,
  73 + col: widget.col
  74 + };
  75 + if (isDefined(widget.config.mobileHeight)) {
  76 + mainLayout.widgets[id].mobileHeight = widget.config.mobileHeight;
  77 + }
  78 + if (isDefined(widget.config.mobileOrder)) {
  79 + mainLayout.widgets[id].mobileOrder = widget.config.mobileOrder;
  80 + }
  81 + }
  82 + } else {
  83 + const states = dashboard.configuration.states;
  84 + let rootFound = false;
  85 + for (const stateId of Object.keys(states)) {
  86 + const state = states[stateId];
  87 + if (isUndefined(state.root)) {
  88 + state.root = false;
  89 + } else if (state.root) {
  90 + rootFound = true;
  91 + }
  92 + }
  93 + if (!rootFound) {
  94 + const firstStateId = Object.keys(states)[0];
  95 + states[firstStateId].root = true;
  96 + }
  97 + }
  98 + const datasourcesByAliasId: {[aliasId: string]: Array<Datasource>} = {};
  99 + const targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>} = {};
  100 + for (const widgetId of Object.keys(dashboard.configuration.widgets)) {
  101 + const widget = dashboard.configuration.widgets[widgetId];
  102 + widget.config.datasources.forEach((datasource) => {
  103 + if (datasource.entityAliasId) {
  104 + const aliasId = datasource.entityAliasId;
  105 + let aliasDatasources = datasourcesByAliasId[aliasId];
  106 + if (!aliasDatasources) {
  107 + aliasDatasources = [];
  108 + datasourcesByAliasId[aliasId] = aliasDatasources;
  109 + }
  110 + aliasDatasources.push(datasource);
  111 + }
  112 + });
  113 + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length) {
  114 + const aliasId = widget.config.targetDeviceAliasIds[0];
  115 + let targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
  116 + if (!targetDeviceAliasIdsList) {
  117 + targetDeviceAliasIdsList = [];
  118 + targetDevicesByAliasId[aliasId] = targetDeviceAliasIdsList;
  119 + }
  120 + targetDeviceAliasIdsList.push(widget.config.targetDeviceAliasIds);
  121 + }
  122 + }
  123 +
  124 + dashboard.configuration = this.validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId);
  125 +
  126 + if (isUndefined(dashboard.configuration.timewindow)) {
  127 + dashboard.configuration.timewindow = this.timeService.defaultTimewindow();
  128 + }
  129 + if (isUndefined(dashboard.configuration.settings)) {
  130 + dashboard.configuration.settings = {};
  131 + dashboard.configuration.settings.stateControllerId = 'entity';
  132 + dashboard.configuration.settings.showTitle = false;
  133 + dashboard.configuration.settings.showDashboardsSelect = true;
  134 + dashboard.configuration.settings.showEntitiesSelect = true;
  135 + dashboard.configuration.settings.showDashboardTimewindow = true;
  136 + dashboard.configuration.settings.showDashboardExport = true;
  137 + dashboard.configuration.settings.toolbarAlwaysOpen = true;
  138 + } else {
  139 + if (isUndefined(dashboard.configuration.settings.stateControllerId)) {
  140 + dashboard.configuration.settings.stateControllerId = 'entity';
  141 + }
  142 + }
  143 + if (isDefined(dashboard.configuration.gridSettings)) {
  144 + const gridSettings = dashboard.configuration.gridSettings;
  145 + if (isDefined(gridSettings.showTitle)) {
  146 + dashboard.configuration.settings.showTitle = gridSettings.showTitle;
  147 + delete gridSettings.showTitle;
  148 + }
  149 + if (isDefined(gridSettings.titleColor)) {
  150 + dashboard.configuration.settings.titleColor = gridSettings.titleColor;
  151 + delete gridSettings.titleColor;
  152 + }
  153 + if (isDefined(gridSettings.showDevicesSelect)) {
  154 + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect;
  155 + delete gridSettings.showDevicesSelect;
  156 + }
  157 + if (isDefined(gridSettings.showEntitiesSelect)) {
  158 + dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect;
  159 + delete gridSettings.showEntitiesSelect;
  160 + }
  161 + if (isDefined(gridSettings.showDashboardTimewindow)) {
  162 + dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow;
  163 + delete gridSettings.showDashboardTimewindow;
  164 + }
  165 + if (isDefined(gridSettings.showDashboardExport)) {
  166 + dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport;
  167 + delete gridSettings.showDashboardExport;
  168 + }
  169 + dashboard.configuration.states.default.layouts.main.gridSettings = gridSettings;
  170 + delete dashboard.configuration.gridSettings;
  171 + }
  172 + return dashboard;
  173 + }
  174 +
  175 + public createSingleWidgetDashboard(widget: Widget): Dashboard {
  176 + if (!widget.id) {
  177 + widget.id = this.utils.guid();
  178 + }
  179 + let dashboard: Dashboard = {};
  180 + dashboard = this.validateAndUpdateDashboard(dashboard);
  181 + dashboard.configuration.widgets[widget.id] = widget;
  182 + dashboard.configuration.states.default.layouts.main.widgets[widget.id] = {
  183 + sizeX: widget.sizeX,
  184 + sizeY: widget.sizeY,
  185 + row: widget.row,
  186 + col: widget.col,
  187 + };
  188 + return dashboard;
  189 + }
  190 +
  191 + public validateAndUpdateWidget(widget: Widget): Widget {
  192 + if (!widget.config) {
  193 + widget.config = {};
  194 + }
  195 + if (!widget.config.datasources) {
  196 + widget.config.datasources = [];
  197 + }
  198 + widget.config.datasources.forEach((datasource) => {
  199 + if (datasource.type === 'device') {
  200 + datasource.type = DatasourceType.entity;
  201 + }
  202 + if (datasource.deviceAliasId) {
  203 + datasource.entityAliasId = datasource.deviceAliasId;
  204 + delete datasource.deviceAliasId;
  205 + }
  206 + });
  207 + // TODO: Temp workaround
  208 + if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
  209 + widget.typeAlias = 'basic_timeseries';
  210 + }
  211 + return widget;
  212 + }
  213 +
  214 + public createDefaultLayoutData(): DashboardLayout {
  215 + return {
  216 + widgets: {},
  217 + gridSettings: {
  218 + backgroundColor: '#eeeeee',
  219 + color: 'rgba(0,0,0,0.870588)',
  220 + columns: 24,
  221 + margins: [10, 10],
  222 + backgroundSizeMode: '100%'
  223 + }
  224 + };
  225 + }
  226 +
  227 + public createDefaultLayouts(): DashboardStateLayouts {
  228 + return {
  229 + main: this.createDefaultLayoutData()
  230 + };
  231 + }
  232 +
  233 + public createDefaultState(name: string, root: boolean): DashboardState {
  234 + return {
  235 + name,
  236 + root,
  237 + layouts: this.createDefaultLayouts()
  238 + };
  239 + }
  240 +
  241 + private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
  242 + datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
  243 + targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {
  244 + let entityAlias: EntityAlias;
  245 + if (isUndefined(configuration.entityAliases)) {
  246 + configuration.entityAliases = {};
  247 + if (configuration.deviceAliases) {
  248 + const deviceAliases = configuration.deviceAliases;
  249 + for (const aliasId of Object.keys(deviceAliases)) {
  250 + const deviceAlias = deviceAliases[aliasId];
  251 + entityAlias = this.validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId);
  252 + configuration.entityAliases[entityAlias.id] = entityAlias;
  253 + }
  254 + delete configuration.deviceAliases;
  255 + }
  256 + } else {
  257 + const entityAliases = configuration.entityAliases;
  258 + for (const aliasId of Object.keys(entityAliases)) {
  259 + entityAlias = entityAliases[aliasId];
  260 + entityAlias = this.validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId);
  261 + if (aliasId !== entityAlias.id) {
  262 + delete entityAliases[aliasId];
  263 + }
  264 + entityAliases[entityAlias.id] = entityAlias;
  265 + }
  266 + }
  267 + return configuration;
  268 + }
  269 +
  270 + private validateAndUpdateDeviceAlias(aliasId: string,
  271 + deviceAlias: any,
  272 + datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
  273 + targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): EntityAlias {
  274 + aliasId = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
  275 + const alias = deviceAlias.alias;
  276 + const entityAlias: EntityAlias = {
  277 + id: aliasId,
  278 + alias,
  279 + filter: {
  280 + type: null,
  281 + entityType: EntityType.DEVICE,
  282 + resolveMultiple: false
  283 + },
  284 + };
  285 + if (deviceAlias.deviceFilter) {
  286 + entityAlias.filter.type =
  287 + deviceAlias.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList;
  288 + if (entityAlias.filter.type === AliasFilterType.entityList) {
  289 + entityAlias.filter.entityList = deviceAlias.deviceFilter.deviceList;
  290 + } else {
  291 + entityAlias.filter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
  292 + }
  293 + } else {
  294 + entityAlias.filter.type = AliasFilterType.entityList;
  295 + entityAlias.filter.entityList = [deviceAlias.deviceId];
  296 + }
  297 + return entityAlias;
  298 + }
  299 +
  300 + private validateAndUpdateEntityAlias(aliasId: string, entityAlias: EntityAlias,
  301 + datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
  302 + targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): EntityAlias {
  303 + entityAlias.id = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
  304 + if (!entityAlias.filter) {
  305 + entityAlias.filter = {
  306 + type: entityAlias.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList,
  307 + entityType: entityAlias.entityType,
  308 + resolveMultiple: false
  309 + };
  310 + if (entityAlias.filter.type === AliasFilterType.entityList) {
  311 + entityAlias.filter.entityList = entityAlias.entityFilter.entityList;
  312 + } else {
  313 + entityAlias.filter.entityNameFilter = entityAlias.entityFilter.entityNameFilter;
  314 + }
  315 + delete entityAlias.entityType;
  316 + delete entityAlias.entityFilter;
  317 + }
  318 + return entityAlias;
  319 + }
  320 +
  321 + private validateAliasId(aliasId: string,
  322 + datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
  323 + targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): string {
  324 + if (!aliasId || !isString(aliasId) || aliasId.length !== 36) {
  325 + const newAliasId = this.utils.guid();
  326 + const aliasDatasources = datasourcesByAliasId[aliasId];
  327 + if (aliasDatasources) {
  328 + aliasDatasources.forEach(
  329 + (datasource) => {
  330 + datasource.entityAliasId = newAliasId;
  331 + }
  332 + );
  333 + }
  334 + const targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
  335 + if (targetDeviceAliasIdsList) {
  336 + targetDeviceAliasIdsList.forEach(
  337 + (targetDeviceAliasIds) => {
  338 + targetDeviceAliasIds[0] = newAliasId;
  339 + }
  340 + );
  341 + }
  342 + return newAliasId;
  343 + } else {
  344 + return aliasId;
  345 + }
  346 + }
  347 +
  348 +}
@@ -21,11 +21,12 @@ import { isUndefined, isDefined } from '@core/utils'; @@ -21,11 +21,12 @@ import { isUndefined, isDefined } from '@core/utils';
21 import { WindowMessage } from '@shared/models/window-message.model'; 21 import { WindowMessage } from '@shared/models/window-message.model';
22 import { TranslateService } from '@ngx-translate/core'; 22 import { TranslateService } from '@ngx-translate/core';
23 import { customTranslationsPrefix } from '@app/shared/models/constants'; 23 import { customTranslationsPrefix } from '@app/shared/models/constants';
24 -import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; 24 +import { DataKey, Datasource, DatasourceType, KeyInfo, Widget } from '@shared/models/widget.models';
25 import { EntityType } from '@shared/models/entity-type.models'; 25 import { EntityType } from '@shared/models/entity-type.models';
26 import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; 26 import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
27 import { alarmFields } from '@shared/models/alarm.models'; 27 import { alarmFields } from '@shared/models/alarm.models';
28 import { materialColors } from '@app/shared/models/material.models'; 28 import { materialColors } from '@app/shared/models/material.models';
  29 +import { WidgetInfo } from '@home/models/widget-component.models';
29 30
30 @Injectable({ 31 @Injectable({
31 providedIn: 'root' 32 providedIn: 'root'
@@ -34,7 +35,7 @@ export class UtilsService { @@ -34,7 +35,7 @@ export class UtilsService {
34 35
35 iframeMode = false; 36 iframeMode = false;
36 widgetEditMode = false; 37 widgetEditMode = false;
37 - editWidgetInfo: any = null; 38 + editWidgetInfo: WidgetInfo = null;
38 39
39 constructor(@Inject(WINDOW) private window: Window, 40 constructor(@Inject(WINDOW) private window: Window,
40 private translate: TranslateService) { 41 private translate: TranslateService) {
@@ -87,7 +88,7 @@ export class UtilsService { @@ -87,7 +88,7 @@ export class UtilsService {
87 type: 'widgetException', 88 type: 'widgetException',
88 data 89 data
89 }; 90 };
90 - this.window.parent.postMessage(message, '*'); 91 + this.window.parent.postMessage(JSON.stringify(message), '*');
91 } 92 }
92 return data; 93 return data;
93 } 94 }
@@ -95,6 +95,10 @@ export function isNumber(value: any): boolean { @@ -95,6 +95,10 @@ export function isNumber(value: any): boolean {
95 return typeof value === 'number'; 95 return typeof value === 'number';
96 } 96 }
97 97
  98 +export function isString(value: any): boolean {
  99 + return typeof value === 'string';
  100 +}
  101 +
98 export function objToBase64(obj: any): string { 102 export function objToBase64(obj: any): string {
99 const json = JSON.stringify(obj); 103 const json = JSON.stringify(obj);
100 const encoded = utf8Encode(json); 104 const encoded = utf8Encode(json);
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import { HomeComponentsModule } from '@modules/home/components/home-components.module';
  21 +import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module';
  22 +import { DashboardModule } from '@home/pages/dashboard/dashboard.module';
  23 +import { DashboardPagesRoutingModule } from './dashboard-pages.routing.module';
  24 +
  25 +@NgModule({
  26 + entryComponents: [],
  27 + imports: [
  28 + CommonModule,
  29 + SharedModule,
  30 + HomeComponentsModule,
  31 + HomeDialogsModule,
  32 + DashboardModule,
  33 + DashboardPagesRoutingModule
  34 + ]
  35 +})
  36 +export class DashboardPagesModule { }
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
  19 +
  20 +import {Authority} from '@shared/models/authority.enum';
  21 +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
  22 +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
  23 +import { widgetTypesBreadcumbLabelFunction } from '@home/pages/widget/widget-library-routing.module';
  24 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  25 +import { WidgetService } from '@core/http/widget.service';
  26 +import { Observable } from 'rxjs';
  27 +import { Dashboard } from '@app/shared/models/dashboard.models';
  28 +import { DashboardService } from '@core/http/dashboard.service';
  29 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  30 +import { map } from 'rxjs/operators';
  31 +import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module';
  32 +import { UtilsService } from '@core/services/utils.service';
  33 +import { Widget } from '@app/shared/models/widget.models';
  34 +
  35 +@Injectable()
  36 +export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
  37 +
  38 + constructor(private dashboardService: DashboardService,
  39 + private dashboardUtils: DashboardUtilsService,
  40 + private utils: UtilsService) {
  41 + }
  42 +
  43 + resolve(route: ActivatedRouteSnapshot): Dashboard {
  44 + const editWidgetInfo = this.utils.editWidgetInfo;
  45 + const widget: Widget = {
  46 + isSystemType: true,
  47 + bundleAlias: 'customWidgetBundle',
  48 + typeAlias: 'customWidget',
  49 + type: editWidgetInfo.type,
  50 + title: 'My widget',
  51 + sizeX: editWidgetInfo.sizeX * 2,
  52 + sizeY: editWidgetInfo.sizeY * 2,
  53 + row: 2,
  54 + col: 4,
  55 + config: JSON.parse(editWidgetInfo.defaultConfig)
  56 + };
  57 + widget.config.title = widget.config.title || editWidgetInfo.widgetName;
  58 + return this.dashboardUtils.createSingleWidgetDashboard(widget);
  59 + }
  60 +}
  61 +
  62 +const routes: Routes = [
  63 + {
  64 + path: 'dashboard/:dashboardId',
  65 + component: DashboardPageComponent,
  66 + data: {
  67 + breadcrumb: {
  68 + skip: true
  69 + },
  70 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  71 + title: 'dashboard.dashboard',
  72 + widgetEditMode: false,
  73 + singlePageMode: true
  74 + },
  75 + resolve: {
  76 + dashboard: DashboardResolver
  77 + }
  78 + },
  79 + {
  80 + path: 'widget-editor',
  81 + component: DashboardPageComponent,
  82 + data: {
  83 + breadcrumb: {
  84 + skip: true
  85 + },
  86 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  87 + title: 'widget.editor',
  88 + widgetEditMode: true,
  89 + singlePageMode: true
  90 + },
  91 + resolve: {
  92 + dashboard: WidgetEditorDashboardResolver
  93 + }
  94 + }
  95 +];
  96 +
  97 +@NgModule({
  98 + imports: [RouterModule.forChild(routes)],
  99 + exports: [RouterModule],
  100 + providers: [
  101 + WidgetEditorDashboardResolver
  102 + ]
  103 +})
  104 +export class DashboardPagesRoutingModule { }
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { Routes, RouterModule } from '@angular/router';
  19 +
  20 +import { AuthGuard } from '@core/guards/auth.guard';
  21 +import { StoreModule } from '@ngrx/store';
  22 +
  23 +const routes: Routes = [
  24 + { path: '',
  25 + data: {
  26 + title: 'dashboard.dashboard',
  27 + breadcrumb: {
  28 + skip: true
  29 + }
  30 + },
  31 + canActivate: [AuthGuard],
  32 + canActivateChild: [AuthGuard],
  33 + loadChildren: './dashboard-pages.module#DashboardPagesModule'
  34 + }
  35 +];
  36 +
  37 +@NgModule({
  38 + imports: [
  39 + StoreModule,
  40 + RouterModule.forChild(routes)],
  41 + exports: [RouterModule]
  42 +})
  43 +export class DashboardRoutingModule { }
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column" class="mat-content mat-padding">
  19 + <div fxLayout="row" *ngFor="let alias of entityAliasesInfo | keyvalue">
  20 + <mat-form-field>
  21 + <mat-label>{{alias.value.alias}}</mat-label>
  22 + <mat-select matInput [(ngModel)]="alias.value.selectedId"
  23 + (ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.selectedId)">
  24 + <mat-option *ngFor="let resolvedEntity of alias.value.resolvedEntities" [value]="resolvedEntity.id">
  25 + {{resolvedEntity.name}}
  26 + </mat-option>
  27 + </mat-select>
  28 + </mat-form-field>
  29 + </div>
  30 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + min-width: 300px;
  18 + max-height: 150px;
  19 + overflow-x: hidden;
  20 + overflow-y: auto;
  21 + background: #fff;
  22 + border-radius: 4px;
  23 + box-shadow:
  24 + 0 7px 8px -4px rgba(0, 0, 0, .2),
  25 + 0 13px 19px 2px rgba(0, 0, 0, .14),
  26 + 0 5px 24px 4px rgba(0, 0, 0, .12);
  27 +
  28 + @media (min-height: 350px) {
  29 + max-height: 250px;
  30 + }
  31 +
  32 + .mat-content {
  33 + background-color: #fff;
  34 + }
  35 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, InjectionToken, OnInit } from '@angular/core';
  18 +import { Timewindow } from '@shared/models/time/time.models';
  19 +import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { TIMEWINDOW_PANEL_DATA, TimewindowPanelData } from '@shared/components/time/timewindow-panel.component';
  22 +import { deepClone } from '@core/utils';
  23 +
  24 +export const ALIASES_ENTITY_SELECT_PANEL_DATA = new InjectionToken<any>('AliasesEntitySelectPanelData');
  25 +
  26 +export interface AliasesEntitySelectPanelData {
  27 + aliasController: IAliasController;
  28 +}
  29 +
  30 +@Component({
  31 + selector: 'tb-aliases-entity-select-panel',
  32 + templateUrl: './aliases-entity-select-panel.component.html',
  33 + styleUrls: ['./aliases-entity-select-panel.component.scss']
  34 +})
  35 +export class AliasesEntitySelectPanelComponent {
  36 +
  37 + entityAliasesInfo: {[aliasId: string]: AliasInfo} = {};
  38 +
  39 + constructor(@Inject(ALIASES_ENTITY_SELECT_PANEL_DATA) public data: AliasesEntitySelectPanelData) {
  40 + const allEntityAliases = this.data.aliasController.getEntityAliases();
  41 + for (const aliasId of Object.keys(allEntityAliases)) {
  42 + const aliasInfo = this.data.aliasController.getInstantAliasInfo(aliasId);
  43 + if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity
  44 + && aliasInfo.resolvedEntities.length > 1) {
  45 + this.entityAliasesInfo[aliasId] = deepClone(aliasInfo);
  46 + this.entityAliasesInfo[aliasId].selectedId = aliasInfo.currentEntity.id;
  47 + }
  48 + }
  49 + }
  50 +
  51 + public currentAliasEntityChanged(aliasId: string, selectedId: string) {
  52 + const resolvedEntities = this.entityAliasesInfo[aliasId].resolvedEntities;
  53 + const selected = resolvedEntities.find((entity) => entity.id === selectedId);
  54 + if (selected) {
  55 + this.data.aliasController.updateCurrentAliasEntity(aliasId, selected[0]);
  56 + }
  57 + }
  58 +
  59 +
  60 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section class="tb-aliases-entity-select" fxLayout="row" fxLayoutAlign="start center">
  19 + <button mat-button mat-icon-button
  20 + cdkOverlayOrigin #aliasEntitySelectPanelOrigin="cdkOverlayOrigin"
  21 + (click)="openEditMode()"
  22 + matTooltip="{{ 'entity.select-entities' | translate }}"
  23 + [matTooltipPosition]="tooltipPosition">
  24 + <mat-icon class="material-icons">devices_other</mat-icon>
  25 + </button>
  26 + <span fxHide.xs fxHide.sm fxHide.md
  27 + (click)="openEditMode()"
  28 + matTooltip="{{ 'entity.select-entities' | translate }}"
  29 + [matTooltipPosition]="tooltipPosition">
  30 + {{displayValue}}
  31 + </span>
  32 +</section>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + min-width: 52px;
  20 +
  21 + section.tb-aliases-entity-select {
  22 + min-height: 32px;
  23 + padding: 0 6px;
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding: 0;
  27 + }
  28 +
  29 + span {
  30 + max-width: 200px;
  31 + overflow: hidden;
  32 + text-overflow: ellipsis;
  33 + white-space: nowrap;
  34 + pointer-events: all;
  35 + cursor: pointer;
  36 + }
  37 + }
  38 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  18 +import { TooltipPosition } from '@angular/material';
  19 +import { IAliasController } from '@core/api/widget-api.models';
  20 +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { Subscription } from 'rxjs';
  23 +import { BreakpointObserver } from '@angular/cdk/layout';
  24 +import { DOCUMENT } from '@angular/common';
  25 +import { WINDOW } from '@core/services/window.service';
  26 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  27 +import {
  28 + ALIASES_ENTITY_SELECT_PANEL_DATA,
  29 + AliasesEntitySelectPanelComponent,
  30 + AliasesEntitySelectPanelData
  31 +} from './aliases-entity-select-panel.component';
  32 +
  33 +@Component({
  34 + selector: 'tb-aliases-entity-select',
  35 + templateUrl: './aliases-entity-select.component.html',
  36 + styleUrls: ['./aliases-entity-select.component.scss']
  37 +})
  38 +export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
  39 +
  40 + @Input()
  41 + aliasController: IAliasController;
  42 +
  43 + @Input()
  44 + tooltipPosition: TooltipPosition = 'above';
  45 +
  46 + @Input() disabled: boolean;
  47 +
  48 + @ViewChild('aliasEntitySelectPanelOrigin', {static: false}) aliasEntitySelectPanelOrigin: CdkOverlayOrigin;
  49 +
  50 + displayValue: string;
  51 +
  52 + private rxSubscriptions = new Array<Subscription>();
  53 +
  54 + constructor(private translate: TranslateService,
  55 + private overlay: Overlay,
  56 + private breakpointObserver: BreakpointObserver,
  57 + private viewContainerRef: ViewContainerRef,
  58 + @Inject(DOCUMENT) private document: Document,
  59 + @Inject(WINDOW) private window: Window) {
  60 + }
  61 +
  62 + ngOnInit(): void {
  63 + this.rxSubscriptions.push(this.aliasController.entityAliasesChanged.subscribe(
  64 + () => {
  65 + this.updateDisplayValue();
  66 + }
  67 + ));
  68 + this.rxSubscriptions.push(this.aliasController.entityAliasResolved.subscribe(
  69 + () => {
  70 + this.updateDisplayValue();
  71 + }
  72 + ));
  73 + }
  74 +
  75 + ngOnDestroy(): void {
  76 + this.rxSubscriptions.forEach((subscription) => {
  77 + subscription.unsubscribe();
  78 + });
  79 + this.rxSubscriptions.length = 0;
  80 + }
  81 +
  82 + openEditMode() {
  83 + if (this.disabled) {
  84 + return;
  85 + }
  86 + const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
  87 + const panelWidth = 300;
  88 + const position = this.overlay.position();
  89 + const config = new OverlayConfig({
  90 + panelClass: 'tb-aliases-entity-select-panel',
  91 + backdropClass: 'cdk-overlay-transparent-backdrop',
  92 + hasBackdrop: true,
  93 + });
  94 + const el = this.aliasEntitySelectPanelOrigin.elementRef.nativeElement;
  95 + const offset = el.getBoundingClientRect();
  96 + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
  97 + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
  98 + const bottomY = offset.bottom - scrollTop;
  99 + const leftX = offset.left - scrollLeft;
  100 + let originX;
  101 + let originY;
  102 + let overlayX;
  103 + let overlayY;
  104 + const wHeight = this.document.documentElement.clientHeight;
  105 + const wWidth = this.document.documentElement.clientWidth;
  106 + if (bottomY + panelHeight > wHeight) {
  107 + originY = 'top';
  108 + overlayY = 'bottom';
  109 + } else {
  110 + originY = 'bottom';
  111 + overlayY = 'top';
  112 + }
  113 + if (leftX + panelWidth > wWidth) {
  114 + originX = 'end';
  115 + overlayX = 'end';
  116 + } else {
  117 + originX = 'start';
  118 + overlayX = 'start';
  119 + }
  120 + const connectedPosition: ConnectedPosition = {
  121 + originX,
  122 + originY,
  123 + overlayX,
  124 + overlayY
  125 + };
  126 + config.positionStrategy = position.flexibleConnectedTo(this.aliasEntitySelectPanelOrigin.elementRef)
  127 + .withPositions([connectedPosition]);
  128 + const overlayRef = this.overlay.create(config);
  129 + overlayRef.backdropClick().subscribe(() => {
  130 + overlayRef.dispose();
  131 + });
  132 +
  133 + const injector = this._createAliasesEntitySelectPanelInjector(
  134 + overlayRef,
  135 + {
  136 + aliasController: this.aliasController
  137 + }
  138 + );
  139 + overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector));
  140 + }
  141 +
  142 + private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector {
  143 + const injectionTokens = new WeakMap<any, any>([
  144 + [ALIASES_ENTITY_SELECT_PANEL_DATA, data],
  145 + [OverlayRef, overlayRef]
  146 + ]);
  147 + return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  148 + }
  149 +
  150 + private updateDisplayValue() {
  151 + let displayValue;
  152 + let singleValue = true;
  153 + let currentAliasId;
  154 + const entityAliases = this.aliasController.getEntityAliases();
  155 + for (const aliasId of Object.keys(entityAliases)) {
  156 + const entityAlias = entityAliases[aliasId];
  157 + if (!entityAlias.filter.resolveMultiple) {
  158 + const resolvedAlias = this.aliasController.getInstantAliasInfo(aliasId);
  159 + if (resolvedAlias && resolvedAlias.currentEntity) {
  160 + if (!currentAliasId) {
  161 + currentAliasId = aliasId;
  162 + } else {
  163 + singleValue = false;
  164 + break;
  165 + }
  166 + }
  167 + }
  168 + }
  169 + if (singleValue && currentAliasId) {
  170 + const aliasInfo = this.aliasController.getInstantAliasInfo(currentAliasId);
  171 + displayValue = aliasInfo.currentEntity.name;
  172 + } else {
  173 + displayValue = this.translate.instant('entity.entities');
  174 + }
  175 + this.displayValue = displayValue;
  176 + }
  177 +
  178 +}
@@ -38,6 +38,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone @@ -38,6 +38,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
38 import { WidgetComponent } from '@home/components/widget/widget.component'; 38 import { WidgetComponent } from '@home/components/widget/widget.component';
39 import { WidgetComponentService } from './widget/widget-component.service'; 39 import { WidgetComponentService } from './widget/widget-component.service';
40 import { LegendComponent } from '@home/components/widget/legend.component'; 40 import { LegendComponent } from '@home/components/widget/legend.component';
  41 +import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
  42 +import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
41 43
42 @NgModule({ 44 @NgModule({
43 entryComponents: [ 45 entryComponents: [
@@ -48,7 +50,8 @@ import { LegendComponent } from '@home/components/widget/legend.component'; @@ -48,7 +50,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
48 AlarmTableHeaderComponent, 50 AlarmTableHeaderComponent,
49 AlarmDetailsDialogComponent, 51 AlarmDetailsDialogComponent,
50 AddAttributeDialogComponent, 52 AddAttributeDialogComponent,
51 - EditAttributeValuePanelComponent 53 + EditAttributeValuePanelComponent,
  54 + AliasesEntitySelectPanelComponent
52 ], 55 ],
53 declarations: 56 declarations:
54 [ 57 [
@@ -69,6 +72,8 @@ import { LegendComponent } from '@home/components/widget/legend.component'; @@ -69,6 +72,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
69 AttributeTableComponent, 72 AttributeTableComponent,
70 AddAttributeDialogComponent, 73 AddAttributeDialogComponent,
71 EditAttributeValuePanelComponent, 74 EditAttributeValuePanelComponent,
  75 + AliasesEntitySelectPanelComponent,
  76 + AliasesEntitySelectComponent,
72 DashboardComponent, 77 DashboardComponent,
73 WidgetComponent, 78 WidgetComponent,
74 LegendComponent 79 LegendComponent
@@ -89,6 +94,7 @@ import { LegendComponent } from '@home/components/widget/legend.component'; @@ -89,6 +94,7 @@ import { LegendComponent } from '@home/components/widget/legend.component';
89 AlarmTableComponent, 94 AlarmTableComponent,
90 AlarmDetailsDialogComponent, 95 AlarmDetailsDialogComponent,
91 AttributeTableComponent, 96 AttributeTableComponent,
  97 + AliasesEntitySelectComponent,
92 DashboardComponent, 98 DashboardComponent,
93 WidgetComponent, 99 WidgetComponent,
94 LegendComponent 100 LegendComponent
@@ -61,10 +61,9 @@ import { @@ -61,10 +61,9 @@ import {
61 WidgetTypeInstance 61 WidgetTypeInstance
62 } from '@home/models/widget-component.models'; 62 } from '@home/models/widget-component.models';
63 import { 63 import {
64 - EntityInfo,  
65 IWidgetSubscription, 64 IWidgetSubscription,
66 StateObject, 65 StateObject,
67 - StateParams, 66 + StateParams, SubscriptionEntityInfo,
68 SubscriptionInfo, 67 SubscriptionInfo,
69 WidgetSubscriptionContext, 68 WidgetSubscriptionContext,
70 WidgetSubscriptionOptions 69 WidgetSubscriptionOptions
@@ -1065,7 +1064,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -1065,7 +1064,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1065 this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); 1064 this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
1066 } 1065 }
1067 1066
1068 - private getActiveEntityInfo(): EntityInfo { 1067 + private getActiveEntityInfo(): SubscriptionEntityInfo {
1069 let entityInfo = this.widgetContext.activeEntityInfo; 1068 let entityInfo = this.widgetContext.activeEntityInfo;
1070 if (!entityInfo) { 1069 if (!entityInfo) {
1071 for (const id of Object.keys(this.widgetContext.subscriptions)) { 1070 for (const id of Object.keys(this.widgetContext.subscriptions)) {
@@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
38 <button mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()"> 38 <button mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()">
39 <mat-icon class="material-icons">menu</mat-icon> 39 <mat-icon class="material-icons">menu</mat-icon>
40 </button> 40 </button>
41 - <div fxFlex tb-breadcrumb class="mat-toolbar-tools"> 41 + <div fxFlex tb-breadcrumb [activeComponent]="activeComponent" class="mat-toolbar-tools">
42 </div> 42 </div>
43 <button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()"> 43 <button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()">
44 <mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 44 <mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
@@ -49,7 +49,7 @@ @@ -49,7 +49,7 @@
49 *ngIf="isLoading$ | async"> 49 *ngIf="isLoading$ | async">
50 </mat-progress-bar> 50 </mat-progress-bar>
51 <div fxFlex fxLayout="column" tb-toast class="tb-main-content"> 51 <div fxFlex fxLayout="column" tb-toast class="tb-main-content">
52 - <router-outlet></router-outlet> 52 + <router-outlet (activate)="activeComponent = $event;"></router-outlet>
53 </div> 53 </div>
54 </div> 54 </div>
55 </mat-sidenav-content> 55 </mat-sidenav-content>
@@ -40,6 +40,8 @@ import { MatSidenav } from '@angular/material'; @@ -40,6 +40,8 @@ import { MatSidenav } from '@angular/material';
40 }) 40 })
41 export class HomeComponent extends PageComponent implements OnInit { 41 export class HomeComponent extends PageComponent implements OnInit {
42 42
  43 + activeComponent: any;
  44 +
43 sidenavMode = 'side'; 45 sidenavMode = 'side';
44 sidenavOpened = true; 46 sidenavOpened = true;
45 47
@@ -31,12 +31,11 @@ import { @@ -31,12 +31,11 @@ import {
31 } from '@shared/models/widget.models'; 31 } from '@shared/models/widget.models';
32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; 32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
33 import { 33 import {
34 - EntityInfo,  
35 IAliasController, 34 IAliasController,
36 IStateController, 35 IStateController,
37 IWidgetSubscription, 36 IWidgetSubscription,
38 IWidgetUtils, 37 IWidgetUtils,
39 - RpcApi, 38 + RpcApi, SubscriptionEntityInfo,
40 TimewindowFunctions, 39 TimewindowFunctions,
41 WidgetActionsApi, 40 WidgetActionsApi,
42 WidgetSubscriptionApi 41 WidgetSubscriptionApi
@@ -85,7 +84,7 @@ export interface WidgetContext { @@ -85,7 +84,7 @@ export interface WidgetContext {
85 actionsApi?: WidgetActionsApi; 84 actionsApi?: WidgetActionsApi;
86 stateController?: IStateController; 85 stateController?: IStateController;
87 aliasController?: IAliasController; 86 aliasController?: IAliasController;
88 - activeEntityInfo?: EntityInfo; 87 + activeEntityInfo?: SubscriptionEntityInfo;
89 widgetTitleTemplate?: string; 88 widgetTitleTemplate?: string;
90 widgetTitle?: string; 89 widgetTitle?: string;
91 customHeaderActions?: Array<WidgetHeaderAction>; 90 customHeaderActions?: Array<WidgetHeaderAction>;
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
  19 + fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
  20 + <section class="tb-dashboard-toolbar"
  21 + [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-closed': !toolbarOpened }">
  22 + <tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen"
  23 + [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
  24 + <div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse"
  25 + fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center">
  26 + <div class="tb-dashboard-action-panel" fxFlex.md="30" fxLayout="row-reverse"
  27 + fxLayoutAlign.gt-sm="start center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
  28 + <button [fxShow]="showCloseToolbar()" mat-button mat-icon-button class="close-action"
  29 + matTooltip="{{ 'dashboard.close-toolbar' | translate }}"
  30 + matTooltipPosition="below"
  31 + (click)="closeToolbar()">
  32 + <mat-icon>arrow_forward</mat-icon>
  33 + </button>
  34 + <tb-user-menu *ngIf="isPublicUser() && forceFullscreen" fxHide.xs fxHide.sm displayUserInfo="true">
  35 + </tb-user-menu>
  36 + <button [fxShow]="showRightLayoutSwitch()" mat-button mat-icon-button
  37 + matTooltip="{{ (isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}"
  38 + matTooltipPosition="below"
  39 + (click)="toggleLayouts()">
  40 + <mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
  41 + </button>
  42 + <button [fxShow]="!hideFullscreenButton()" mat-button mat-icon-button
  43 + matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  44 + matTooltipPosition="below"
  45 + (click)="isFullscreen = !isFullscreen">
  46 + <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  47 + </button>
  48 + <button [fxShow]="isEdit || displayExport()" mat-button mat-icon-button
  49 + matTooltip="{{'dashboard.export' | translate}}"
  50 + matTooltipPosition="below"
  51 + (click)="exportDashboard($event)">
  52 + <mat-icon>file_download</mat-icon>
  53 + </button>
  54 + <tb-timewindow [fxShow]="isEdit || displayDashboardTimewindow()"
  55 + isToolbar="true"
  56 + direction="left"
  57 + tooltipPosition="below"
  58 + aggregation="true"
  59 + [(ngModel)]="dashboardCtx.dashboardTimewindow">
  60 + </tb-timewindow>
  61 + <tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()"
  62 + tooltipPosition="below"
  63 + [aliasController]="dashboardCtx.aliasController">
  64 + </tb-aliases-entity-select>
  65 + <button [fxShow]="isEdit" mat-button mat-icon-button
  66 + matTooltip="{{ 'entity.aliases' | translate }}"
  67 + matTooltipPosition="below"
  68 + (click)="openEntityAliases($event)">
  69 + <mat-icon>devices_other</mat-icon>
  70 + </button>
  71 + <button [fxShow]="isEdit" mat-button mat-icon-button
  72 + matTooltip="{{ 'dashboard.settings' | translate }}"
  73 + matTooltipPosition="below"
  74 + (click)="openDashboardSettings($event)">
  75 + <mat-icon>settings</mat-icon>
  76 + </button>
  77 + <tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()"
  78 + [(ngModel)]="currentDashboardId"
  79 + (ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
  80 + [customerId]="currentCustomerId"
  81 + [dashboardsScope]="currentDashboardScope">
  82 + </tb-dashboard-select>
  83 + </div>
  84 + <div class="tb-dashboard-action-panel" fxFlex.md="70" fxLayout="row-reverse"
  85 + fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
  86 + <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
  87 + </tb-user-menu>
  88 + <!-- TODO -->
  89 + </div>
  90 + </div>
  91 + </tb-dashboard-toolbar>
  92 + </section>
  93 + <section class="tb-dashboard-container tb-absolute-fill"
  94 + [ngClass]="{ 'is-fullscreen': forceFullscreen,
  95 + 'tb-dashboard-toolbar-opened': toolbarOpened,
  96 + 'tb-dashboard-toolbar-closed': !toolbarOpened }">
  97 + <section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center"
  98 + [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
  99 + <h3 [fxShow]="!isEdit && displayTitle()">{{ dashboard.title }}</h3>
  100 + <mat-form-field [fxShow]="isEdit" class="mat-block" style="height: 30px;">
  101 + <mat-label translate [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">dashboard.title</mat-label>
  102 + <input matInput class="tb-dashboard-title"
  103 + [ngStyle]="{'color': dashboard.configuration.settings.titleColor}"
  104 + required name="title" [(ngModel)]="dashboard.title">
  105 + </mat-form-field>
  106 + </section>
  107 + <div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}"
  108 + [ngClass]="{ 'tb-padded' : !widgetEditMode && (isEdit || displayTitle()), 'tb-shrinked' : isEditingWidget }">
  109 + <div [fxShow]="layouts.main.show"
  110 + id="tb-main-layout"
  111 + [ngStyle]="{width: mainLayoutWidth(),
  112 + height: mainLayoutHeight()}">
  113 + TODO: MAIN LAYOUT tb-dashboard-layout
  114 + </div>
  115 + <mat-sidenav-container *ngIf="layouts.right.show"
  116 + id="tb-right-layout">
  117 + <mat-sidenav
  118 + [ngStyle]="{minWidth: rightLayoutWidth(),
  119 + maxWidth: rightLayoutWidth(),
  120 + height: rightLayoutHeight(),
  121 + zIndex: 25}"
  122 + disableClose="true"
  123 + position="end"
  124 + [mode]="isMobile ? 'over' : 'side'"
  125 + [(opened)]="rightLayoutOpened">
  126 + TODO: RIGHT LAYOUT tb-dashboard-layout
  127 + </mat-sidenav>
  128 + </mat-sidenav-container>
  129 + </div>
  130 + <!--tb-details-sidenav TODO -->
  131 + <!--tb-details-sidenav TODO -->
  132 + <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
  133 + <!--md-fab-speed-dial TODO -->
  134 + <button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
  135 + mat-fab color="accent" class="tb-btn-footer"
  136 + [ngClass]="{'tb-hide': !isEdit || isAddingWidget}"
  137 + [disabled]="isLoading$ | async"
  138 + (click)="saveDashboard()"
  139 + matTooltip="{{ 'action.apply-changes' | translate }}"
  140 + matTooltipPosition="above">
  141 + <mat-icon>done</mat-icon>
  142 + </button>
  143 + <button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
  144 + mat-fab color="accent" class="tb-btn-footer"
  145 + [ngClass]="{'tb-hide': isAddingWidget || (isLoading$ | async)}"
  146 + [disabled]="isLoading$ | async"
  147 + (click)="toggleDashboardEditMode()"
  148 + matTooltip="{{ (isEdit ? 'action.decline-changes': 'action.enter-edit-mode') | translate }}"
  149 + matTooltipPosition="above">
  150 + <mat-icon>{{ isEdit ? 'close' : 'edit' }}</mat-icon>
  151 + </button>
  152 + </section>
  153 + </section>
  154 + <section class="tb-powered-by-footer" [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
  155 + <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ thingsboardVersion }}</a></span>
  156 + </section>
  157 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../../../../scss/constants";
  18 +
  19 +$toolbar-height: 50px !default;
  20 +$fullscreen-toolbar-height: 64px !default;
  21 +$mobile-toolbar-height: 84px !default;
  22 +
  23 +tb-dashboard-page {
  24 + display: flex;
  25 + width: 100%;
  26 + height: 100%;
  27 +}
  28 +
  29 +div.tb-dashboard-page {
  30 + &.mat-content {
  31 + background-color: #eee;
  32 + }
  33 + section.tb-dashboard-title {
  34 + position: absolute;
  35 + top: 0;
  36 + left: 20px;
  37 + mat-form-field {
  38 + .mat-form-field-infix {
  39 + width: 100%;
  40 + }
  41 + }
  42 + input.tb-dashboard-title {
  43 + height: 38px;
  44 + font-size: 2rem;
  45 + font-weight: 500;
  46 + letter-spacing: .005em;
  47 + }
  48 + }
  49 + div.tb-padded {
  50 + top: 60px;
  51 + }
  52 +
  53 + section.tb-padded {
  54 + top: 60px;
  55 + }
  56 +
  57 + div.tb-shrinked {
  58 + width: 40%;
  59 + }
  60 +
  61 + section.tb-dashboard-toolbar {
  62 + position: absolute;
  63 + top: 0;
  64 + left: 0;
  65 + z-index: 13;
  66 + pointer-events: none;
  67 +
  68 + &.tb-dashboard-toolbar-opened {
  69 + right: 0;
  70 + // transition: right .3s cubic-bezier(.55, 0, .55, .2);
  71 + }
  72 +
  73 + &.tb-dashboard-toolbar-closed {
  74 + right: 18px;
  75 + transition: right .3s cubic-bezier(.55, 0, .55, .2) .2s;
  76 + }
  77 + }
  78 +
  79 + .tb-dashboard-container {
  80 + &.tb-dashboard-toolbar-opened {
  81 + &.is-fullscreen {
  82 + margin-top: $mobile-toolbar-height;
  83 +
  84 + @media #{$mat-gt-sm} {
  85 + margin-top: $fullscreen-toolbar-height;
  86 + }
  87 + }
  88 +
  89 + &:not(.is-fullscreen) {
  90 + margin-top: $mobile-toolbar-height;
  91 +
  92 + @media #{$mat-gt-sm} {
  93 + margin-top: $toolbar-height;
  94 + }
  95 +
  96 + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2);
  97 + }
  98 + }
  99 +
  100 + &.tb-dashboard-toolbar-closed {
  101 + margin-top: 0;
  102 +
  103 + transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s;
  104 + }
  105 +
  106 + .tb-dashboard-layouts {
  107 + /*md-backdrop {
  108 + z-index: 1;
  109 + }*/
  110 + #tb-right-layout {
  111 + mat-sidenav {
  112 + z-index: 1;
  113 + }
  114 + }
  115 + }
  116 + }
  117 +
  118 + section.tb-powered-by-footer {
  119 + position: absolute;
  120 + right: 25px;
  121 + bottom: 5px;
  122 + z-index: 30;
  123 + pointer-events: none;
  124 +
  125 + span {
  126 + font-size: 12px;
  127 +
  128 + a {
  129 + font-weight: 700;
  130 + text-decoration: none;
  131 + pointer-events: all;
  132 + border: none;
  133 + }
  134 + }
  135 + }
  136 +
  137 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { ActivatedRoute, Router } from '@angular/router';
  22 +import { UtilsService } from '@core/services/utils.service';
  23 +import { AuthService } from '@core/auth/auth.service';
  24 +import { Dashboard, DashboardConfiguration, WidgetLayout } from '@app/shared/models/dashboard.models';
  25 +import { WINDOW } from '@core/services/window.service';
  26 +import { WindowMessage } from '@shared/models/window-message.model';
  27 +import { deepClone, isDefined } from '@app/core/utils';
  28 +import {
  29 + DashboardContext,
  30 + DashboardPageLayoutContext,
  31 + DashboardPageLayouts,
  32 + DashboardPageScope
  33 +} from './dashboard-page.models';
  34 +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
  35 +import { MediaBreakpoints } from '@shared/models/constants';
  36 +import { AuthUser } from '@shared/models/user.model';
  37 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  38 +import { Widget } from '@app/shared/models/widget.models';
  39 +import { environment as env } from '@env/environment';
  40 +import { Authority } from '@shared/models/authority.enum';
  41 +import { DialogService } from '@core/services/dialog.service';
  42 +import { EntityService } from '@core/http/entity.service';
  43 +import { AliasController } from '@core/api/alias-controller';
  44 +import { Subscription } from 'rxjs';
  45 +
  46 +@Component({
  47 + selector: 'tb-dashboard-page',
  48 + templateUrl: './dashboard-page.component.html',
  49 + styleUrls: ['./dashboard-page.component.scss'],
  50 + encapsulation: ViewEncapsulation.None
  51 +})
  52 +export class DashboardPageComponent extends PageComponent implements OnDestroy {
  53 +
  54 + authUser: AuthUser = getCurrentAuthUser(this.store);
  55 +
  56 + dashboard: Dashboard;
  57 + dashboardConfiguration: DashboardConfiguration;
  58 +
  59 + prevDashboard: Dashboard;
  60 +
  61 + iframeMode = this.utils.iframeMode;
  62 + widgetEditMode: boolean;
  63 + singlePageMode: boolean;
  64 + forceFullscreen = this.authService.forceFullscreen;
  65 +
  66 + isFullscreen = false;
  67 + isEdit = false;
  68 + isEditingWidget = false;
  69 + isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
  70 + forceDashboardMobileMode = false;
  71 + isAddingWidget = false;
  72 +
  73 + isToolbarOpened = false;
  74 + isRightLayoutOpened = false;
  75 +
  76 + editingWidget: Widget = null;
  77 + editingWidgetLayout: WidgetLayout = null;
  78 + editingWidgetOriginal: Widget = null;
  79 + editingWidgetLayoutOriginal: WidgetLayout = null;
  80 + editingWidgetSubtitle: string = null;
  81 + editingLayoutCtx: DashboardPageLayoutContext = null;
  82 +
  83 + thingsboardVersion: string = env.tbVersion;
  84 +
  85 + currentDashboardId: string;
  86 + currentCustomerId: string;
  87 + currentDashboardScope: DashboardPageScope;
  88 +
  89 + layouts: DashboardPageLayouts = {
  90 + main: {
  91 + show: false,
  92 + layoutCtx: {
  93 + id: 'main',
  94 + widgets: [],
  95 + widgetLayouts: {},
  96 + gridSettings: {},
  97 + ignoreLoading: false
  98 + }
  99 + },
  100 + right: {
  101 + show: false,
  102 + layoutCtx: {
  103 + id: 'right',
  104 + widgets: [],
  105 + widgetLayouts: {},
  106 + gridSettings: {},
  107 + ignoreLoading: false
  108 + }
  109 + }
  110 + };
  111 +
  112 + dashboardCtx: DashboardContext = {
  113 + dashboard: null,
  114 + dashboardTimewindow: null,
  115 + state: null,
  116 + stateController: {
  117 + openRightLayout: this.openRightLayout.bind(this)
  118 + },
  119 + aliasController: null
  120 + };
  121 +
  122 + private rxSubscriptions = new Array<Subscription>();
  123 +
  124 + get toolbarOpened(): boolean {
  125 + return !this.widgetEditMode &&
  126 + (this.toolbarAlwaysOpen() || this.isToolbarOpened || this.isEdit || this.showRightLayoutSwitch());
  127 + }
  128 + set toolbarOpened(toolbarOpened: boolean) {
  129 + }
  130 +
  131 + get rightLayoutOpened(): boolean {
  132 + return !this.isMobile || this.isRightLayoutOpened;
  133 + }
  134 + set rightLayoutOpened(rightLayoutOpened: boolean) {
  135 + }
  136 +
  137 + constructor(protected store: Store<AppState>,
  138 + @Inject(WINDOW) private window: Window,
  139 + private breakpointObserver: BreakpointObserver,
  140 + private route: ActivatedRoute,
  141 + private router: Router,
  142 + private utils: UtilsService,
  143 + private authService: AuthService,
  144 + private entityService: EntityService,
  145 + private dialogService: DialogService) {
  146 + super(store);
  147 +
  148 + this.rxSubscriptions.push(this.route.data.subscribe(
  149 + (data) => {
  150 + this.init(data);
  151 + }
  152 + ));
  153 +
  154 + this.rxSubscriptions.push(this.breakpointObserver
  155 + .observe(MediaBreakpoints['gt-sm'])
  156 + .subscribe((state: BreakpointState) => {
  157 + this.isMobile = !state.matches;
  158 + }
  159 + ));
  160 + }
  161 +
  162 + private init(data: any) {
  163 +
  164 + this.reset();
  165 +
  166 + this.currentDashboardId = this.route.snapshot.params.dashboardId;
  167 +
  168 + if (this.route.snapshot.params.customerId) {
  169 + this.currentCustomerId = this.route.snapshot.params.customerId;
  170 + this.currentDashboardScope = 'customer';
  171 + } else {
  172 + this.currentDashboardScope = this.authUser.authority === Authority.TENANT_ADMIN ? 'tenant' : 'customer';
  173 + this.currentCustomerId = this.authUser.customerId;
  174 + }
  175 +
  176 + this.dashboard = data.dashboard;
  177 + this.dashboardConfiguration = this.dashboard.configuration;
  178 + this.widgetEditMode = data.widgetEditMode;
  179 + this.singlePageMode = data.singlePageMode;
  180 +
  181 + this.dashboardCtx.dashboard = this.dashboard;
  182 + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
  183 + this.dashboardCtx.aliasController = new AliasController(this.utils,
  184 + this.entityService,
  185 + this.dashboardCtx.stateController,
  186 + this.dashboardConfiguration.entityAliases);
  187 +
  188 + if (this.widgetEditMode) {
  189 + const message: WindowMessage = {
  190 + type: 'widgetEditModeInited'
  191 + };
  192 + this.window.parent.postMessage(JSON.stringify(message), '*');
  193 + }
  194 + }
  195 +
  196 + private reset() {
  197 + this.dashboard = null;
  198 + this.dashboardConfiguration = null;
  199 + this.prevDashboard = null;
  200 +
  201 + this.widgetEditMode = false;
  202 + this.singlePageMode = false;
  203 +
  204 + this.isFullscreen = false;
  205 + this.isEdit = false;
  206 + this.isEditingWidget = false;
  207 + this.forceDashboardMobileMode = false;
  208 + this.isAddingWidget = false;
  209 +
  210 + this.isToolbarOpened = false;
  211 + this.isRightLayoutOpened = false;
  212 +
  213 + this.editingWidget = null;
  214 + this.editingWidgetLayout = null;
  215 + this.editingWidgetOriginal = null;
  216 + this.editingWidgetLayoutOriginal = null;
  217 + this.editingWidgetSubtitle = null;
  218 + this.editingLayoutCtx = null;
  219 +
  220 + this.currentDashboardId = null;
  221 + this.currentCustomerId = null;
  222 + this.currentDashboardScope = null;
  223 + }
  224 +
  225 + ngOnDestroy(): void {
  226 + this.rxSubscriptions.forEach((subscription) => {
  227 + subscription.unsubscribe();
  228 + });
  229 + this.rxSubscriptions.length = 0;
  230 + }
  231 +
  232 + public openToolbar() {
  233 + this.isToolbarOpened = true;
  234 + }
  235 +
  236 + public closeToolbar() {
  237 + this.isToolbarOpened = false;
  238 + }
  239 +
  240 + public showCloseToolbar() {
  241 + return !this.toolbarAlwaysOpen() && !this.isEdit && !this.showRightLayoutSwitch();
  242 + }
  243 +
  244 + public hideFullscreenButton(): boolean {
  245 + return this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode;
  246 + }
  247 +
  248 + public toolbarAlwaysOpen(): boolean {
  249 + if (this.dashboard.configuration.settings &&
  250 + isDefined(this.dashboard.configuration.settings.toolbarAlwaysOpen)) {
  251 + return this.dashboard.configuration.settings.toolbarAlwaysOpen;
  252 + } else {
  253 + return true;
  254 + }
  255 + }
  256 +
  257 + public displayTitle(): boolean {
  258 + if (this.dashboard.configuration.settings &&
  259 + isDefined(this.dashboard.configuration.settings.showTitle)) {
  260 + return this.dashboard.configuration.settings.showTitle;
  261 + } else {
  262 + return false;
  263 + }
  264 + }
  265 +
  266 + public displayExport(): boolean {
  267 + if (this.dashboard.configuration.settings &&
  268 + isDefined(this.dashboard.configuration.settings.showDashboardExport)) {
  269 + return this.dashboard.configuration.settings.showDashboardExport;
  270 + } else {
  271 + return true;
  272 + }
  273 + }
  274 +
  275 + public displayDashboardTimewindow(): boolean {
  276 + if (this.dashboard.configuration.settings &&
  277 + isDefined(this.dashboard.configuration.settings.showDashboardTimewindow)) {
  278 + return this.dashboard.configuration.settings.showDashboardTimewindow;
  279 + } else {
  280 + return true;
  281 + }
  282 + }
  283 +
  284 + public displayDashboardsSelect(): boolean {
  285 + if (this.dashboard.configuration.settings &&
  286 + isDefined(this.dashboard.configuration.settings.showDashboardsSelect)) {
  287 + return this.dashboard.configuration.settings.showDashboardsSelect;
  288 + } else {
  289 + return true;
  290 + }
  291 + }
  292 +
  293 + public displayEntitiesSelect(): boolean {
  294 + if (this.dashboard.configuration.settings &&
  295 + isDefined(this.dashboard.configuration.settings.showEntitiesSelect)) {
  296 + return this.dashboard.configuration.settings.showEntitiesSelect;
  297 + } else {
  298 + return true;
  299 + }
  300 + }
  301 +
  302 + public showRightLayoutSwitch(): boolean {
  303 + return this.isMobile && this.layouts.right.show;
  304 + }
  305 +
  306 + public toggleLayouts() {
  307 + this.isRightLayoutOpened = !this.isRightLayoutOpened;
  308 + }
  309 +
  310 + public openRightLayout() {
  311 + this.isRightLayoutOpened = true;
  312 + }
  313 +
  314 + public mainLayoutWidth(): string {
  315 + if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
  316 + return '100%';
  317 + } else {
  318 + return this.layouts.right.show && !this.isMobile ? '50%' : '100%';
  319 + }
  320 + }
  321 +
  322 + public mainLayoutHeight(): string {
  323 + if (!this.isEditingWidget || this.editingLayoutCtx.id === 'main') {
  324 + return '100%';
  325 + } else {
  326 + return '0px';
  327 + }
  328 + }
  329 +
  330 + public rightLayoutWidth(): string {
  331 + if (this.isEditingWidget && this.editingLayoutCtx.id === 'right') {
  332 + return '100%';
  333 + } else {
  334 + return this.isMobile ? '100%' : '50%';
  335 + }
  336 + }
  337 +
  338 + public rightLayoutHeight(): string {
  339 + if (!this.isEditingWidget || this.editingLayoutCtx.id === 'right') {
  340 + return '100%';
  341 + } else {
  342 + return '0px';
  343 + }
  344 + }
  345 +
  346 + public isPublicUser(): boolean {
  347 + return this.authUser.isPublic;
  348 + }
  349 +
  350 + public isTenantAdmin(): boolean {
  351 + return this.authUser.authority === Authority.TENANT_ADMIN;
  352 + }
  353 +
  354 + public isSystemAdmin(): boolean {
  355 + return this.authUser.authority === Authority.SYS_ADMIN;
  356 + }
  357 +
  358 + public exportDashboard($event: Event) {
  359 + if ($event) {
  360 + $event.stopPropagation();
  361 + }
  362 + // TODO:
  363 + this.dialogService.todo();
  364 + }
  365 +
  366 + public openEntityAliases($event: Event) {
  367 + if ($event) {
  368 + $event.stopPropagation();
  369 + }
  370 + // TODO:
  371 + this.dialogService.todo();
  372 + }
  373 +
  374 + public openDashboardSettings($event: Event) {
  375 + if ($event) {
  376 + $event.stopPropagation();
  377 + }
  378 + // TODO:
  379 + this.dialogService.todo();
  380 + }
  381 +
  382 + public currentDashboardIdChanged(dashboardId: string) {
  383 + if (!this.widgetEditMode) {
  384 + if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) {
  385 + this.router.navigateByUrl(`customers/${this.currentCustomerId}/dashboards/${dashboardId}`);
  386 + } else {
  387 + if (this.singlePageMode) {
  388 + this.router.navigateByUrl(`dashboard/${dashboardId}`);
  389 + } else {
  390 + this.router.navigateByUrl(`dashboards/${dashboardId}`);
  391 + }
  392 + }
  393 + }
  394 + }
  395 +
  396 + public toggleDashboardEditMode() {
  397 + this.setEditMode(!this.isEdit, true);
  398 + }
  399 +
  400 + private setEditMode(isEdit: boolean, revert: boolean) {
  401 + this.isEdit = isEdit;
  402 + if (this.isEdit) {
  403 + // TODO:
  404 + // this.dashboardCtx.stateController.preserveState();
  405 + this.prevDashboard = deepClone(this.dashboard);
  406 + } else {
  407 + if (this.widgetEditMode) {
  408 + if (revert) {
  409 + this.dashboard = this.prevDashboard;
  410 + }
  411 + } else {
  412 + this.resetHighlight();
  413 + if (revert) {
  414 + this.dashboard = this.prevDashboard;
  415 + this.dashboardConfiguration = this.dashboard.configuration;
  416 + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
  417 + this.entityAliasesUpdated();
  418 + } else {
  419 + this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
  420 + }
  421 + }
  422 + }
  423 + }
  424 +
  425 + private resetHighlight() {
  426 + for (const l of Object.keys(this.layouts)) {
  427 + if (this.layouts[l].layoutCtx) {
  428 + if (this.layouts[l].layoutCtx.ctrl) {
  429 + this.layouts[l].layoutCtx.ctrl.resetHighlight();
  430 + }
  431 + }
  432 + }
  433 + }
  434 +
  435 + private entityAliasesUpdated() {
  436 + this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
  437 + }
  438 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard } from '@app/shared/models/dashboard.models';
  18 +import { Widget } from '@app/shared/models/widget.models';
  19 +import { Timewindow } from '@shared/models/time/time.models';
  20 +import { IAliasController, IStateController } from '@core/api/widget-api.models';
  21 +
  22 +export declare type DashboardPageScope = 'tenant' | 'customer';
  23 +
  24 +export interface DashboardContext {
  25 + state: string;
  26 + dashboard: Dashboard;
  27 + dashboardTimewindow: Timewindow;
  28 + aliasController: IAliasController;
  29 + stateController: IStateController;
  30 +}
  31 +
  32 +export interface DashboardPageLayoutContext {
  33 + id: DashboardLayoutId;
  34 + widgets: Array<Widget>;
  35 + widgetLayouts: {[id: string]: WidgetLayout};
  36 + gridSettings: GridSettings;
  37 + ignoreLoading: boolean;
  38 +}
  39 +
  40 +export interface DashboardPageLayout {
  41 + show: boolean;
  42 + layoutCtx: DashboardPageLayoutContext;
  43 +}
  44 +
  45 +export interface DashboardPageLayouts {
  46 + main: DashboardPageLayout;
  47 + right: DashboardPageLayout;
  48 +}
@@ -14,12 +14,39 @@ @@ -14,12 +14,39 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {NgModule} from '@angular/core';  
18 -import {RouterModule, Routes} from '@angular/router'; 17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; 20 import {EntitiesTableComponent} from '../../components/entity/entities-table.component';
21 import {Authority} from '@shared/models/authority.enum'; 21 import {Authority} from '@shared/models/authority.enum';
22 import {DashboardsTableConfigResolver} from './dashboards-table-config.resolver'; 22 import {DashboardsTableConfigResolver} from './dashboards-table-config.resolver';
  23 +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
  24 +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
  25 +import { widgetTypesBreadcumbLabelFunction } from '@home/pages/widget/widget-library-routing.module';
  26 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  27 +import { WidgetService } from '@core/http/widget.service';
  28 +import { Observable } from 'rxjs';
  29 +import { Dashboard } from '@app/shared/models/dashboard.models';
  30 +import { DashboardService } from '@core/http/dashboard.service';
  31 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  32 +import { map } from 'rxjs/operators';
  33 +
  34 +@Injectable()
  35 +export class DashboardResolver implements Resolve<Dashboard> {
  36 +
  37 + constructor(private dashboardService: DashboardService,
  38 + private dashboardUtils: DashboardUtilsService) {
  39 + }
  40 +
  41 + resolve(route: ActivatedRouteSnapshot): Observable<Dashboard> {
  42 + const dashboardId = route.params.dashboardId;
  43 + return this.dashboardService.getDashboard(dashboardId).pipe(
  44 + map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard))
  45 + );
  46 + }
  47 +}
  48 +
  49 +export const dashboardBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => component.dashboard.title);
23 50
24 const routes: Routes = [ 51 const routes: Routes = [
25 { 52 {
@@ -42,6 +69,22 @@ const routes: Routes = [ @@ -42,6 +69,22 @@ const routes: Routes = [
42 resolve: { 69 resolve: {
43 entitiesTableConfig: DashboardsTableConfigResolver 70 entitiesTableConfig: DashboardsTableConfigResolver
44 } 71 }
  72 + },
  73 + {
  74 + path: ':dashboardId',
  75 + component: DashboardPageComponent,
  76 + data: {
  77 + breadcrumb: {
  78 + labelFunction: dashboardBreadcumbLabelFunction,
  79 + icon: 'dashboard'
  80 + } as BreadCrumbConfig,
  81 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  82 + title: 'dashboard.dashboard',
  83 + widgetEditMode: false
  84 + },
  85 + resolve: {
  86 + dashboard: DashboardResolver
  87 + }
45 } 88 }
46 ] 89 ]
47 } 90 }
@@ -51,7 +94,8 @@ const routes: Routes = [ @@ -51,7 +94,8 @@ const routes: Routes = [
51 imports: [RouterModule.forChild(routes)], 94 imports: [RouterModule.forChild(routes)],
52 exports: [RouterModule], 95 exports: [RouterModule],
53 providers: [ 96 providers: [
54 - DashboardsTableConfigResolver 97 + DashboardsTableConfigResolver,
  98 + DashboardResolver
55 ] 99 ]
56 }) 100 })
57 export class DashboardRoutingModule { } 101 export class DashboardRoutingModule { }
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-fab-toolbar [isOpen]="toolbarOpened"
  19 + color="primary"
  20 + direction="left"
  21 + [ngClass]="{'is-fullscreen': forceFullscreen, 'mat-elevation-z1': forceFullscreen && toolbarOpened }">
  22 + <mat-fab-trigger>
  23 + <button mat-button mat-icon-button mat-fab color="primary"
  24 + [matTooltip]="!toolbarOpened ? ('dashboard.open-toolbar' | translate) : ''"
  25 + matTooltipPosition="below"
  26 + (click)="onTriggerClick()">
  27 + <mat-icon>more_horiz</mat-icon>
  28 + </button>
  29 + </mat-fab-trigger>
  30 + <mat-toolbar color="primary">
  31 + <mat-fab-actions class="mat-toolbar-tools">
  32 + <ng-content></ng-content>
  33 + </mat-fab-actions>
  34 + </mat-toolbar>
  35 +</mat-fab-toolbar>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +$toolbar-height: 50px !default;
  19 +$fullscreen-toolbar-height: 64px !default;
  20 +$mobile-toolbar-height: 80px !default;
  21 +$half-mobile-toolbar-height: 40px !default;
  22 +$mobile-toolbar-height-total: 84px !default;
  23 +
  24 +tb-dashboard-toolbar {
  25 + mat-fab-toolbar {
  26 + mat-fab-trigger {
  27 + .mat-button {
  28 + &.mat-fab {
  29 + width: 36px;
  30 + height: 36px;
  31 + margin: 4px 0 0 4px;
  32 + line-height: 36px;
  33 + .mat-button-wrapper {
  34 + width: 36px;
  35 + height: 36px;
  36 + padding: 0 6px;
  37 + box-sizing: border-box;
  38 + }
  39 + mat-icon {
  40 + display: block;
  41 + position: absolute;
  42 + top: 25%;
  43 + width: 18px;
  44 + min-width: 18px;
  45 + height: 18px;
  46 + min-height: 18px;
  47 + margin: 0;
  48 + line-height: 18px;
  49 + }
  50 + }
  51 + }
  52 + }
  53 +
  54 + &.is-fullscreen {
  55 +
  56 + .mat-fab-toolbar-wrapper {
  57 + height: $mobile-toolbar-height-total;
  58 +
  59 + @media #{$mat-gt-sm} {
  60 + height: $fullscreen-toolbar-height;
  61 + }
  62 +
  63 + mat-toolbar {
  64 + height: $mobile-toolbar-height;
  65 + min-height: $mobile-toolbar-height;
  66 + .mat-toolbar-tools {
  67 + height: $mobile-toolbar-height;
  68 + min-height: $mobile-toolbar-height;
  69 + }
  70 + @media #{$mat-gt-sm} {
  71 + height: $fullscreen-toolbar-height;
  72 + min-height: $fullscreen-toolbar-height;
  73 + .mat-toolbar-tools {
  74 + height: $fullscreen-toolbar-height;
  75 + min-height: $fullscreen-toolbar-height;
  76 + }
  77 + }
  78 + }
  79 + }
  80 + }
  81 +
  82 + .mat-fab-toolbar-wrapper {
  83 + height: $mobile-toolbar-height-total;
  84 +
  85 + @media #{$mat-gt-sm} {
  86 + height: $toolbar-height;
  87 + }
  88 +
  89 + mat-toolbar {
  90 + height: $mobile-toolbar-height;
  91 + min-height: $mobile-toolbar-height;
  92 + .mat-toolbar-tools {
  93 + height: $mobile-toolbar-height;
  94 + min-height: $mobile-toolbar-height;
  95 + }
  96 +
  97 + @media #{$mat-gt-sm} {
  98 + height: $toolbar-height;
  99 + min-height: $toolbar-height;
  100 + .mat-toolbar-tools {
  101 + height: $toolbar-height;
  102 + min-height: $toolbar-height;
  103 + }
  104 + }
  105 +
  106 + mat-fab-actions {
  107 + margin-top: 0;
  108 + font-size: 16px;
  109 +
  110 + @media #{$mat-lt-md} {
  111 + height: $mobile-toolbar-height;
  112 + max-height: $mobile-toolbar-height;
  113 + }
  114 +
  115 + .close-action {
  116 + margin-right: -18px;
  117 + }
  118 +
  119 + .mat-fab-action-item {
  120 + width: 100%;
  121 + height: $mobile-toolbar-height;
  122 +
  123 + @media #{$mat-gt-sm} {
  124 + height: 46px;
  125 + }
  126 +
  127 + .tb-dashboard-action-panels {
  128 + height: $mobile-toolbar-height;
  129 +
  130 + @media #{$mat-gt-sm} {
  131 + height: 46px;
  132 + }
  133 +
  134 + .tb-dashboard-action-panel {
  135 + min-width: 0;
  136 + height: $half-mobile-toolbar-height;
  137 +
  138 + @media #{$mat-lt-md} {
  139 + mat-menu{
  140 + margin: 0;
  141 + }
  142 + }
  143 +
  144 + @media #{$mat-gt-sm} {
  145 + height: 46px;
  146 + }
  147 +
  148 + > div {
  149 + height: $half-mobile-toolbar-height;
  150 +
  151 + @media #{$mat-gt-sm} {
  152 + height: 46px;
  153 + }
  154 + }
  155 +
  156 + mat-select {
  157 + margin: 0;
  158 + pointer-events: all;
  159 + }
  160 +
  161 + tb-states-component {
  162 + pointer-events: all;
  163 + }
  164 +
  165 + button.mat-icon-button:not(.tb-mat-32) {
  166 + min-width: 40px;
  167 +
  168 + @media #{$mat-lt-md} {
  169 + min-width: 28px;
  170 + padding: 2px;
  171 + margin: 0;
  172 + .mat-button-wrapper {
  173 + display: block;
  174 + line-height: 24px;
  175 + }
  176 + }
  177 + }
  178 + }
  179 + }
  180 + }
  181 + }
  182 + }
  183 + }
  184 + }
  185 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnDestroy, OnInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +
  20 +@Component({
  21 + selector: 'tb-dashboard-toolbar',
  22 + templateUrl: './dashboard-toolbar.component.html',
  23 + styleUrls: ['./dashboard-toolbar.component.scss'],
  24 + encapsulation: ViewEncapsulation.None
  25 +})
  26 +export class DashboardToolbarComponent implements OnInit {
  27 +
  28 + @Input()
  29 + toolbarOpened: boolean;
  30 +
  31 + @Input()
  32 + forceFullscreen: boolean;
  33 +
  34 + @Output()
  35 + triggerClick = new EventEmitter<void>();
  36 +
  37 + constructor() {
  38 + }
  39 +
  40 + ngOnInit(): void {
  41 + }
  42 +
  43 + onTriggerClick() {
  44 + this.triggerClick.emit();
  45 + }
  46 +
  47 +}
@@ -14,16 +14,18 @@ @@ -14,16 +14,18 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import {NgModule} from '@angular/core';  
18 -import {CommonModule} from '@angular/common';  
19 -import {SharedModule} from '@shared/shared.module';  
20 -import {HomeDialogsModule} from '../../dialogs/home-dialogs.module';  
21 -import {DashboardFormComponent} from '@modules/home/pages/dashboard/dashboard-form.component';  
22 -import {ManageDashboardCustomersDialogComponent} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';  
23 -import {DashboardRoutingModule} from './dashboard-routing.module';  
24 -import {MakeDashboardPublicDialogComponent} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';  
25 -import {HomeComponentsModule} from '@modules/home/components/home-components.module'; 17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
  21 +import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component';
  22 +import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
  23 +import { DashboardRoutingModule } from './dashboard-routing.module';
  24 +import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
  25 +import { HomeComponentsModule } from '@modules/home/components/home-components.module';
26 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; 26 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
  27 +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
  28 +import { DashboardToolbarComponent } from './dashboard-toolbar.component';
27 29
28 @NgModule({ 30 @NgModule({
29 entryComponents: [ 31 entryComponents: [
@@ -36,7 +38,9 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com @@ -36,7 +38,9 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
36 DashboardFormComponent, 38 DashboardFormComponent,
37 DashboardTabsComponent, 39 DashboardTabsComponent,
38 ManageDashboardCustomersDialogComponent, 40 ManageDashboardCustomersDialogComponent,
39 - MakeDashboardPublicDialogComponent 41 + MakeDashboardPublicDialogComponent,
  42 + DashboardToolbarComponent,
  43 + DashboardPageComponent
40 ], 44 ],
41 imports: [ 45 imports: [
42 CommonModule, 46 CommonModule,
@@ -303,9 +303,11 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -303,9 +303,11 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
303 if ($event) { 303 if ($event) {
304 $event.stopPropagation(); 304 $event.stopPropagation();
305 } 305 }
306 - // TODO:  
307 - // this.router.navigateByUrl(`customers/${customer.id.id}/users`);  
308 - this.dialogService.todo(); 306 + if (this.config.componentsData.dashboardScope === 'customer') {
  307 + this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
  308 + } else {
  309 + this.router.navigateByUrl(`dashboards/${dashboard.id.id}`);
  310 + }
309 } 311 }
310 312
311 importDashboard($event: Event) { 313 importDashboard($event: Event) {
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form style="width: 800px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>widget.select-widget-type</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <fieldset [disabled]="(isLoading$ | async)">
  33 + <div fxLayout="column" fxLayoutGap="16px" fxLayout.gt-sm="row" fxLayoutAlign="center center">
  34 + <button *ngFor="let type of allWidgetTypes;" class="tb-card-button" mat-button mat-raised-button color="primary"
  35 + type="button"
  36 + (click)="typeSelected(type)">
  37 + <mat-icon *ngIf="!widgetTypesDataMap.get(type).isMdiIcon; else mdiIconBlock" class="tb-mat-96">
  38 + {{ widgetTypesDataMap.get(type).icon }}
  39 + </mat-icon>
  40 + <ng-template #mdiIconBlock>
  41 + <mat-icon class="tb-mat-96" [svgIcon]="widgetTypesDataMap.get(type).icon">
  42 + </mat-icon>
  43 + </ng-template>
  44 + <span translate>{{ widgetTypesDataMap.get(type).name }}</span>
  45 + </button>
  46 + </div>
  47 + </fieldset>
  48 + </div>
  49 + <div mat-dialog-actions fxLayout="row">
  50 + <span fxFlex></span>
  51 + <button mat-button color="primary"
  52 + style="margin-right: 20px;"
  53 + type="button"
  54 + [disabled]="(isLoading$ | async)"
  55 + (click)="cancel()" cdkFocusInitial>
  56 + {{ 'action.cancel' | translate }}
  57 + </button>
  58 + </div>
  59 +</form>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host ::ng-deep {
  18 + button.tb-card-button {
  19 + width: 100%;
  20 + height: 100%;
  21 + max-width: 240px;
  22 + .mat-button-wrapper {
  23 + width: 100%;
  24 + height: 100%;
  25 + display: flex;
  26 + flex-direction: column;
  27 + align-items: center;
  28 + mat-icon {
  29 + margin: auto;
  30 + }
  31 + span {
  32 + height: 18px;
  33 + min-height: 18px;
  34 + max-height: 18px;
  35 + padding: 0 0 20px 0;
  36 + margin: auto;
  37 + font-size: 18px;
  38 + font-weight: 400;
  39 + line-height: 18px;
  40 + white-space: normal;
  41 + }
  42 + }
  43 + &.mat-raised-button.mat-primary {
  44 + .mat-ripple-element {
  45 + opacity: 0.3;
  46 + background-color: rgba(255, 255, 255, 0.3);
  47 + }
  48 + }
  49 + }
  50 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component } from '@angular/core';
  18 +import { MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { DialogComponent } from '@shared/components/dialog.component';
  22 +import { Router } from '@angular/router';
  23 +import { widgetType, widgetTypesData } from '@shared/models/widget.models';
  24 +
  25 +@Component({
  26 + selector: 'tb-select-widget-type-dialog',
  27 + templateUrl: './select-widget-type-dialog.component.html',
  28 + styleUrls: ['./select-widget-type-dialog.component.scss']
  29 +})
  30 +export class SelectWidgetTypeDialogComponent extends
  31 + DialogComponent<SelectWidgetTypeDialogComponent, widgetType> {
  32 +
  33 + widgetTypes = widgetType;
  34 +
  35 + allWidgetTypes = Object.keys(widgetType);
  36 +
  37 + widgetTypesDataMap = widgetTypesData;
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + protected router: Router,
  41 + public dialogRef: MatDialogRef<SelectWidgetTypeDialogComponent, widgetType>) {
  42 + super(store, router, dialogRef);
  43 + }
  44 +
  45 + cancel(): void {
  46 + this.dialogRef.close(null);
  47 + }
  48 +
  49 + typeSelected(type: widgetType) {
  50 + this.dialogRef.close(type);
  51 + }
  52 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<hotkeys-cheatsheet></hotkeys-cheatsheet>
  19 +<div fxFlex fxLayout="column">
  20 + <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
  21 + <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
  22 + <mat-form-field floatLabel="always" class="tb-widget-title">
  23 + <mat-label></mat-label>
  24 + <input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
  25 + placeholder="{{ 'widget.title' | translate }}"/>
  26 + </mat-form-field>
  27 + <mat-form-field>
  28 + <mat-select [disabled]="isReadOnly" matInput placeholder="{{ 'widget.type' | translate }}"
  29 + [(ngModel)]="widget.type" (ngModelChange)="widetTypeChanged()">
  30 + <mat-option *ngFor="let type of allWidgetTypes" [value]="type">
  31 + {{ widgetTypesDataMap.get(type).name | translate }}
  32 + </mat-option>
  33 + </mat-select>
  34 + </mat-form-field>
  35 + <span fxFlex></span>
  36 + <button mat-button fxHide.xs fxHide.sm [disabled]="!iframeWidgetEditModeInited"
  37 + (click)="applyWidgetScript()"
  38 + matTooltip="{{ 'widget.run' | translate }} (CTRL + Return)"
  39 + matTooltipPosition="below">
  40 + <mat-icon>play_arrow</mat-icon>
  41 + <span translate>action.run</span>
  42 + </button>
  43 + <button mat-button mat-raised-button
  44 + fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || undoDisabled()"
  45 + (click)="undoWidget()"
  46 + matTooltip="{{ 'widget.undo' | translate }} (CTRL + Q)"
  47 + matTooltipPosition="below">
  48 + <mat-icon>undo</mat-icon>
  49 + <span translate>action.undo</span>
  50 + </button>
  51 + <button *ngIf="!isReadOnly" mat-button mat-raised-button
  52 + fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || saveDisabled()"
  53 + (click)="saveWidget()"
  54 + [tb-circular-progress]="saveWidgetPending"
  55 + matTooltip="{{ 'widget.save' | translate }} (CTRL + S)"
  56 + matTooltipPosition="below">
  57 + <mat-icon>save</mat-icon>
  58 + <span translate>action.save</span>
  59 + </button>
  60 + <button mat-button mat-raised-button
  61 + fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || saveAsDisabled()"
  62 + (click)="saveWidgetAs()"
  63 + [tb-circular-progress]="saveWidgetAsPending"
  64 + matTooltip="{{ 'widget.saveAs' | translate }} (Shift + CTRL + S)"
  65 + matTooltipPosition="below">
  66 + <mat-icon>save</mat-icon>
  67 + <span translate>action.saveAs</span>
  68 + </button>
  69 + <button mat-button
  70 + fxHide.xs fxHide.sm
  71 + (click)="fullscreen = !fullscreen"
  72 + matTooltip="{{ 'widget.toggle-fullscreen' | translate }} (Shift + CTRL + F)"
  73 + matTooltipPosition="below">
  74 + <mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  75 + <span translate>widget.toggle-fullscreen</span>
  76 + </button>
  77 + <button mat-button mat-icon-button fxHide.gt-sm
  78 + [matMenuTriggerFor]="widgetEditMenu">
  79 + <mat-icon>more_vert</mat-icon>
  80 + </button>
  81 + <mat-menu #widgetEditMenu="matMenu" xPosition="before">
  82 + <button mat-menu-item
  83 + [disabled]="!iframeWidgetEditModeInited"
  84 + (click)="applyWidgetScript()">
  85 + <mat-icon>play_arrow</mat-icon>
  86 + <span translate>action.run</span>
  87 + </button>
  88 + <button mat-menu-item
  89 + [disabled]="(isLoading$ | async) || undoDisabled()"
  90 + (click)="undoWidget()">
  91 + <mat-icon>undo</mat-icon>
  92 + <span translate>action.undo</span>
  93 + </button>
  94 + <button *ngIf="!isReadOnly" mat-menu-item
  95 + [disabled]="(isLoading$ | async) || saveDisabled()"
  96 + (click)="saveWidget()">
  97 + <mat-icon>save</mat-icon>
  98 + <span translate>action.save</span>
  99 + </button>
  100 + <button mat-menu-item
  101 + [disabled]="(isLoading$ | async) || saveAsDisabled()"
  102 + (click)="saveWidgetAs()">
  103 + <mat-icon>save</mat-icon>
  104 + <span translate>action.saveAs</span>
  105 + </button>
  106 + </mat-menu>
  107 + </mat-toolbar>
  108 + <div fxFlex style="position: relative;">
  109 + <div class="tb-editor tb-absolute-fill">
  110 + <div #topPanel class="tb-split tb-split-vertical">
  111 + <div #topLeftPanel class="tb-split tb-content">
  112 + <mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;">
  113 + <mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;">
  114 + <div class="tb-resize-container" style="background-color: #fff;">
  115 + <div class="mat-padding">
  116 + <div fxFlex fxLayout="row" style="max-height: 40px;"
  117 + fxLayoutAlign="start center"
  118 + *ngFor="let resource of widget.resources; let i = index" >
  119 + <mat-form-field fxFlex class="mat-block resource-field" floatLabel="never"
  120 + style="margin: 10px 0px 0px 0px; max-height: 40px;">
  121 + <input required matInput [(ngModel)]="resource.url"
  122 + (ngModelChange)="isDirty = true"
  123 + placeholder="{{ 'widget.resource-url' | translate }}"/>
  124 + </mat-form-field>
  125 + <button mat-button mat-icon-button color="primary"
  126 + [disabled]="isLoading$ | async"
  127 + (click)="removeResource(i)"
  128 + matTooltip="{{'widget.remove-resource' | translate}}"
  129 + matTooltipPosition="above">
  130 + <mat-icon>close</mat-icon>
  131 + </button>
  132 + </div>
  133 + <div style="margin-top: 6px;">
  134 + <button mat-button mat-raised-button color="primary"
  135 + [disabled]="isLoading$ | async"
  136 + (click)="addResource()"
  137 + matTooltip="{{'widget.add-resource' | translate}}"
  138 + matTooltipPosition="above">
  139 + <span translate>action.add</span>
  140 + </button>
  141 + </div>
  142 + </div>
  143 + </div>
  144 + </mat-tab>
  145 + <mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;">
  146 + <div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen">
  147 + <div class="tb-editor-area-title-panel">
  148 + <button mat-button (click)="beautifyHtml()">
  149 + {{ 'widget.tidy' | translate }}
  150 + </button>
  151 + <button mat-button mat-icon-button class="tb-mat-32"
  152 + (click)="htmlFullscreen = !htmlFullscreen"
  153 + matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  154 + matTooltipPosition="above">
  155 + <mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  156 + </button>
  157 + </div>
  158 + <div #htmlInput></div>
  159 + </div>
  160 + </mat-tab>
  161 + <mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;">
  162 + <div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen">
  163 + <div class="tb-editor-area-title-panel">
  164 + <button mat-button (click)="beautifyCss()">
  165 + {{ 'widget.tidy' | translate }}
  166 + </button>
  167 + <button mat-button mat-icon-button class="tb-mat-32"
  168 + (click)="cssFullscreen = !cssFullscreen"
  169 + matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  170 + matTooltipPosition="above">
  171 + <mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  172 + </button>
  173 + </div>
  174 + <div #cssInput></div>
  175 + </div>
  176 + </mat-tab>
  177 + </mat-tab-group>
  178 + </div>
  179 + <div #topRightPanel class="tb-split tb-content">
  180 + <mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;">
  181 + <mat-tab label="{{ 'widget.settings-schema' | translate }}" style="width: 100%; height: 100%;">
  182 + <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
  183 + <div class="tb-editor-area-title-panel">
  184 + <button mat-button (click)="beautifyJson()">
  185 + {{ 'widget.tidy' | translate }}
  186 + </button>
  187 + <button mat-button mat-icon-button class="tb-mat-32"
  188 + (click)="jsonSettingsFullscreen = !jsonSettingsFullscreen"
  189 + matTooltip="{{(jsonSettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  190 + matTooltipPosition="above">
  191 + <mat-icon>{{ jsonSettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  192 + </button>
  193 + </div>
  194 + <div #settingsJsonInput></div>
  195 + </div>
  196 + </mat-tab>
  197 + <mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}" style="width: 100%; height: 100%;">
  198 + <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen">
  199 + <div class="tb-editor-area-title-panel">
  200 + <button mat-button (click)="beautifyDataKeyJson()">
  201 + {{ 'widget.tidy' | translate }}
  202 + </button>
  203 + <button mat-button mat-icon-button class="tb-mat-32"
  204 + (click)="jsonDataKeySettingsFullscreen = !jsonDataKeySettingsFullscreen"
  205 + matTooltip="{{(jsonDataKeySettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  206 + matTooltipPosition="above">
  207 + <mat-icon>{{ jsonDataKeySettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  208 + </button>
  209 + </div>
  210 + <div #dataKeySettingsJsonInput></div>
  211 + </div>
  212 + </mat-tab>
  213 + </mat-tab-group>
  214 + </div>
  215 + </div>
  216 + <div #bottomPanel class="tb-split tb-split-vertical">
  217 + <div #javascriptPanel class="tb-split tb-content" tb-toast toastTarget="javascriptPanel">
  218 + <div class="tb-resize-container" tb-fullscreen [fullscreen]="javascriptFullscreen">
  219 + <div class="tb-editor-area-title-panel">
  220 + <label translate>widget.javascript</label>
  221 + <button mat-button (click)="beautifyJs()">
  222 + {{ 'widget.tidy' | translate }}
  223 + </button>
  224 + <button mat-button mat-icon-button class="tb-mat-32"
  225 + (click)="javascriptFullscreen = !javascriptFullscreen"
  226 + matTooltip="{{(javascriptFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  227 + matTooltipPosition="above">
  228 + <mat-icon>{{ javascriptFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  229 + </button>
  230 + </div>
  231 + <div #javascriptInput></div>
  232 + </div>
  233 + </div>
  234 + <div #framePanel class="tb-split tb-content" style="overflow-y: hidden; position: relative;">
  235 + <div class="mat-content tb-progress-cover" fxFlex fxLayout="column" fxLayoutAlign="center center"
  236 + *ngIf="!iframeWidgetEditModeInited">
  237 + <mat-spinner diameter="100" mode="indeterminate" color="warn"></mat-spinner>
  238 + </div>
  239 + <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;">
  240 + <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe>
  241 + <button mat-button mat-icon-button
  242 + class="tb-fullscreen-button-style"
  243 + style="position: absolute; top: 10px; left: 10px; bottom: initial;"
  244 + (click)="iFrameFullscreen = !iFrameFullscreen"
  245 + matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  246 + matTooltipPosition="above">
  247 + <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  248 + </button>
  249 + </div>
  250 + </div>
  251 + </div>
  252 + </div>
  253 + </div>
  254 + </div>
  255 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +$edit-toolbar-height: 40px !default;
  18 +
  19 +tb-widget-editor {
  20 + flex: 1;
  21 + display: flex;
  22 + flex-direction: column;
  23 +}
  24 +
  25 +
  26 +.tb-editor {
  27 + .tb-split {
  28 + box-sizing: border-box;
  29 + overflow-x: hidden;
  30 + overflow-y: auto;
  31 + }
  32 +
  33 + mat-form-field.resource-field {
  34 + max-height: 40px;
  35 + margin: 10px 0px 0px 0px;
  36 + .mat-form-field-wrapper {
  37 + padding-bottom: 0;
  38 + .mat-form-field-flex {
  39 + max-height: 40px;
  40 + .mat-form-field-infix {
  41 + border: 0;
  42 + }
  43 + }
  44 + .mat-form-field-underline {
  45 + bottom: 0;
  46 + }
  47 + }
  48 + }
  49 +
  50 + .ace_editor {
  51 + font-size: 14px !important;
  52 + }
  53 +
  54 + .tb-content {
  55 + border: 1px solid #c0c0c0;
  56 + }
  57 +
  58 + .gutter {
  59 + background-color: transparent;
  60 +
  61 + background-repeat: no-repeat;
  62 + background-position: 50%;
  63 +
  64 + &.gutter-horizontal {
  65 + cursor: col-resize;
  66 + background-image: url("../../../../../assets/split.js/grips/vertical.png");
  67 + }
  68 +
  69 + &.gutter-vertical {
  70 + cursor: row-resize;
  71 + background-image: url("../../../../../assets/split.js/grips/horizontal.png");
  72 + }
  73 + }
  74 +
  75 + .tb-split.tb-split-horizontal,
  76 + .gutter.gutter-horizontal {
  77 + float: left;
  78 + height: 100%;
  79 + }
  80 +
  81 + .tb-split.tb-split-vertical {
  82 + display: flex;
  83 +
  84 + .tb-split.tb-content {
  85 + height: 100%;
  86 + }
  87 + }
  88 +}
  89 +
  90 +.tb-split-vertical {
  91 + mat-tab-group {
  92 + .mat-tab-body-wrapper {
  93 + height: calc(100% - 49px);
  94 +
  95 + mat-tab-body {
  96 + height: 100%;
  97 +
  98 + & > div {
  99 + height: 100%;
  100 + }
  101 + }
  102 + }
  103 + }
  104 +}
  105 +
  106 +div.tb-editor-area-title-panel {
  107 + position: absolute;
  108 + top: 5px;
  109 + right: 20px;
  110 + z-index: 5;
  111 + font-size: .8rem;
  112 + font-weight: 500;
  113 +
  114 + label {
  115 + padding: 4px;
  116 + color: #00acc1;
  117 + text-transform: uppercase;
  118 + background: rgba(220, 220, 220, .35);
  119 + border-radius: 5px;
  120 + &:not(:last-child) {
  121 + margin-right: 4px;
  122 + }
  123 + }
  124 +
  125 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  126 + align-items: center;
  127 + vertical-align: middle;
  128 + min-width: 32px;
  129 + min-height: 15px;
  130 + padding: 4px;
  131 + margin: 0;
  132 + font-size: .8rem;
  133 + line-height: 15px;
  134 + color: #7b7b7b;
  135 + background: rgba(220, 220, 220, .35);
  136 + &:not(:last-child) {
  137 + margin-right: 4px;
  138 + }
  139 + }
  140 +}
  141 +
  142 +.tb-resize-container {
  143 + position: relative;
  144 + width: 100%;
  145 + height: 100%;
  146 + overflow-y: auto;
  147 +
  148 + .ace_editor {
  149 + height: 100%;
  150 + }
  151 +}
  152 +
  153 +mat-toolbar.tb-edit-toolbar {
  154 +
  155 + min-height: $edit-toolbar-height !important;
  156 + max-height: $edit-toolbar-height !important;
  157 +
  158 + button.mat-button {
  159 + min-width: 65px;
  160 + min-height: 30px;
  161 + font-size: 12px;
  162 + line-height: 30px;
  163 +
  164 + mat-icon {
  165 + font-size: 20px;
  166 + }
  167 +
  168 + span {
  169 + padding-right: 6px;
  170 + }
  171 + }
  172 +
  173 + mat-form-field {
  174 + input {
  175 + font-size: 1.2rem;
  176 + font-weight: 400;
  177 + letter-spacing: .005em;
  178 + }
  179 + div.mat-form-field-infix {
  180 + padding-bottom: 5px;
  181 + }
  182 + &.tb-widget-title {
  183 + min-width: 250px;
  184 + }
  185 + }
  186 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
  19 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { WidgetService } from '@core/http/widget.service';
  23 +import { WidgetInfo } from '@home/models/widget-component.models';
  24 +import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models';
  25 +import { ActivatedRoute } from '@angular/router';
  26 +import { deepClone } from '@core/utils';
  27 +import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
  28 +import { AuthUser } from '@shared/models/user.model';
  29 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  30 +import { Authority } from '@shared/models/authority.enum';
  31 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  32 +import { Hotkey, HotkeysService } from 'angular2-hotkeys';
  33 +import { TranslateService } from '@ngx-translate/core';
  34 +import { getCurrentIsLoading } from '@app/core/interceptors/load.selectors';
  35 +import * as ace from 'ace-builds';
  36 +import { css_beautify, html_beautify } from 'js-beautify';
  37 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  38 +import { WINDOW } from '@core/services/window.service';
  39 +import { WindowMessage } from '@shared/models/window-message.model';
  40 +import { ExceptionData } from '@shared/models/error.models';
  41 +import Timeout = NodeJS.Timeout;
  42 +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
  43 +
  44 +@Component({
  45 + selector: 'tb-widget-editor',
  46 + templateUrl: './widget-editor.component.html',
  47 + styleUrls: ['./widget-editor.component.scss'],
  48 + encapsulation: ViewEncapsulation.None
  49 +})
  50 +export class WidgetEditorComponent extends PageComponent implements OnInit, OnDestroy, HasDirtyFlag {
  51 +
  52 + @ViewChild('topPanel', {static: true})
  53 + topPanelElmRef: ElementRef;
  54 +
  55 + @ViewChild('topLeftPanel', {static: true})
  56 + topLeftPanelElmRef: ElementRef;
  57 +
  58 + @ViewChild('topRightPanel', {static: true})
  59 + topRightPanelElmRef: ElementRef;
  60 +
  61 + @ViewChild('bottomPanel', {static: true})
  62 + bottomPanelElmRef: ElementRef;
  63 +
  64 + @ViewChild('javascriptPanel', {static: true})
  65 + javascriptPanelElmRef: ElementRef;
  66 +
  67 + @ViewChild('framePanel', {static: true})
  68 + framePanelElmRef: ElementRef;
  69 +
  70 + @ViewChild('htmlInput', {static: true})
  71 + htmlInputElmRef: ElementRef;
  72 +
  73 + @ViewChild('cssInput', {static: true})
  74 + cssInputElmRef: ElementRef;
  75 +
  76 + @ViewChild('settingsJsonInput', {static: true})
  77 + settingsJsonInputElmRef: ElementRef;
  78 +
  79 + @ViewChild('dataKeySettingsJsonInput', {static: true})
  80 + dataKeySettingsJsonInputElmRef: ElementRef;
  81 +
  82 + @ViewChild('javascriptInput', {static: true})
  83 + javascriptInputElmRef: ElementRef;
  84 +
  85 + @ViewChild('widgetIFrame', {static: true})
  86 + widgetIFrameElmRef: ElementRef<HTMLIFrameElement>;
  87 +
  88 + iframe: JQuery<HTMLIFrameElement>;
  89 +
  90 + widgetTypes = widgetType;
  91 + allWidgetTypes = Object.keys(widgetType);
  92 + widgetTypesDataMap = widgetTypesData;
  93 +
  94 + authUser: AuthUser;
  95 +
  96 + isReadOnly: boolean;
  97 +
  98 + widgetsBundle: WidgetsBundle;
  99 + widgetType: WidgetType;
  100 + widget: WidgetInfo;
  101 + origWidget: WidgetInfo;
  102 +
  103 + isDirty = false;
  104 +
  105 + fullscreen = false;
  106 + htmlFullscreen = false;
  107 + cssFullscreen = false;
  108 + jsonSettingsFullscreen = false;
  109 + jsonDataKeySettingsFullscreen = false;
  110 + javascriptFullscreen = false;
  111 + iFrameFullscreen = false;
  112 +
  113 + aceEditors: ace.Ace.Editor[] = [];
  114 + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {};
  115 + htmlEditor: ace.Ace.Editor;
  116 + cssEditor: ace.Ace.Editor;
  117 + jsonSettingsEditor: ace.Ace.Editor;
  118 + dataKeyJsonSettingsEditor: ace.Ace.Editor;
  119 + jsEditor: ace.Ace.Editor;
  120 + aceResizeListeners: { element: any, resizeListener: any }[] = [];
  121 +
  122 + onWindowMessageListener = this.onWindowMessage.bind(this);
  123 +
  124 + iframeWidgetEditModeInited = false;
  125 + saveWidgetPending = false;
  126 + saveWidgetAsPending = false;
  127 +
  128 + gotError = false;
  129 + errorMarkers: number[] = [];
  130 + errorAnnotationId = -1;
  131 +
  132 + saveWidgetTimeout: Timeout;
  133 +
  134 + constructor(protected store: Store<AppState>,
  135 + @Inject(WINDOW) private window: Window,
  136 + private route: ActivatedRoute,
  137 + private widgetService: WidgetService,
  138 + private hotkeysService: HotkeysService,
  139 + private translate: TranslateService,
  140 + private raf: RafService) {
  141 + super(store);
  142 +
  143 + this.authUser = getCurrentAuthUser(store);
  144 +
  145 + this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
  146 + if (this.authUser.authority === Authority.TENANT_ADMIN) {
  147 + this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
  148 + } else {
  149 + this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
  150 + }
  151 + this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType;
  152 + this.widget = this.route.snapshot.data.widgetEditorData.widget;
  153 + if (this.widgetType) {
  154 + const config = JSON.parse(this.widget.defaultConfig);
  155 + this.widget.defaultConfig = JSON.stringify(config);
  156 + }
  157 + this.origWidget = deepClone(this.widget);
  158 + if (!this.widgetType) {
  159 + this.isDirty = true;
  160 + }
  161 + }
  162 +
  163 + ngOnInit(): void {
  164 + this.initHotKeys();
  165 + this.initSplitLayout();
  166 + this.initAceEditors();
  167 + this.iframe = $(this.widgetIFrameElmRef.nativeElement);
  168 + this.window.addEventListener('message', this.onWindowMessageListener);
  169 + this.iframe.attr('data-widget', JSON.stringify(this.widget));
  170 + this.iframe.attr('src', '/widget-editor');
  171 + }
  172 +
  173 + ngOnDestroy(): void {
  174 + this.window.removeEventListener('message', this.onWindowMessageListener);
  175 + this.aceResizeListeners.forEach((resizeListener) => {
  176 + // @ts-ignore
  177 + removeResizeListener(resizeListener.element, resizeListener.resizeListener);
  178 + });
  179 + }
  180 +
  181 + private initHotKeys(): void {
  182 + this.hotkeysService.add(
  183 + new Hotkey('ctrl+q', (event: KeyboardEvent) => {
  184 + if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) {
  185 + event.preventDefault();
  186 + this.undoWidget();
  187 + }
  188 + return false;
  189 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  190 + this.translate.instant('widget.undo'))
  191 + );
  192 + this.hotkeysService.add(
  193 + new Hotkey('ctrl+s', (event: KeyboardEvent) => {
  194 + if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) {
  195 + event.preventDefault();
  196 + this.saveWidget();
  197 + }
  198 + return false;
  199 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  200 + this.translate.instant('widget.save'))
  201 + );
  202 + this.hotkeysService.add(
  203 + new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => {
  204 + if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) {
  205 + event.preventDefault();
  206 + this.saveWidgetAs();
  207 + }
  208 + return false;
  209 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  210 + this.translate.instant('widget.saveAs'))
  211 + );
  212 + this.hotkeysService.add(
  213 + new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
  214 + event.preventDefault();
  215 + this.fullscreen = !this.fullscreen;
  216 + return false;
  217 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  218 + this.translate.instant('widget.toggle-fullscreen'))
  219 + );
  220 + this.hotkeysService.add(
  221 + new Hotkey('ctrl+enter', (event: KeyboardEvent) => {
  222 + event.preventDefault();
  223 + this.applyWidgetScript();
  224 + return false;
  225 + }, ['INPUT', 'SELECT', 'TEXTAREA'],
  226 + this.translate.instant('widget.run'))
  227 + );
  228 + }
  229 +
  230 + private initSplitLayout() {
  231 + Split([this.topPanelElmRef.nativeElement, this.bottomPanelElmRef.nativeElement], {
  232 + sizes: [35, 65],
  233 + gutterSize: 8,
  234 + cursor: 'row-resize',
  235 + direction: 'vertical'
  236 + });
  237 + Split([this.topLeftPanelElmRef.nativeElement, this.topRightPanelElmRef.nativeElement], {
  238 + sizes: [50, 50],
  239 + gutterSize: 8,
  240 + cursor: 'col-resize'
  241 + });
  242 + Split([this.javascriptPanelElmRef.nativeElement, this.framePanelElmRef.nativeElement], {
  243 + sizes: [50, 50],
  244 + gutterSize: 8,
  245 + cursor: 'col-resize'
  246 + });
  247 + }
  248 +
  249 + private initAceEditors() {
  250 + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html');
  251 + this.htmlEditor.on('input', () => {
  252 + const editorValue = this.htmlEditor.getValue();
  253 + if (this.widget.templateHtml !== editorValue) {
  254 + this.widget.templateHtml = editorValue;
  255 + this.isDirty = true;
  256 + }
  257 + });
  258 + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css');
  259 + this.cssEditor.on('input', () => {
  260 + const editorValue = this.cssEditor.getValue();
  261 + if (this.widget.templateCss !== editorValue) {
  262 + this.widget.templateCss = editorValue;
  263 + this.isDirty = true;
  264 + }
  265 + });
  266 + this.jsonSettingsEditor = this.createAceEditor(this.settingsJsonInputElmRef, 'json');
  267 + this.jsonSettingsEditor.on('input', () => {
  268 + const editorValue = this.jsonSettingsEditor.getValue();
  269 + if (this.widget.settingsSchema !== editorValue) {
  270 + this.widget.settingsSchema = editorValue;
  271 + this.isDirty = true;
  272 + }
  273 + });
  274 + this.dataKeyJsonSettingsEditor = this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json');
  275 + this.dataKeyJsonSettingsEditor.on('input', () => {
  276 + const editorValue = this.dataKeyJsonSettingsEditor.getValue();
  277 + if (this.widget.dataKeySettingsSchema !== editorValue) {
  278 + this.widget.dataKeySettingsSchema = editorValue;
  279 + this.isDirty = true;
  280 + }
  281 + });
  282 + this.jsEditor = this.createAceEditor(this.javascriptInputElmRef, 'javascript');
  283 + this.jsEditor.on('input', () => {
  284 + const editorValue = this.jsEditor.getValue();
  285 + if (this.widget.controllerScript !== editorValue) {
  286 + this.widget.controllerScript = editorValue;
  287 + this.isDirty = true;
  288 + }
  289 + });
  290 + this.jsEditor.on('change', () => {
  291 + this.cleanupJsErrors();
  292 + });
  293 + this.setAceEditorValues();
  294 + }
  295 +
  296 + private setAceEditorValues() {
  297 + this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1);
  298 + this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
  299 + this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
  300 + this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
  301 + this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1);
  302 + }
  303 +
  304 + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor {
  305 + const editorElement = editorElementRef.nativeElement;
  306 + let editorOptions: Partial<ace.Ace.EditorOptions> = {
  307 + mode: `ace/mode/${mode}`,
  308 + showGutter: true,
  309 + showPrintMargin: true
  310 + };
  311 + const advancedOptions = {
  312 + enableSnippets: true,
  313 + enableBasicAutocompletion: true,
  314 + enableLiveAutocompletion: true
  315 + };
  316 + editorOptions = {...editorOptions, ...advancedOptions};
  317 + const aceEditor = ace.edit(editorElement, editorOptions);
  318 + aceEditor.session.setUseWrapMode(true);
  319 + this.aceEditors.push(aceEditor);
  320 +
  321 + const resizeListener = this.onAceEditorResize.bind(this, aceEditor);
  322 +
  323 + // @ts-ignore
  324 + addResizeListener(editorElement, resizeListener);
  325 + this.aceResizeListeners.push({element: editorElement, resizeListener});
  326 + return aceEditor;
  327 + }
  328 +
  329 + private onAceEditorResize(aceEditor: ace.Ace.Editor) {
  330 + if (this.editorsResizeCafs[aceEditor.id]) {
  331 + this.editorsResizeCafs[aceEditor.id]();
  332 + delete this.editorsResizeCafs[aceEditor.id];
  333 + }
  334 + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => {
  335 + aceEditor.resize();
  336 + aceEditor.renderer.updateFull();
  337 + });
  338 + }
  339 +
  340 + private onWindowMessage(event: MessageEvent) {
  341 + let message: WindowMessage;
  342 + if (event.data) {
  343 + try {
  344 + message = JSON.parse(event.data);
  345 + } catch (e) {}
  346 + }
  347 + if (message) {
  348 + switch (message.type) {
  349 + case 'widgetException':
  350 + this.onWidgetException(message.data);
  351 + break;
  352 + case 'widgetEditModeInited':
  353 + this.onWidgetEditModeInited();
  354 + break;
  355 + case 'widgetEditUpdated':
  356 + this.onWidgetEditUpdated(message.data);
  357 + break;
  358 + }
  359 + }
  360 + }
  361 +
  362 + private onWidgetEditModeInited() {
  363 + this.iframeWidgetEditModeInited = true;
  364 + if (this.saveWidgetPending || this.saveWidgetAsPending) {
  365 + if (!this.saveWidgetTimeout) {
  366 + this.saveWidgetTimeout = setTimeout(() => {
  367 + if (!this.gotError) {
  368 + if (this.saveWidgetPending) {
  369 + this.commitSaveWidget();
  370 + } else if (this.saveWidgetAsPending) {
  371 + this.commitSaveWidgetAs();
  372 + }
  373 + } else {
  374 + this.store.dispatch(new ActionNotificationShow(
  375 + {message: this.translate.instant('widget.unable-to-save-widget-error'), type: 'error'}));
  376 + this.saveWidgetPending = false;
  377 + this.saveWidgetAsPending = false;
  378 + }
  379 + this.saveWidgetTimeout = undefined;
  380 + }, 1500);
  381 + }
  382 + }
  383 + }
  384 +
  385 + private onWidgetEditUpdated(widget: Widget) {
  386 + this.widget.sizeX = widget.sizeX / 2;
  387 + this.widget.sizeY = widget.sizeY / 2;
  388 + this.widget.defaultConfig = JSON.stringify(widget.config);
  389 + this.iframe.attr('data-widget', JSON.stringify(this.widget));
  390 + this.isDirty = true;
  391 + }
  392 +
  393 + private onWidgetException(details: ExceptionData) {
  394 + if (!this.gotError) {
  395 + this.gotError = true;
  396 + let errorInfo = 'Error:';
  397 + if (details.name) {
  398 + errorInfo += ' ' + details.name + ':';
  399 + }
  400 + if (details.message) {
  401 + errorInfo += ' ' + details.message;
  402 + }
  403 + if (details.lineNumber) {
  404 + errorInfo += '<br>Line ' + details.lineNumber;
  405 + if (details.columnNumber) {
  406 + errorInfo += ' column ' + details.columnNumber;
  407 + }
  408 + errorInfo += ' of script.';
  409 + }
  410 + if (!this.saveWidgetPending && !this.saveWidgetAsPending) {
  411 + this.store.dispatch(new ActionNotificationShow(
  412 + {message: errorInfo, type: 'error', target: 'javascriptPanel'}));
  413 + }
  414 + if (details.lineNumber) {
  415 + const line = details.lineNumber - 1;
  416 + let column = 0;
  417 + if (details.columnNumber) {
  418 + column = details.columnNumber;
  419 + }
  420 + const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity),
  421 + 'ace_active-line', 'screenLine');
  422 + this.errorMarkers.push(errorMarkerId);
  423 + const annotations = this.jsEditor.session.getAnnotations();
  424 + const errorAnnotation: ace.Ace.Annotation = {
  425 + row: line,
  426 + column,
  427 + text: details.message,
  428 + type: 'error'
  429 + };
  430 + this.errorAnnotationId = annotations.push(errorAnnotation) - 1;
  431 + this.jsEditor.session.setAnnotations(annotations);
  432 + }
  433 + }
  434 + }
  435 +
  436 + private cleanupJsErrors() {
  437 + this.store.dispatch(new ActionNotificationHide({}));
  438 + this.errorMarkers.forEach((errorMarker) => {
  439 + this.jsEditor.session.removeMarker(errorMarker);
  440 + });
  441 + this.errorMarkers.length = 0;
  442 + if (this.errorAnnotationId > -1) {
  443 + const annotations = this.jsEditor.session.getAnnotations();
  444 + annotations.splice(this.errorAnnotationId, 1);
  445 + this.jsEditor.session.setAnnotations(annotations);
  446 + this.errorAnnotationId = -1;
  447 + }
  448 + }
  449 +
  450 + private commitSaveWidget() {
  451 + // TODO:
  452 + this.saveWidgetPending = false;
  453 + }
  454 +
  455 + private commitSaveWidgetAs() {
  456 + // TODO:
  457 + this.saveWidgetAsPending = false;
  458 + }
  459 +
  460 + applyWidgetScript(): void {
  461 + this.cleanupJsErrors();
  462 + this.gotError = false;
  463 + this.iframeWidgetEditModeInited = false;
  464 + const config: WidgetConfig = JSON.parse(this.widget.defaultConfig);
  465 + config.title = this.widget.widgetName;
  466 + this.widget.defaultConfig = JSON.stringify(config);
  467 + this.iframe.attr('data-widget', JSON.stringify(this.widget));
  468 + this.iframe[0].contentWindow.location.reload(true);
  469 + }
  470 +
  471 + undoWidget(): void {
  472 + this.widget = deepClone(this.origWidget);
  473 + this.setAceEditorValues();
  474 + this.isDirty = false;
  475 + this.applyWidgetScript();
  476 + }
  477 +
  478 + saveWidget(): void {
  479 + if (!this.widget.widgetName) {
  480 + this.store.dispatch(new ActionNotificationShow(
  481 + {message: this.translate.instant('widget.missing-widget-title-error'), type: 'error'}));
  482 + } else {
  483 + this.saveWidgetPending = true;
  484 + this.applyWidgetScript();
  485 + }
  486 + }
  487 +
  488 + saveWidgetAs(): void {
  489 + this.saveWidgetAsPending = true;
  490 + this.applyWidgetScript();
  491 + }
  492 +
  493 + undoDisabled(): boolean {
  494 + return !this.isDirty
  495 + || !this.iframeWidgetEditModeInited
  496 + || this.saveWidgetPending
  497 + || this.saveWidgetAsPending;
  498 + }
  499 +
  500 + saveDisabled(): boolean {
  501 + return this.isReadOnly
  502 + || !this.isDirty
  503 + || !this.iframeWidgetEditModeInited
  504 + || this.saveWidgetPending
  505 + || this.saveWidgetAsPending;
  506 + }
  507 +
  508 + saveAsDisabled(): boolean {
  509 + return !this.iframeWidgetEditModeInited
  510 + || this.saveWidgetPending
  511 + || this.saveWidgetAsPending;
  512 + }
  513 +
  514 + beautifyCss(): void {
  515 + const res = css_beautify(this.widget.templateCss, {indent_size: 4});
  516 + if (this.widget.templateCss !== res) {
  517 + this.isDirty = true;
  518 + this.widget.templateCss = res;
  519 + this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
  520 + }
  521 + }
  522 +
  523 + beautifyHtml(): void {
  524 + const res = html_beautify(this.widget.templateHtml, {indent_size: 4, wrap_line_length: 60});
  525 + if (this.widget.templateHtml !== res) {
  526 + this.isDirty = true;
  527 + this.widget.templateHtml = res;
  528 + this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1);
  529 + }
  530 + }
  531 +
  532 + beautifyJson(): void {
  533 + const res = js_beautify(this.widget.settingsSchema, {indent_size: 4});
  534 + if (this.widget.settingsSchema !== res) {
  535 + this.isDirty = true;
  536 + this.widget.settingsSchema = res;
  537 + this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
  538 + }
  539 + }
  540 +
  541 + beautifyDataKeyJson(): void {
  542 + const res = js_beautify(this.widget.dataKeySettingsSchema, {indent_size: 4});
  543 + if (this.widget.dataKeySettingsSchema !== res) {
  544 + this.isDirty = true;
  545 + this.widget.dataKeySettingsSchema = res;
  546 + this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
  547 + }
  548 + }
  549 +
  550 + beautifyJs(): void {
  551 + const res = js_beautify(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60});
  552 + if (this.widget.controllerScript !== res) {
  553 + this.isDirty = true;
  554 + this.widget.controllerScript = res;
  555 + this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1);
  556 + }
  557 + }
  558 +
  559 + removeResource(index: number) {
  560 + if (index > -1) {
  561 + if (this.widget.resources.splice(index, 1).length > 0) {
  562 + this.isDirty = true;
  563 + }
  564 + }
  565 + }
  566 +
  567 + addResource() {
  568 + this.widget.resources.push({url: ''});
  569 + this.isDirty = true;
  570 + }
  571 +
  572 + widetTypeChanged() {
  573 + const config: WidgetConfig = JSON.parse(this.widget.defaultConfig);
  574 + if (this.widget.type !== widgetType.rpc &&
  575 + this.widget.type !== widgetType.alarm) {
  576 + if (config.targetDeviceAliases) {
  577 + delete config.targetDeviceAliases;
  578 + }
  579 + if (config.alarmSource) {
  580 + delete config.alarmSource;
  581 + }
  582 + if (!config.datasources) {
  583 + config.datasources = [];
  584 + }
  585 + if (!config.timewindow) {
  586 + config.timewindow = {
  587 + realtime: {
  588 + timewindowMs: 60000
  589 + }
  590 + };
  591 + }
  592 + } else if (this.widget.type === widgetType.rpc) {
  593 + if (config.datasources) {
  594 + delete config.datasources;
  595 + }
  596 + if (config.alarmSource) {
  597 + delete config.alarmSource;
  598 + }
  599 + if (config.timewindow) {
  600 + delete config.timewindow;
  601 + }
  602 + if (!config.targetDeviceAliases) {
  603 + config.targetDeviceAliases = [];
  604 + }
  605 + } else { // alarm
  606 + if (config.datasources) {
  607 + delete config.datasources;
  608 + }
  609 + if (config.targetDeviceAliases) {
  610 + delete config.targetDeviceAliases;
  611 + }
  612 + if (!config.alarmSource) {
  613 + config.alarmSource = {};
  614 + }
  615 + if (!config.timewindow) {
  616 + config.timewindow = {
  617 + realtime: {
  618 + timewindowMs: 24 * 60 * 60 * 1000
  619 + }
  620 + };
  621 + }
  622 + }
  623 + this.widget.defaultConfig = JSON.stringify(config);
  624 + this.isDirty = true;
  625 + }
  626 +}
@@ -17,20 +17,24 @@ @@ -17,20 +17,24 @@
17 import { Injectable, NgModule } from '@angular/core'; 17 import { Injectable, NgModule } from '@angular/core';
18 import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; 18 import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
19 19
20 -import {EntitiesTableComponent} from '../../components/entity/entities-table.component';  
21 -import {Authority} from '@shared/models/authority.enum';  
22 -import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';  
23 -import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; 20 +import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
  21 +import { Authority } from '@shared/models/authority.enum';
  22 +import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
24 import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; 23 import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
25 import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; 24 import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
26 -import { User } from '@shared/models/user.model';  
27 -import { Store } from '@ngrx/store';  
28 -import { AppState } from '@core/core.state';  
29 -import { UserService } from '@core/http/user.service';  
30 import { Observable } from 'rxjs'; 25 import { Observable } from 'rxjs';
31 -import { getCurrentAuthUser } from '@core/auth/auth.selectors';  
32 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 26 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
33 import { WidgetService } from '@core/http/widget.service'; 27 import { WidgetService } from '@core/http/widget.service';
  28 +import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
  29 +import { map } from 'rxjs/operators';
  30 +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
  31 +import { widgetType, WidgetType } from '@app/shared/models/widget.models';
  32 +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
  33 +
  34 +export interface WidgetEditorData {
  35 + widgetType: WidgetType;
  36 + widget: WidgetInfo;
  37 +}
34 38
35 @Injectable() 39 @Injectable()
36 export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { 40 export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
@@ -39,13 +43,61 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { @@ -39,13 +43,61 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
39 } 43 }
40 44
41 resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> { 45 resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
42 - const widgetsBundleId = route.params.widgetsBundleId; 46 + let widgetsBundleId = route.params.widgetsBundleId;
  47 + if (!widgetsBundleId) {
  48 + widgetsBundleId = route.parent.params.widgetsBundleId;
  49 + }
43 return this.widgetsService.getWidgetsBundle(widgetsBundleId); 50 return this.widgetsService.getWidgetsBundle(widgetsBundleId);
44 } 51 }
45 } 52 }
46 53
  54 +@Injectable()
  55 +export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
  56 +
  57 + constructor(private widgetsService: WidgetService) {
  58 + }
  59 +
  60 + resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
  61 + const widgetTypeId = route.params.widgetTypeId;
  62 + return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe(
  63 + map((result) => {
  64 + return {
  65 + widgetType: result,
  66 + widget: toWidgetInfo(result)
  67 + };
  68 + })
  69 + );
  70 + }
  71 +}
  72 +
  73 +@Injectable()
  74 +export class WidgetEditorAddDataResolver implements Resolve<WidgetEditorData> {
  75 +
  76 + constructor(private widgetsService: WidgetService) {
  77 + }
  78 +
  79 + resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
  80 + let widgetTypeParam = route.params.widgetType as widgetType;
  81 + if (!widgetTypeParam) {
  82 + widgetTypeParam = widgetType.timeseries;
  83 + }
  84 + return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe(
  85 + map((widget) => {
  86 + widget.widgetName = null;
  87 + return {
  88 + widgetType: null,
  89 + widget
  90 + };
  91 + })
  92 + );
  93 + }
  94 +}
  95 +
47 export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title); 96 export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title);
48 97
  98 +export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction =
  99 + ((route, translate, component) => component ? (component as WidgetEditorComponent).widget.widgetName : '');
  100 +
49 export const routes: Routes = [ 101 export const routes: Routes = [
50 { 102 {
51 path: 'widgets-bundles', 103 path: 'widgets-bundles',
@@ -69,10 +121,7 @@ export const routes: Routes = [ @@ -69,10 +121,7 @@ export const routes: Routes = [
69 }, 121 },
70 { 122 {
71 path: ':widgetsBundleId/widgetTypes', 123 path: ':widgetsBundleId/widgetTypes',
72 - component: WidgetLibraryComponent,  
73 data: { 124 data: {
74 - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],  
75 - title: 'widget.widget-library',  
76 breadcrumb: { 125 breadcrumb: {
77 labelFunction: widgetTypesBreadcumbLabelFunction, 126 labelFunction: widgetTypesBreadcumbLabelFunction,
78 icon: 'now_widgets' 127 icon: 'now_widgets'
@@ -80,7 +129,49 @@ export const routes: Routes = [ @@ -80,7 +129,49 @@ export const routes: Routes = [
80 }, 129 },
81 resolve: { 130 resolve: {
82 widgetsBundle: WidgetsBundleResolver 131 widgetsBundle: WidgetsBundleResolver
83 - } 132 + },
  133 + children: [
  134 + {
  135 + path: '',
  136 + component: WidgetLibraryComponent,
  137 + data: {
  138 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  139 + title: 'widget.widget-library'
  140 + }
  141 + },
  142 + {
  143 + path: ':widgetTypeId',
  144 + component: WidgetEditorComponent,
  145 + canDeactivate: [ConfirmOnExitGuard],
  146 + data: {
  147 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  148 + title: 'widget.editor',
  149 + breadcrumb: {
  150 + labelFunction: widgetEditorBreadcumbLabelFunction,
  151 + icon: 'insert_chart'
  152 + } as BreadCrumbConfig
  153 + },
  154 + resolve: {
  155 + widgetEditorData: WidgetEditorDataResolver
  156 + }
  157 + },
  158 + {
  159 + path: 'add/:widgetType',
  160 + component: WidgetEditorComponent,
  161 + canDeactivate: [ConfirmOnExitGuard],
  162 + data: {
  163 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
  164 + title: 'widget.editor',
  165 + breadcrumb: {
  166 + labelFunction: widgetEditorBreadcumbLabelFunction,
  167 + icon: 'insert_chart'
  168 + } as BreadCrumbConfig
  169 + },
  170 + resolve: {
  171 + widgetEditorData: WidgetEditorAddDataResolver
  172 + }
  173 + }
  174 + ]
84 } 175 }
85 ] 176 ]
86 } 177 }
@@ -91,7 +182,9 @@ export const routes: Routes = [ @@ -91,7 +182,9 @@ export const routes: Routes = [
91 exports: [RouterModule], 182 exports: [RouterModule],
92 providers: [ 183 providers: [
93 WidgetsBundlesTableConfigResolver, 184 WidgetsBundlesTableConfigResolver,
94 - WidgetsBundleResolver 185 + WidgetsBundleResolver,
  186 + WidgetEditorDataResolver,
  187 + WidgetEditorAddDataResolver
95 ] 188 ]
96 }) 189 })
97 export class WidgetLibraryRoutingModule { } 190 export class WidgetLibraryRoutingModule { }
@@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component'; @@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component';
21 import { AuthUser } from '@shared/models/user.model'; 21 import { AuthUser } from '@shared/models/user.model';
22 import { getCurrentAuthUser } from '@core/auth/auth.selectors'; 22 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 -import { ActivatedRoute } from '@angular/router'; 24 +import { ActivatedRoute, Router } from '@angular/router';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 import { NULL_UUID } from '@shared/models/id/has-uuid'; 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 import { Observable } from 'rxjs'; 27 import { Observable } from 'rxjs';
@@ -34,6 +34,13 @@ import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-componen @@ -34,6 +34,13 @@ import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-componen
34 import { IAliasController } from '@app/core/api/widget-api.models'; 34 import { IAliasController } from '@app/core/api/widget-api.models';
35 import { toWidgetInfo } from '@home/models/widget-component.models'; 35 import { toWidgetInfo } from '@home/models/widget-component.models';
36 import { DummyAliasController } from '@core/api/alias-controller'; 36 import { DummyAliasController } from '@core/api/alias-controller';
  37 +import {
  38 + DeviceCredentialsDialogComponent,
  39 + DeviceCredentialsDialogData
  40 +} from '@home/pages/device/device-credentials-dialog.component';
  41 +import { DeviceCredentials } from '@shared/models/device.models';
  42 +import { MatDialog } from '@angular/material/dialog';
  43 +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
37 44
38 @Component({ 45 @Component({
39 selector: 'tb-widget-library', 46 selector: 'tb-widget-library',
@@ -83,8 +90,10 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -83,8 +90,10 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
83 90
84 constructor(protected store: Store<AppState>, 91 constructor(protected store: Store<AppState>,
85 private route: ActivatedRoute, 92 private route: ActivatedRoute,
  93 + private router: Router,
86 private widgetService: WidgetService, 94 private widgetService: WidgetService,
87 - private dialogService: DialogService) { 95 + private dialogService: DialogService,
  96 + private dialog: MatDialog) {
88 super(store); 97 super(store);
89 98
90 this.authUser = getCurrentAuthUser(store); 99 this.authUser = getCurrentAuthUser(store);
@@ -163,33 +172,43 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -163,33 +172,43 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
163 } 172 }
164 173
165 importWidgetType($event: Event): void { 174 importWidgetType($event: Event): void {
166 - if (event) {  
167 - event.stopPropagation(); 175 + if ($event) {
  176 + $event.stopPropagation();
168 } 177 }
169 this.dialogService.todo(); 178 this.dialogService.todo();
170 } 179 }
171 180
172 openWidgetType($event: Event, widget?: Widget): void { 181 openWidgetType($event: Event, widget?: Widget): void {
173 - if (event) {  
174 - event.stopPropagation(); 182 + if ($event) {
  183 + $event.stopPropagation();
175 } 184 }
176 if (widget) { 185 if (widget) {
177 - this.dialogService.todo(); 186 + this.router.navigate([widget.typeId.id], {relativeTo: this.route});
178 } else { 187 } else {
179 - this.dialogService.todo(); 188 + this.dialog.open<SelectWidgetTypeDialogComponent, any,
  189 + widgetType>(SelectWidgetTypeDialogComponent, {
  190 + disableClose: true,
  191 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
  192 + }).afterClosed().subscribe(
  193 + (type) => {
  194 + if (type) {
  195 + this.router.navigate(['add', type], {relativeTo: this.route});
  196 + }
  197 + }
  198 + );
180 } 199 }
181 } 200 }
182 201
183 exportWidgetType($event: Event, widget: Widget): void { 202 exportWidgetType($event: Event, widget: Widget): void {
184 - if (event) {  
185 - event.stopPropagation(); 203 + if ($event) {
  204 + $event.stopPropagation();
186 } 205 }
187 this.dialogService.todo(); 206 this.dialogService.todo();
188 } 207 }
189 208
190 removeWidgetType($event: Event, widget: Widget): void { 209 removeWidgetType($event: Event, widget: Widget): void {
191 - if (event) {  
192 - event.stopPropagation(); 210 + if ($event) {
  211 + $event.stopPropagation();
193 } 212 }
194 this.dialogService.todo(); 213 this.dialogService.todo();
195 } 214 }
@@ -21,14 +21,19 @@ import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle. @@ -21,14 +21,19 @@ import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.
21 import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module'; 21 import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
22 import {HomeComponentsModule} from '@modules/home/components/home-components.module'; 22 import {HomeComponentsModule} from '@modules/home/components/home-components.module';
23 import { WidgetLibraryComponent } from './widget-library.component'; 23 import { WidgetLibraryComponent } from './widget-library.component';
  24 +import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
  25 +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
24 26
25 @NgModule({ 27 @NgModule({
26 entryComponents: [ 28 entryComponents: [
27 - WidgetsBundleComponent 29 + WidgetsBundleComponent,
  30 + SelectWidgetTypeDialogComponent
28 ], 31 ],
29 declarations: [ 32 declarations: [
30 WidgetsBundleComponent, 33 WidgetsBundleComponent,
31 - WidgetLibraryComponent 34 + WidgetLibraryComponent,
  35 + WidgetEditorComponent,
  36 + SelectWidgetTypeDialogComponent
32 ], 37 ],
33 imports: [ 38 imports: [
34 CommonModule, 39 CommonModule,
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 --> 17 -->
18 <div fxFlex class="tb-breadcrumb" fxLayout="row"> 18 <div fxFlex class="tb-breadcrumb" fxLayout="row">
19 <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb"> 19 <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
20 - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} 20 + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
21 </h1> 21 </h1>
22 <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast"> 22 <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast">
23 <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams"> 23 <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> 26 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
27 {{ breadcrumb.icon }} 27 {{ breadcrumb.icon }}
28 </mat-icon> 28 </mat-icon>
29 - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} 29 + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
30 </a> 30 </a>
31 <span *ngSwitchCase="true"> 31 <span *ngSwitchCase="true">
32 <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> 32 <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> 34 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
35 {{ breadcrumb.icon }} 35 {{ breadcrumb.icon }}
36 </mat-icon> 36 </mat-icon>
37 - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} 37 + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
38 </span> 38 </span>
39 <span class="divider" [fxHide]="isLast"> > </span> 39 <span class="divider" [fxHide]="isLast"> > </span>
40 </span> 40 </span>
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnDestroy, OnInit } from '@angular/core'; 17 +import { Component, Input, OnDestroy, OnInit } from '@angular/core';
18 import { BehaviorSubject, Subject } from 'rxjs'; 18 import { BehaviorSubject, Subject } from 'rxjs';
19 import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; 19 import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
20 import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; 20 import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
@@ -28,6 +28,13 @@ import { TranslateService } from '@ngx-translate/core'; @@ -28,6 +28,13 @@ import { TranslateService } from '@ngx-translate/core';
28 }) 28 })
29 export class BreadcrumbComponent implements OnInit, OnDestroy { 29 export class BreadcrumbComponent implements OnInit, OnDestroy {
30 30
  31 + activeComponentValue: any;
  32 +
  33 + @Input()
  34 + set activeComponent(activeComponent: any) {
  35 + this.activeComponentValue = activeComponent;
  36 + }
  37 +
31 breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); 38 breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot));
32 39
33 routerEventsSubscription = this.router.events.pipe( 40 routerEventsSubscription = this.router.events.pipe(
@@ -61,9 +68,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { @@ -61,9 +68,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
61 const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; 68 const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig;
62 if (breadcrumbConfig && !breadcrumbConfig.skip) { 69 if (breadcrumbConfig && !breadcrumbConfig.skip) {
63 let label; 70 let label;
  71 + let labelFunction;
64 let ignoreTranslate; 72 let ignoreTranslate;
65 if (breadcrumbConfig.labelFunction) { 73 if (breadcrumbConfig.labelFunction) {
66 - label = breadcrumbConfig.labelFunction(route, this.translate); 74 + labelFunction = () => {
  75 + return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue);
  76 + };
67 ignoreTranslate = true; 77 ignoreTranslate = true;
68 } else { 78 } else {
69 label = breadcrumbConfig.label || 'home.home'; 79 label = breadcrumbConfig.label || 'home.home';
@@ -71,10 +81,11 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { @@ -71,10 +81,11 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
71 } 81 }
72 const icon = breadcrumbConfig.icon || 'home'; 82 const icon = breadcrumbConfig.icon || 'home';
73 const isMdiIcon = icon.startsWith('mdi:'); 83 const isMdiIcon = icon.startsWith('mdi:');
74 - const link = [ '/' + route.url.join('') ]; 84 + const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ];
75 const queryParams = route.queryParams; 85 const queryParams = route.queryParams;
76 const breadcrumb = { 86 const breadcrumb = {
77 label, 87 label,
  88 + labelFunction,
78 ignoreTranslate, 89 ignoreTranslate,
79 icon, 90 icon,
80 isMdiIcon, 91 isMdiIcon,
@@ -89,5 +100,4 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { @@ -89,5 +100,4 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
89 } 100 }
90 return newBreadcrumbs; 101 return newBreadcrumbs;
91 } 102 }
92 -  
93 } 103 }
@@ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core'; @@ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core';
19 19
20 export interface BreadCrumb { 20 export interface BreadCrumb {
21 label: string; 21 label: string;
  22 + labelFunction?: () => string;
22 ignoreTranslate: boolean; 23 ignoreTranslate: boolean;
23 icon: string; 24 icon: string;
24 isMdiIcon: boolean; 25 isMdiIcon: boolean;
@@ -26,7 +27,7 @@ export interface BreadCrumb { @@ -26,7 +27,7 @@ export interface BreadCrumb {
26 queryParams: Params; 27 queryParams: Params;
27 } 28 }
28 29
29 -export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService) => string; 30 +export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService, component: any) => string;
30 31
31 export interface BreadCrumbConfig { 32 export interface BreadCrumbConfig {
32 labelFunction: BreadCrumbLabelFunction; 33 labelFunction: BreadCrumbLabelFunction;
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Directive, ElementRef, ViewContainerRef, ComponentFactoryResolver, ComponentRef, Input } from '@angular/core';
  18 +import { Overlay } from '@angular/cdk/overlay';
  19 +import { MatProgressBar, MatSpinner } from '@angular/material';
  20 +
  21 +@Directive({
  22 + selector: '[tb-circular-progress]'
  23 +})
  24 +export class CircularProgressDirective {
  25 +
  26 + showProgressValue = false;
  27 +
  28 + children: JQuery<any>;
  29 +
  30 + cssWidth: any;
  31 +
  32 + @Input('tb-circular-progress')
  33 + set showProgress(showProgress: boolean) {
  34 + if (this.showProgressValue !== showProgress) {
  35 + const element = this.elementRef.nativeElement;
  36 + this.showProgressValue = showProgress;
  37 + this.spinnerRef.instance._elementRef.nativeElement.style.display = showProgress ? 'block' : 'none';
  38 + if (showProgress) {
  39 + this.cssWidth = $(element).prop('style').width;
  40 + if (!this.cssWidth) {
  41 + $(element).css('width', '');
  42 + const width = $(element).prop('offsetWidth');
  43 + $(element).css('width', width + 'px');
  44 + }
  45 + this.children = $(element).children();
  46 + $(element).empty();
  47 + $(element).append($(this.spinnerRef.instance._elementRef.nativeElement));
  48 + } else {
  49 + $(element).empty();
  50 + $(element).append(this.children);
  51 + if (this.cssWidth) {
  52 + $(element).css('width', this.cssWidth);
  53 + } else {
  54 + $(element).css('width', '');
  55 + }
  56 + }
  57 + }
  58 + }
  59 +
  60 + spinnerRef: ComponentRef<MatSpinner>;
  61 +
  62 + constructor(private elementRef: ElementRef,
  63 + private componentFactoryResolver: ComponentFactoryResolver,
  64 + private viewContainerRef: ViewContainerRef) {
  65 + this.createCircularProgress();
  66 + }
  67 +
  68 + createCircularProgress() {
  69 + this.elementRef.nativeElement.style.position = 'relative';
  70 + const factory = this.componentFactoryResolver.resolveComponentFactory(MatSpinner);
  71 + this.spinnerRef = this.viewContainerRef.createComponent(factory, 0);
  72 + this.spinnerRef.instance.mode = 'indeterminate';
  73 + this.spinnerRef.instance.diameter = 20;
  74 + const el = this.spinnerRef.instance._elementRef.nativeElement;
  75 + el.style.margin = 'auto';
  76 + el.style.position = 'absolute';
  77 + el.style.left = '0';
  78 + el.style.right = '0';
  79 + el.style.top = '0';
  80 + el.style.bottom = '0';
  81 + el.style.display = 'none';
  82 + }
  83 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div fxLayout="column" class="mat-content mat-padding">
  19 + <mat-form-field>
  20 + <mat-label>{{'dashboard.select-dashboard' | translate}}</mat-label>
  21 + <mat-select matInput [(ngModel)]="dashboardId"
  22 + (ngModelChange)="dashboardSelected(dashboardId)">
  23 + <mat-option *ngFor="let dashboard of dashboards$ | async" [value]="dashboard.id.id">
  24 + {{dashboard.title}}
  25 + </mat-option>
  26 + </mat-select>
  27 + </mat-form-field>
  28 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../scss/constants";
  17 +
  18 +:host {
  19 + min-width: 300px;
  20 + max-width: 320px;
  21 + max-height: 150px;
  22 + overflow-x: hidden;
  23 + overflow-y: auto;
  24 + background: #fff;
  25 + border-radius: 4px;
  26 + box-shadow:
  27 + 0 7px 8px -4px rgba(0, 0, 0, .2),
  28 + 0 13px 19px 2px rgba(0, 0, 0, .14),
  29 + 0 5px 24px 4px rgba(0, 0, 0, .12);
  30 +
  31 + @media (min-height: 350px) {
  32 + max-height: 250px;
  33 + }
  34 +
  35 + @media #{$mat-gt-xs} {
  36 + max-width: 100%;
  37 + }
  38 +
  39 + .mat-content {
  40 + background-color: #fff;
  41 + }
  42 +}
  43 +
  44 +:host ::ng-deep {
  45 + mat-form-field {
  46 + .mat-form-field-infix {
  47 + width: 100%;
  48 + mat-select {
  49 + .mat-select-value {
  50 + max-width: 100%;
  51 + }
  52 + }
  53 + }
  54 + }
  55 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, InjectionToken } from '@angular/core';
  18 +import { Observable } from 'rxjs';
  19 +import { DashboardInfo } from '../models/dashboard.models';
  20 +
  21 +export const DASHBOARD_SELECT_PANEL_DATA = new InjectionToken<any>('DashboardSelectPanelData');
  22 +
  23 +export interface DashboardSelectPanelData {
  24 + dashboards$: Observable<Array<DashboardInfo>>;
  25 + dashboardId: string;
  26 + onDashboardSelected: (dashboardId: string) => void;
  27 +}
  28 +
  29 +@Component({
  30 + selector: 'tb-dashboard-select-panel',
  31 + templateUrl: './dashboard-select-panel.component.html',
  32 + styleUrls: ['./dashboard-select-panel.component.scss']
  33 +})
  34 +export class DashboardSelectPanelComponent {
  35 +
  36 + dashboards$: Observable<Array<DashboardInfo>>;
  37 + dashboardId: string;
  38 +
  39 + constructor(@Inject(DASHBOARD_SELECT_PANEL_DATA)
  40 + private data: DashboardSelectPanelData) {
  41 + this.dashboards$ = this.data.dashboards$;
  42 + this.dashboardId = this.data.dashboardId;
  43 + }
  44 +
  45 + public dashboardSelected(dashboardId: string) {
  46 + this.data.onDashboardSelected(dashboardId);
  47 + }
  48 +}