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 47 "node_modules/flot/src/plugins/jquery.flot.stack.js",
48 48 "node_modules/flot.curvedlines/curvedLines.js",
49 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 54 "node_modules/ace-builds/src-min/ace.js",
51 55 "node_modules/ace-builds/src-min/ext-language_tools.js",
52 56 "node_modules/ace-builds/src-min/ext-searchbox.js",
... ...
... ... @@ -1170,12 +1170,23 @@
1170 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 1179 "@types/minimatch": {
1174 1180 "version": "3.0.3",
1175 1181 "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
1176 1182 "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
1177 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 1190 "@types/node": {
1180 1191 "version": "10.14.15",
1181 1192 "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz",
... ... @@ -1434,6 +1445,11 @@
1434 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 1453 "accepts": {
1438 1454 "version": "1.3.7",
1439 1455 "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
... ... @@ -1523,6 +1539,15 @@
1523 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 1551 "ansi-colors": {
1527 1552 "version": "3.2.4",
1528 1553 "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
... ... @@ -1955,8 +1980,7 @@
1955 1980 "balanced-match": {
1956 1981 "version": "1.0.0",
1957 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 1985 "base": {
1962 1986 "version": "0.11.2",
... ... @@ -2143,7 +2167,6 @@
2143 2167 "version": "1.1.11",
2144 2168 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
2145 2169 "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
2146   - "dev": true,
2147 2170 "requires": {
2148 2171 "balanced-match": "^1.0.0",
2149 2172 "concat-map": "0.0.1"
... ... @@ -2854,8 +2877,7 @@
2854 2877 "commander": {
2855 2878 "version": "2.20.0",
2856 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 2882 "commondir": {
2861 2883 "version": "1.0.1",
... ... @@ -3107,8 +3129,7 @@
3107 3129 "concat-map": {
3108 3130 "version": "0.0.1",
3109 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 3134 "concat-stream": {
3114 3135 "version": "1.6.2",
... ... @@ -3122,6 +3143,15 @@
3122 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 3155 "connect": {
3126 3156 "version": "3.7.0",
3127 3157 "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
... ... @@ -3762,6 +3792,17 @@
3762 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 3806 "ee-first": {
3766 3807 "version": "1.1.1",
3767 3808 "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
... ... @@ -4574,8 +4615,7 @@
4574 4615 "fs.realpath": {
4575 4616 "version": "1.0.0",
4576 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 4620 "fsevents": {
4581 4621 "version": "1.2.9",
... ... @@ -5183,7 +5223,6 @@
5183 5223 "version": "7.1.3",
5184 5224 "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
5185 5225 "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
5186   - "dev": true,
5187 5226 "requires": {
5188 5227 "fs.realpath": "^1.0.0",
5189 5228 "inflight": "^1.0.4",
... ... @@ -5709,7 +5748,6 @@
5709 5748 "version": "1.0.6",
5710 5749 "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
5711 5750 "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
5712   - "dev": true,
5713 5751 "requires": {
5714 5752 "once": "^1.3.0",
5715 5753 "wrappy": "1"
... ... @@ -5718,14 +5756,12 @@
5718 5756 "inherits": {
5719 5757 "version": "2.0.4",
5720 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 5761 "ini": {
5725 5762 "version": "1.3.5",
5726 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 5766 "inquirer": {
5731 5767 "version": "6.5.0",
... ... @@ -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 6495 "js-tokens": {
6448 6496 "version": "3.0.2",
6449 6497 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
... ... @@ -6918,7 +6966,6 @@
6918 6966 "version": "4.1.5",
6919 6967 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
6920 6968 "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
6921   - "dev": true,
6922 6969 "requires": {
6923 6970 "pseudomap": "^1.0.2",
6924 6971 "yallist": "^2.1.2"
... ... @@ -7258,7 +7305,6 @@
7258 7305 "version": "3.0.4",
7259 7306 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
7260 7307 "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
7261   - "dev": true,
7262 7308 "requires": {
7263 7309 "brace-expansion": "^1.1.7"
7264 7310 }
... ... @@ -7368,7 +7414,6 @@
7368 7414 "version": "0.5.1",
7369 7415 "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
7370 7416 "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
7371   - "dev": true,
7372 7417 "requires": {
7373 7418 "minimist": "0.0.8"
7374 7419 },
... ... @@ -7376,8 +7421,7 @@
7376 7421 "minimist": {
7377 7422 "version": "0.0.8",
7378 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 7430 "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
7387 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 7438 "move-concurrently": {
7390 7439 "version": "1.0.1",
7391 7440 "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
... ... @@ -7563,6 +7612,15 @@
7563 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 7624 "normalize-package-data": {
7567 7625 "version": "2.5.0",
7568 7626 "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
... ... @@ -7801,7 +7859,6 @@
7801 7859 "version": "1.4.0",
7802 7860 "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
7803 7861 "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
7804   - "dev": true,
7805 7862 "requires": {
7806 7863 "wrappy": "1"
7807 7864 }
... ... @@ -7877,8 +7934,7 @@
7877 7934 "os-homedir": {
7878 7935 "version": "1.0.2",
7879 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 7939 "os-locale": {
7884 7940 "version": "3.1.0",
... ... @@ -7894,14 +7950,12 @@
7894 7950 "os-tmpdir": {
7895 7951 "version": "1.0.2",
7896 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 7955 "osenv": {
7901 7956 "version": "0.1.5",
7902 7957 "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
7903 7958 "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
7904   - "dev": true,
7905 7959 "requires": {
7906 7960 "os-homedir": "^1.0.0",
7907 7961 "os-tmpdir": "^1.0.0"
... ... @@ -8222,8 +8276,7 @@
8222 8276 "path-is-absolute": {
8223 8277 "version": "1.0.1",
8224 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 8281 "path-is-inside": {
8229 8282 "version": "1.0.2",
... ... @@ -8465,6 +8518,11 @@
8465 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 8526 "protoduck": {
8469 8527 "version": "5.0.1",
8470 8528 "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz",
... ... @@ -8612,8 +8670,7 @@
8612 8670 "pseudomap": {
8613 8671 "version": "1.0.2",
8614 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 8675 "psl": {
8619 8676 "version": "1.3.0",
... ... @@ -9208,8 +9265,7 @@
9208 9265 "semver": {
9209 9266 "version": "5.6.0",
9210 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 9270 "semver-dsl": {
9215 9271 "version": "1.0.1",
... ... @@ -9408,6 +9464,11 @@
9408 9464 "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
9409 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 9472 "signal-exit": {
9412 9473 "version": "3.0.2",
9413 9474 "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
... ... @@ -9920,6 +9981,11 @@
9920 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 9989 "sprintf-js": {
9924 9990 "version": "1.0.3",
9925 9991 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
... ... @@ -11121,8 +11187,7 @@
11121 11187 "wrappy": {
11122 11188 "version": "1.0.2",
11123 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 11192 "ws": {
11128 11193 "version": "3.3.3",
... ... @@ -11180,8 +11245,7 @@
11180 11245 "yallist": {
11181 11246 "version": "2.1.2",
11182 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 11250 "yargs": {
11187 11251 "version": "12.0.5",
... ...
... ... @@ -33,6 +33,7 @@
33 33 "@ngx-translate/http-loader": "^4.0.0",
34 34 "ace-builds": "^1.4.5",
35 35 "angular-gridster2": "^8.1.0",
  36 + "angular2-hotkeys": "^2.1.5",
36 37 "base64-js": "^1.3.1",
37 38 "compass-sass-mixins": "^0.12.7",
38 39 "core-js": "^3.1.4",
... ... @@ -44,6 +45,7 @@
44 45 "javascript-detect-element-resize": "^0.5.3",
45 46 "jquery": "^3.4.1",
46 47 "jquery.terminal": "^2.8.0",
  48 + "js-beautify": "^1.10.2",
47 49 "material-design-icons": "^3.0.1",
48 50 "messageformat": "^2.3.0",
49 51 "moment": "^2.24.0",
... ... @@ -51,6 +53,7 @@
51 53 "ngx-translate-messageformat-compiler": "^4.5.0",
52 54 "rxjs": "~6.5.2",
53 55 "screenfull": "^4.2.1",
  56 + "split.js": "^1.5.11",
54 57 "tinycolor2": "^1.4.1",
55 58 "tslib": "^1.10.0",
56 59 "typeface-roboto": "^0.0.75",
... ... @@ -66,6 +69,7 @@
66 69 "@types/jasmine": "~3.4.0",
67 70 "@types/jasminewd2": "~2.0.6",
68 71 "@types/jquery": "^3.3.31",
  72 + "@types/js-beautify": "^1.8.1",
69 73 "@types/node": "~10.14.15",
70 74 "@types/tinycolor2": "^1.4.2",
71 75 "codelyzer": "~5.1.0",
... ...
... ... @@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module';
24 24 import { HomeModule } from './modules/home/home.module';
25 25
26 26 import { AppComponent } from './app.component';
  27 +import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module';
27 28
28 29 @NgModule({
29 30 declarations: [
... ... @@ -35,7 +36,8 @@ import { AppComponent } from './app.component';
35 36 AppRoutingModule,
36 37 CoreModule,
37 38 LoginModule,
38   - HomeModule
  39 + HomeModule,
  40 + DashboardRoutingModule
39 41 ],
40 42 providers: [],
41 43 bootstrap: [AppComponent]
... ...
... ... @@ -14,19 +14,26 @@
14 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 18 import { Observable, of, Subject } from 'rxjs';
19 19 import { Datasource } from '@app/shared/models/widget.models';
20 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 27 export class DummyAliasController implements IAliasController {
23 28
24 29 entityAliasesChanged: Observable<Array<string>>;
  30 + entityAliasResolved: Observable<string>;
25 31
26 32 [key: string]: any | null;
27 33
28 34 constructor() {
29 35 this.entityAliasesChanged = new Subject<Array<string>>().asObservable();
  36 + this.entityAliasResolved = new Subject<string>().asObservable();
30 37 }
31 38
32 39 getAliasInfo(aliasId): Observable<AliasInfo> {
... ... @@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController {
36 43 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
37 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 34 import { HttpErrorResponse } from '@angular/common/http';
35 35 import { DatasourceService } from '@core/api/datasource.service';
36 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 40 export interface TimewindowFunctions {
39 41 onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
... ... @@ -66,20 +68,24 @@ export interface WidgetActionsApi {
66 68 }
67 69
68 70 export interface AliasInfo {
  71 + alias?: string;
69 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 76 [key: string]: any | null;
76 77 // TODO:
77 78 }
78 79
79 80 export interface IAliasController {
80 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 85 resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>;
  86 + getEntityAliases(): EntityAliases;
  87 + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
  88 + updateEntityAliases(entityAliases: EntityAliases);
83 89 [key: string]: any | null;
84 90 // TODO:
85 91 }
... ... @@ -97,17 +103,14 @@ export interface StateParams {
97 103 }
98 104
99 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 111 // TODO:
104 112 }
105 113
106   -export interface EntityInfo {
107   - entityId: EntityId;
108   - entityName: string;
109   -}
110   -
111 114 export interface SubscriptionInfo {
112 115 type: DatasourceType;
113 116 name?: string;
... ... @@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions {
171 174 // TODO:
172 175 }
173 176
  177 +export interface SubscriptionEntityInfo {
  178 + entityId: EntityId;
  179 + entityName: string;
  180 +}
  181 +
174 182 export interface IWidgetSubscription {
175 183
176 184 id: string;
... ... @@ -201,7 +209,7 @@ export interface IWidgetSubscription {
201 209 rpcErrorText?: string;
202 210 rpcRejection?: HttpErrorResponse;
203 211
204   - getFirstEntityInfo(): EntityInfo;
  212 + getFirstEntityInfo(): SubscriptionEntityInfo;
205 213
206 214 onAliasesChanged(aliasIds: Array<string>): boolean;
207 215
... ...
... ... @@ -15,8 +15,7 @@
15 15 ///
16 16
17 17 import {
18   - EntityInfo,
19   - IWidgetSubscription,
  18 + IWidgetSubscription, SubscriptionEntityInfo,
20 19 WidgetSubscriptionCallbacks,
21 20 WidgetSubscriptionContext,
22 21 WidgetSubscriptionOptions
... ... @@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription {
339 338 this.onDataUpdated();
340 339 }
341 340
342   - getFirstEntityInfo(): EntityInfo {
  341 + getFirstEntityInfo(): SubscriptionEntityInfo {
343 342 return undefined;
344 343 }
345 344
... ...
... ... @@ -42,6 +42,8 @@ import {TimeService} from '@core/services/time.service';
42 42 })
43 43 export class AuthService {
44 44
  45 + forceFullscreen = false; // TODO:
  46 +
45 47 constructor(
46 48 private store: Store<AppState>,
47 49 private http: HttpClient,
... ...
... ... @@ -39,6 +39,7 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
39 39 import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
40 40 import { WINDOW_PROVIDERS } from '@core/services/window.service';
41 41 import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
  42 +import { HotkeyModule } from 'angular2-hotkeys';
42 43
43 44 export function HttpLoaderFactory(http: HttpClient) {
44 45 return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
... ... @@ -79,6 +80,7 @@ export function HttpLoaderFactory(http: HttpClient) {
79 80 useClass: TranslateDefaultCompiler
80 81 }
81 82 }),
  83 + HotkeyModule.forRoot(),
82 84
83 85 // ngrx
84 86 StoreModule.forRoot(reducers, { metaReducers }),
... ...
... ... @@ -28,21 +28,26 @@ import { selectAuth } from '@core/auth/auth.selectors';
28 28 import { take } from 'rxjs/operators';
29 29 import { DialogService } from '@core/services/dialog.service';
30 30 import { TranslateService } from '@ngx-translate/core';
  31 +import { isDefined } from '../utils';
31 32
32 33 export interface HasConfirmForm {
33 34 confirmForm(): FormGroup;
34 35 }
35 36
  37 +export interface HasDirtyFlag {
  38 + isDirty: boolean;
  39 +}
  40 +
36 41 @Injectable({
37 42 providedIn: 'root'
38 43 })
39   -export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> {
  44 +export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm & HasDirtyFlag> {
40 45
41 46 constructor(private store: Store<AppState>,
42 47 private dialogService: DialogService,
43 48 private translate: TranslateService) { }
44 49
45   - canDeactivate(component: HasConfirmForm,
  50 + canDeactivate(component: HasConfirmForm & HasDirtyFlag,
46 51 route: ActivatedRouteSnapshot,
47 52 state: RouterStateSnapshot) {
48 53
... ... @@ -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 73 return this.dialogService.confirm(
61 74 this.translate.instant('confirm-on-exit.title'),
62 75 this.translate.instant('confirm-on-exit.html-message')
... ...
... ... @@ -21,10 +21,12 @@ import { HttpClient } from '@angular/common/http';
21 21 import { PageLink } from '@shared/models/page/page-link';
22 22 import { PageData } from '@shared/models/page/page-data';
23 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 25 import { UtilsService } from '@core/services/utils.service';
26 26 import { TranslateService } from '@ngx-translate/core';
27 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 31 @Injectable({
30 32 providedIn: 'root'
... ... @@ -70,4 +72,23 @@ export class WidgetService {
70 72 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
71 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 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 19 import { AppState } from '../core.state';
20 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 25 export const selectLoadState = createFeatureSelector<AppState, LoadState>(
23 26 'load'
... ... @@ -32,3 +35,11 @@ export const selectIsLoading = createSelector(
32 35 selectLoadState,
33 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 21 import { WindowMessage } from '@shared/models/window-message.model';
22 22 import { TranslateService } from '@ngx-translate/core';
23 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 25 import { EntityType } from '@shared/models/entity-type.models';
26 26 import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
27 27 import { alarmFields } from '@shared/models/alarm.models';
28 28 import { materialColors } from '@app/shared/models/material.models';
  29 +import { WidgetInfo } from '@home/models/widget-component.models';
29 30
30 31 @Injectable({
31 32 providedIn: 'root'
... ... @@ -34,7 +35,7 @@ export class UtilsService {
34 35
35 36 iframeMode = false;
36 37 widgetEditMode = false;
37   - editWidgetInfo: any = null;
  38 + editWidgetInfo: WidgetInfo = null;
38 39
39 40 constructor(@Inject(WINDOW) private window: Window,
40 41 private translate: TranslateService) {
... ... @@ -87,7 +88,7 @@ export class UtilsService {
87 88 type: 'widgetException',
88 89 data
89 90 };
90   - this.window.parent.postMessage(message, '*');
  91 + this.window.parent.postMessage(JSON.stringify(message), '*');
91 92 }
92 93 return data;
93 94 }
... ...
... ... @@ -95,6 +95,10 @@ export function isNumber(value: any): boolean {
95 95 return typeof value === 'number';
96 96 }
97 97
  98 +export function isString(value: any): boolean {
  99 + return typeof value === 'string';
  100 +}
  101 +
98 102 export function objToBase64(obj: any): string {
99 103 const json = JSON.stringify(obj);
100 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 38 import { WidgetComponent } from '@home/components/widget/widget.component';
39 39 import { WidgetComponentService } from './widget/widget-component.service';
40 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 44 @NgModule({
43 45 entryComponents: [
... ... @@ -48,7 +50,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
48 50 AlarmTableHeaderComponent,
49 51 AlarmDetailsDialogComponent,
50 52 AddAttributeDialogComponent,
51   - EditAttributeValuePanelComponent
  53 + EditAttributeValuePanelComponent,
  54 + AliasesEntitySelectPanelComponent
52 55 ],
53 56 declarations:
54 57 [
... ... @@ -69,6 +72,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
69 72 AttributeTableComponent,
70 73 AddAttributeDialogComponent,
71 74 EditAttributeValuePanelComponent,
  75 + AliasesEntitySelectPanelComponent,
  76 + AliasesEntitySelectComponent,
72 77 DashboardComponent,
73 78 WidgetComponent,
74 79 LegendComponent
... ... @@ -89,6 +94,7 @@ import { LegendComponent } from '@home/components/widget/legend.component';
89 94 AlarmTableComponent,
90 95 AlarmDetailsDialogComponent,
91 96 AttributeTableComponent,
  97 + AliasesEntitySelectComponent,
92 98 DashboardComponent,
93 99 WidgetComponent,
94 100 LegendComponent
... ...
... ... @@ -61,10 +61,9 @@ import {
61 61 WidgetTypeInstance
62 62 } from '@home/models/widget-component.models';
63 63 import {
64   - EntityInfo,
65 64 IWidgetSubscription,
66 65 StateObject,
67   - StateParams,
  66 + StateParams, SubscriptionEntityInfo,
68 67 SubscriptionInfo,
69 68 WidgetSubscriptionContext,
70 69 WidgetSubscriptionOptions
... ... @@ -1065,7 +1064,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
1065 1064 this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
1066 1065 }
1067 1066
1068   - private getActiveEntityInfo(): EntityInfo {
  1067 + private getActiveEntityInfo(): SubscriptionEntityInfo {
1069 1068 let entityInfo = this.widgetContext.activeEntityInfo;
1070 1069 if (!entityInfo) {
1071 1070 for (const id of Object.keys(this.widgetContext.subscriptions)) {
... ...
... ... @@ -38,7 +38,7 @@
38 38 <button mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()">
39 39 <mat-icon class="material-icons">menu</mat-icon>
40 40 </button>
41   - <div fxFlex tb-breadcrumb class="mat-toolbar-tools">
  41 + <div fxFlex tb-breadcrumb [activeComponent]="activeComponent" class="mat-toolbar-tools">
42 42 </div>
43 43 <button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()">
44 44 <mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
... ... @@ -49,7 +49,7 @@
49 49 *ngIf="isLoading$ | async">
50 50 </mat-progress-bar>
51 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 53 </div>
54 54 </div>
55 55 </mat-sidenav-content>
... ...
... ... @@ -40,6 +40,8 @@ import { MatSidenav } from '@angular/material';
40 40 })
41 41 export class HomeComponent extends PageComponent implements OnInit {
42 42
  43 + activeComponent: any;
  44 +
43 45 sidenavMode = 'side';
44 46 sidenavOpened = true;
45 47
... ...
... ... @@ -31,12 +31,11 @@ import {
31 31 } from '@shared/models/widget.models';
32 32 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
33 33 import {
34   - EntityInfo,
35 34 IAliasController,
36 35 IStateController,
37 36 IWidgetSubscription,
38 37 IWidgetUtils,
39   - RpcApi,
  38 + RpcApi, SubscriptionEntityInfo,
40 39 TimewindowFunctions,
41 40 WidgetActionsApi,
42 41 WidgetSubscriptionApi
... ... @@ -85,7 +84,7 @@ export interface WidgetContext {
85 84 actionsApi?: WidgetActionsApi;
86 85 stateController?: IStateController;
87 86 aliasController?: IAliasController;
88   - activeEntityInfo?: EntityInfo;
  87 + activeEntityInfo?: SubscriptionEntityInfo;
89 88 widgetTitleTemplate?: string;
90 89 widgetTitle?: string;
91 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 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 20 import {EntitiesTableComponent} from '../../components/entity/entities-table.component';
21 21 import {Authority} from '@shared/models/authority.enum';
22 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 51 const routes: Routes = [
25 52 {
... ... @@ -42,6 +69,22 @@ const routes: Routes = [
42 69 resolve: {
43 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 94 imports: [RouterModule.forChild(routes)],
52 95 exports: [RouterModule],
53 96 providers: [
54   - DashboardsTableConfigResolver
  97 + DashboardsTableConfigResolver,
  98 + DashboardResolver
55 99 ]
56 100 })
57 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 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 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 30 @NgModule({
29 31 entryComponents: [
... ... @@ -36,7 +38,9 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
36 38 DashboardFormComponent,
37 39 DashboardTabsComponent,
38 40 ManageDashboardCustomersDialogComponent,
39   - MakeDashboardPublicDialogComponent
  41 + MakeDashboardPublicDialogComponent,
  42 + DashboardToolbarComponent,
  43 + DashboardPageComponent
40 44 ],
41 45 imports: [
42 46 CommonModule,
... ...
... ... @@ -303,9 +303,11 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
303 303 if ($event) {
304 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 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 17 import { Injectable, NgModule } from '@angular/core';
18 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 23 import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
25 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 25 import { Observable } from 'rxjs';
31   -import { getCurrentAuthUser } from '@core/auth/auth.selectors';
32 26 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
33 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 39 @Injectable()
36 40 export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
... ... @@ -39,13 +43,61 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
39 43 }
40 44
41 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 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 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 101 export const routes: Routes = [
50 102 {
51 103 path: 'widgets-bundles',
... ... @@ -69,10 +121,7 @@ export const routes: Routes = [
69 121 },
70 122 {
71 123 path: ':widgetsBundleId/widgetTypes',
72   - component: WidgetLibraryComponent,
73 124 data: {
74   - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
75   - title: 'widget.widget-library',
76 125 breadcrumb: {
77 126 labelFunction: widgetTypesBreadcumbLabelFunction,
78 127 icon: 'now_widgets'
... ... @@ -80,7 +129,49 @@ export const routes: Routes = [
80 129 },
81 130 resolve: {
82 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 182 exports: [RouterModule],
92 183 providers: [
93 184 WidgetsBundlesTableConfigResolver,
94   - WidgetsBundleResolver
  185 + WidgetsBundleResolver,
  186 + WidgetEditorDataResolver,
  187 + WidgetEditorAddDataResolver
95 188 ]
96 189 })
97 190 export class WidgetLibraryRoutingModule { }
... ...
... ... @@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component';
21 21 import { AuthUser } from '@shared/models/user.model';
22 22 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
23 23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24   -import { ActivatedRoute } from '@angular/router';
  24 +import { ActivatedRoute, Router } from '@angular/router';
25 25 import { Authority } from '@shared/models/authority.enum';
26 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 27 import { Observable } from 'rxjs';
... ... @@ -34,6 +34,13 @@ import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-componen
34 34 import { IAliasController } from '@app/core/api/widget-api.models';
35 35 import { toWidgetInfo } from '@home/models/widget-component.models';
36 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 45 @Component({
39 46 selector: 'tb-widget-library',
... ... @@ -83,8 +90,10 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
83 90
84 91 constructor(protected store: Store<AppState>,
85 92 private route: ActivatedRoute,
  93 + private router: Router,
86 94 private widgetService: WidgetService,
87   - private dialogService: DialogService) {
  95 + private dialogService: DialogService,
  96 + private dialog: MatDialog) {
88 97 super(store);
89 98
90 99 this.authUser = getCurrentAuthUser(store);
... ... @@ -163,33 +172,43 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
163 172 }
164 173
165 174 importWidgetType($event: Event): void {
166   - if (event) {
167   - event.stopPropagation();
  175 + if ($event) {
  176 + $event.stopPropagation();
168 177 }
169 178 this.dialogService.todo();
170 179 }
171 180
172 181 openWidgetType($event: Event, widget?: Widget): void {
173   - if (event) {
174   - event.stopPropagation();
  182 + if ($event) {
  183 + $event.stopPropagation();
175 184 }
176 185 if (widget) {
177   - this.dialogService.todo();
  186 + this.router.navigate([widget.typeId.id], {relativeTo: this.route});
178 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 202 exportWidgetType($event: Event, widget: Widget): void {
184   - if (event) {
185   - event.stopPropagation();
  203 + if ($event) {
  204 + $event.stopPropagation();
186 205 }
187 206 this.dialogService.todo();
188 207 }
189 208
190 209 removeWidgetType($event: Event, widget: Widget): void {
191   - if (event) {
192   - event.stopPropagation();
  210 + if ($event) {
  211 + $event.stopPropagation();
193 212 }
194 213 this.dialogService.todo();
195 214 }
... ...
... ... @@ -21,14 +21,19 @@ import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.
21 21 import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
22 22 import {HomeComponentsModule} from '@modules/home/components/home-components.module';
23 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 27 @NgModule({
26 28 entryComponents: [
27   - WidgetsBundleComponent
  29 + WidgetsBundleComponent,
  30 + SelectWidgetTypeDialogComponent
28 31 ],
29 32 declarations: [
30 33 WidgetsBundleComponent,
31   - WidgetLibraryComponent
  34 + WidgetLibraryComponent,
  35 + WidgetEditorComponent,
  36 + SelectWidgetTypeDialogComponent
32 37 ],
33 38 imports: [
34 39 CommonModule,
... ...
... ... @@ -17,7 +17,7 @@
17 17 -->
18 18 <div fxFlex class="tb-breadcrumb" fxLayout="row">
19 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 21 </h1>
22 22 <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast">
23 23 <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
... ... @@ -26,7 +26,7 @@
26 26 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
27 27 {{ breadcrumb.icon }}
28 28 </mat-icon>
29   - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
  29 + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
30 30 </a>
31 31 <span *ngSwitchCase="true">
32 32 <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
... ... @@ -34,7 +34,7 @@
34 34 <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
35 35 {{ breadcrumb.icon }}
36 36 </mat-icon>
37   - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }}
  37 + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
38 38 </span>
39 39 <span class="divider" [fxHide]="isLast"> > </span>
40 40 </span>
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { BehaviorSubject, Subject } from 'rxjs';
19 19 import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
20 20 import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
... ... @@ -28,6 +28,13 @@ import { TranslateService } from '@ngx-translate/core';
28 28 })
29 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 38 breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot));
32 39
33 40 routerEventsSubscription = this.router.events.pipe(
... ... @@ -61,9 +68,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
61 68 const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig;
62 69 if (breadcrumbConfig && !breadcrumbConfig.skip) {
63 70 let label;
  71 + let labelFunction;
64 72 let ignoreTranslate;
65 73 if (breadcrumbConfig.labelFunction) {
66   - label = breadcrumbConfig.labelFunction(route, this.translate);
  74 + labelFunction = () => {
  75 + return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue);
  76 + };
67 77 ignoreTranslate = true;
68 78 } else {
69 79 label = breadcrumbConfig.label || 'home.home';
... ... @@ -71,10 +81,11 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
71 81 }
72 82 const icon = breadcrumbConfig.icon || 'home';
73 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 85 const queryParams = route.queryParams;
76 86 const breadcrumb = {
77 87 label,
  88 + labelFunction,
78 89 ignoreTranslate,
79 90 icon,
80 91 isMdiIcon,
... ... @@ -89,5 +100,4 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
89 100 }
90 101 return newBreadcrumbs;
91 102 }
92   -
93 103 }
... ...
... ... @@ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core';
19 19
20 20 export interface BreadCrumb {
21 21 label: string;
  22 + labelFunction?: () => string;
22 23 ignoreTranslate: boolean;
23 24 icon: string;
24 25 isMdiIcon: boolean;
... ... @@ -26,7 +27,7 @@ export interface BreadCrumb {
26 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 32 export interface BreadCrumbConfig {
32 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 +}
... ...