Commit 700293d5f9c51f5062cb28bd9d1d37afbc83cb4c

Authored by Igor Kulikov
1 parent d29a8731

Implement Widget Editor. Dashboard page initial implementation.

Showing 74 changed files with 4959 additions and 184 deletions
... ... @@ -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 +}
... ...
  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-select fxHide.xs fxHide.sm fxHide.md
  19 + [required]="required"
  20 + [disabled]="disabled"
  21 + [(ngModel)]="dashboardId"
  22 + (ngModelChange)="dashboardIdChanged()">
  23 + <mat-option *ngFor="let dashboard of dashboards$ | async" [value]="dashboard.id.id">
  24 + {{dashboard.title}}
  25 + </mat-option>
  26 +</mat-select>
  27 +<section fxHide.gt-md class="tb-dashboard-select">
  28 + <button mat-button mat-icon-button
  29 + cdkOverlayOrigin #dashboardSelectPanelOrigin="cdkOverlayOrigin"
  30 + (click)="openDashboardSelectPanel()"
  31 + matTooltip="{{ 'dashboard.select-dashboard' | translate }}"
  32 + [matTooltipPosition]="tooltipPosition">
  33 + <mat-icon>dashboards</mat-icon>
  34 + </button>
  35 +</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 +:host {
  17 + min-width: 52px;
  18 +
  19 + mat-select {
  20 + max-width: 300px;
  21 + pointer-events: all;
  22 + }
  23 +
  24 + .tb-dashboard-select {
  25 + min-height: 32px;
  26 +
  27 + span {
  28 + pointer-events: all;
  29 + cursor: pointer;
  30 + }
  31 + }
  32 +}
  33 +
  34 +:host ::ng-deep {
  35 + mat-select {
  36 + .mat-select-value {
  37 + max-width: 282px;
  38 + }
  39 + }
  40 +}
... ...
  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, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
  18 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { Observable, of } from 'rxjs';
  20 +import { PageLink } from '@shared/models/page/page-link';
  21 +import { map, share } from 'rxjs/operators';
  22 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  23 +import { DashboardInfo } from '@app/shared/models/dashboard.models';
  24 +import { DashboardService } from '@core/http/dashboard.service';
  25 +import { Store } from '@ngrx/store';
  26 +import { AppState } from '@app/core/core.state';
  27 +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
  28 +import { Authority } from '@shared/models/authority.enum';
  29 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  30 +import { TooltipPosition } from '@angular/material/tooltip';
  31 +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  32 +import { BreakpointObserver } from '@angular/cdk/layout';
  33 +import { DOCUMENT } from '@angular/common';
  34 +import { WINDOW } from '@core/services/window.service';
  35 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  36 +import {
  37 + DASHBOARD_SELECT_PANEL_DATA,
  38 + DashboardSelectPanelComponent,
  39 + DashboardSelectPanelData
  40 +} from './dashboard-select-panel.component';
  41 +
  42 +@Component({
  43 + selector: 'tb-dashboard-select',
  44 + templateUrl: './dashboard-select.component.html',
  45 + styleUrls: ['./dashboard-select.component.scss'],
  46 + providers: [{
  47 + provide: NG_VALUE_ACCESSOR,
  48 + useExisting: forwardRef(() => DashboardSelectComponent),
  49 + multi: true
  50 + }]
  51 +})
  52 +export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
  53 +
  54 + @Input()
  55 + dashboardsScope: 'customer' | 'tenant';
  56 +
  57 + @Input()
  58 + customerId: string;
  59 +
  60 + @Input()
  61 + tooltipPosition: TooltipPosition = 'above';
  62 +
  63 + private requiredValue: boolean;
  64 + get required(): boolean {
  65 + return this.requiredValue;
  66 + }
  67 + @Input()
  68 + set required(value: boolean) {
  69 + this.requiredValue = coerceBooleanProperty(value);
  70 + }
  71 +
  72 + @Input()
  73 + disabled: boolean;
  74 +
  75 + dashboards$: Observable<Array<DashboardInfo>>;
  76 +
  77 + dashboardId: string | null;
  78 +
  79 + @ViewChild('dashboardSelectPanelOrigin', {static: false}) dashboardSelectPanelOrigin: CdkOverlayOrigin;
  80 +
  81 + private propagateChange = (v: any) => { };
  82 +
  83 + constructor(private store: Store<AppState>,
  84 + private dashboardService: DashboardService,
  85 + private overlay: Overlay,
  86 + private breakpointObserver: BreakpointObserver,
  87 + private viewContainerRef: ViewContainerRef,
  88 + @Inject(DOCUMENT) private document: Document,
  89 + @Inject(WINDOW) private window: Window) {
  90 + }
  91 +
  92 + registerOnChange(fn: any): void {
  93 + this.propagateChange = fn;
  94 + }
  95 +
  96 + registerOnTouched(fn: any): void {
  97 + }
  98 +
  99 + ngOnInit() {
  100 +
  101 + const pageLink = new PageLink(100);
  102 +
  103 + this.dashboards$ = this.getDashboards(pageLink).pipe(
  104 + map((pageData) => pageData.data),
  105 + share()
  106 + );
  107 + }
  108 +
  109 + setDisabledState(isDisabled: boolean): void {
  110 + this.disabled = isDisabled;
  111 + }
  112 +
  113 + writeValue(value: string | null): void {
  114 + this.dashboardId = value;
  115 + }
  116 +
  117 + dashboardIdChanged() {
  118 + this.updateView();
  119 + }
  120 +
  121 + openDashboardSelectPanel() {
  122 + if (this.disabled) {
  123 + return;
  124 + }
  125 + const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
  126 + const panelWidth = 300;
  127 + const position = this.overlay.position();
  128 + const config = new OverlayConfig({
  129 + panelClass: 'tb-dashboard-select-panel',
  130 + backdropClass: 'cdk-overlay-transparent-backdrop',
  131 + hasBackdrop: true,
  132 + });
  133 + const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement;
  134 + const offset = el.getBoundingClientRect();
  135 + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
  136 + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
  137 + const bottomY = offset.bottom - scrollTop;
  138 + const leftX = offset.left - scrollLeft;
  139 + let originX;
  140 + let originY;
  141 + let overlayX;
  142 + let overlayY;
  143 + const wHeight = this.document.documentElement.clientHeight;
  144 + const wWidth = this.document.documentElement.clientWidth;
  145 + if (bottomY + panelHeight > wHeight) {
  146 + originY = 'top';
  147 + overlayY = 'bottom';
  148 + } else {
  149 + originY = 'bottom';
  150 + overlayY = 'top';
  151 + }
  152 + if (leftX + panelWidth > wWidth) {
  153 + originX = 'end';
  154 + overlayX = 'end';
  155 + } else {
  156 + originX = 'start';
  157 + overlayX = 'start';
  158 + }
  159 + const connectedPosition: ConnectedPosition = {
  160 + originX,
  161 + originY,
  162 + overlayX,
  163 + overlayY
  164 + };
  165 + config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef)
  166 + .withPositions([connectedPosition]);
  167 + const overlayRef = this.overlay.create(config);
  168 + overlayRef.backdropClick().subscribe(() => {
  169 + overlayRef.dispose();
  170 + });
  171 +
  172 + const injector = this._createDashboardSelectPanelInjector(
  173 + overlayRef,
  174 + {
  175 + dashboards$: this.dashboards$,
  176 + dashboardId: this.dashboardId,
  177 + onDashboardSelected: (dashboardId) => {
  178 + overlayRef.dispose();
  179 + this.dashboardId = dashboardId;
  180 + this.updateView();
  181 + }
  182 + }
  183 + );
  184 + overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
  185 + }
  186 +
  187 + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector {
  188 + const injectionTokens = new WeakMap<any, any>([
  189 + [DASHBOARD_SELECT_PANEL_DATA, data],
  190 + [OverlayRef, overlayRef]
  191 + ]);
  192 + return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  193 + }
  194 +
  195 + private updateView() {
  196 + this.propagateChange(this.dashboardId);
  197 + }
  198 +
  199 + private getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> {
  200 + let dashboardsObservable: Observable<PageData<DashboardInfo>>;
  201 + const authUser = getCurrentAuthUser(this.store);
  202 + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
  203 + if (this.customerId) {
  204 + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
  205 + } else {
  206 + dashboardsObservable = of(emptyPageData());
  207 + }
  208 + } else {
  209 + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true);
  210 + }
  211 + return dashboardsObservable;
  212 + }
  213 +
  214 +}
... ...
  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="mat-fab-toolbar-wrapper">
  19 + <div class="mat-fab-toolbar-content">
  20 + <ng-content></ng-content>
  21 + </div>
  22 +</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 +$font-size: 10px !default;
  17 +@function rem($multiplier) {
  18 + @return $multiplier * $font-size;
  19 +}
  20 +
  21 +$button-fab-width: rem(5.600) !default;
  22 +$button-fab-height: rem(5.600) !default;
  23 +$button-fab-padding: rem(1.60) !default;
  24 +$icon-button-margin: rem(0.600) !default;
  25 +$z-index-fab: 20 !default;
  26 +
  27 +$swift-ease-in-duration: 0.3s !default;
  28 +$swift-ease-in-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2) !default;
  29 +$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default;
  30 +
  31 +@mixin rtl-prop($ltr-prop, $rtl-prop, $value, $reset-value) {
  32 + #{$ltr-prop}: $value;
  33 + [dir=rtl] & {
  34 + #{$ltr-prop}: $reset-value;
  35 + #{$rtl-prop}: $value;
  36 + }
  37 +}
  38 +
  39 +@mixin fab-position($spot, $top: auto, $right: auto, $bottom: auto, $left: auto) {
  40 + &.mat-fab-#{$spot} {
  41 + top: $top;
  42 + right: $right;
  43 + bottom: $bottom;
  44 + left: $left;
  45 + position: absolute;
  46 + }
  47 +}
  48 +
  49 +@mixin fab-all-positions() {
  50 + @include fab-position(bottom-right, auto, ($button-fab-width - $button-fab-padding)/2, ($button-fab-height - $button-fab-padding)/2, auto);
  51 + @include fab-position(bottom-left, auto, auto, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2);
  52 + @include fab-position(top-right, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2, auto, auto);
  53 + @include fab-position(top-left, ($button-fab-height - $button-fab-padding)/2, auto, auto, ($button-fab-width - $button-fab-padding)/2);
  54 +}
  55 +
  56 +mat-fab-toolbar {
  57 + $icon-delay: 200ms;
  58 + @include fab-all-positions();
  59 + display: block;
  60 +
  61 + .mat-fab-toolbar-wrapper {
  62 + display: block;
  63 + position: relative;
  64 + overflow: hidden;
  65 +
  66 + height: $button-fab-width + ($icon-button-margin * 2);
  67 + }
  68 +
  69 + mat-fab-trigger {
  70 + position: absolute;
  71 + z-index: $z-index-fab;
  72 +
  73 + button {
  74 + overflow: visible !important;
  75 + opacity: .5;
  76 + }
  77 +
  78 + .mat-fab-toolbar-background {
  79 + display: block;
  80 + position: absolute;
  81 + z-index: $z-index-fab + 1;
  82 + opacity: 1;
  83 + }
  84 +
  85 + mat-icon {
  86 + position: relative;
  87 + z-index: $z-index-fab + 2;
  88 +
  89 + opacity: 1;
  90 +
  91 + }
  92 +
  93 + }
  94 +
  95 + &.mat-left {
  96 + mat-fab-trigger {
  97 + @include rtl-prop(right, left, 0, auto);
  98 + }
  99 +
  100 + .mat-toolbar-tools {
  101 + flex-direction: row-reverse;
  102 +
  103 + > .mat-button:first-child {
  104 + @include rtl-prop(margin-right, margin-left, 0.6rem, auto)
  105 + }
  106 +
  107 + > .mat-button:first-child {
  108 + @include rtl-prop(margin-left, margin-right, -0.8rem, auto);
  109 + }
  110 +
  111 +
  112 + > .mat-button:last-child {
  113 + @include rtl-prop(margin-right, margin-left, 8px, auto);
  114 + }
  115 +
  116 + }
  117 + }
  118 +
  119 + &.mat-right {
  120 + mat-fab-trigger {
  121 + @include rtl-prop(left, right, 0, auto);
  122 + }
  123 +
  124 + .mat-toolbar-tools {
  125 + flex-direction: row;
  126 + }
  127 + }
  128 +
  129 + mat-toolbar {
  130 + padding: 0 !important;
  131 + background-color: transparent !important;
  132 + pointer-events: none;
  133 + position: relative;
  134 + z-index: $z-index-fab + 3;
  135 +
  136 + .mat-toolbar-tools {
  137 + padding: 0 20px !important;
  138 + margin-top: 3px;
  139 + }
  140 +
  141 + .mat-fab-action-item {
  142 + opacity: 0;
  143 + transform: scale(0);
  144 + }
  145 + }
  146 +
  147 + &.mat-is-open {
  148 + mat-fab-trigger > button {
  149 + box-shadow: none;
  150 + opacity: 1;
  151 +
  152 + mat-icon {
  153 + opacity: 0;
  154 + }
  155 + }
  156 +
  157 + .mat-fab-action-item {
  158 + opacity: 1;
  159 + transform: scale(1);
  160 + }
  161 + }
  162 +
  163 + &.mat-animation {
  164 + mat-fab-trigger {
  165 + button {
  166 + transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s;
  167 + }
  168 + .mat-fab-toolbar-background {
  169 + transition: $swift-ease-in;
  170 + }
  171 + mat-icon {
  172 + transition: all $icon-delay ease-in;
  173 + }
  174 + }
  175 + mat-toolbar {
  176 + .mat-fab-action-item {
  177 + transition: $swift-ease-in;
  178 + transition-duration: $swift-ease-in-duration / 2;
  179 + }
  180 + }
  181 + &.mat-is-open {
  182 + mat-fab-trigger > button {
  183 + transition: opacity .3s cubic-bezier(.55, 0, .55, .2);
  184 + }
  185 + }
  186 + }
  187 +}
... ...
  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 {
  18 + Component,
  19 + ElementRef,
  20 + Input,
  21 + OnChanges,
  22 + OnInit,
  23 + Renderer2,
  24 + ViewEncapsulation,
  25 + SimpleChanges,
  26 + Inject, AfterViewInit, AfterViewChecked, Directive, OnDestroy
  27 +} from '@angular/core';
  28 +import { PageComponent } from '@shared/components/page.component';
  29 +import { WINDOW } from '@core/services/window.service';
  30 +import { mixinColor, CanColorCtor } from '@angular/material';
  31 +
  32 +export declare type FabToolbarDirection = 'left' | 'right';
  33 +
  34 +class MatFabToolbarBase {
  35 + // tslint:disable-next-line:variable-name
  36 + constructor(public _elementRef: ElementRef) {}
  37 +}
  38 +const MatFabToolbarMixinBase: CanColorCtor & typeof MatFabToolbarBase = mixinColor(MatFabToolbarBase);
  39 +
  40 +@Directive({
  41 + selector: 'mat-fab-trigger'
  42 +})
  43 +export class FabTriggerDirective {
  44 +
  45 + constructor(private el: ElementRef<HTMLElement>) {
  46 + }
  47 +
  48 +}
  49 +
  50 +@Directive({
  51 + selector: 'mat-fab-actions'
  52 +})
  53 +export class FabActionsDirective implements OnInit {
  54 +
  55 + constructor(private el: ElementRef<HTMLElement>) {
  56 + }
  57 +
  58 + ngOnInit(): void {
  59 + const element = $(this.el.nativeElement);
  60 + const children = element.children();
  61 + children.wrap('<div class="mat-fab-action-item">');
  62 + }
  63 +
  64 +}
  65 +
  66 +@Component({
  67 + selector: 'mat-fab-toolbar',
  68 + templateUrl: './fab-toolbar.component.html',
  69 + styleUrls: ['./fab-toolbar.component.scss'],
  70 + inputs: ['color'],
  71 + encapsulation: ViewEncapsulation.None
  72 +})
  73 +export class FabToolbarComponent extends MatFabToolbarMixinBase implements OnInit, OnDestroy, AfterViewInit, OnChanges {
  74 +
  75 + @Input()
  76 + isOpen: boolean;
  77 +
  78 + @Input()
  79 + direction: FabToolbarDirection;
  80 +
  81 + fabToolbarResizeListener = this.onFabToolbarResize.bind(this);
  82 +
  83 + constructor(private el: ElementRef<HTMLElement>,
  84 + @Inject(WINDOW) private window: Window) {
  85 + super(el);
  86 + }
  87 +
  88 + ngOnInit(): void {
  89 + const element = $(this.el.nativeElement);
  90 + element.addClass('mat-fab-toolbar');
  91 + element.find('mat-fab-trigger').find('button')
  92 + .prepend('<div class="mat-fab-toolbar-background"></div>');
  93 + element.addClass(`mat-${this.direction}`);
  94 + // @ts-ignore
  95 + addResizeListener(this.el.nativeElement, this.fabToolbarResizeListener);
  96 + }
  97 +
  98 + ngOnDestroy(): void {
  99 + // @ts-ignore
  100 + removeResizeListener(this.el.nativeElement, this.fabToolbarResizeListener);
  101 + }
  102 +
  103 + ngAfterViewInit(): void {
  104 + this.triggerOpenClose(true);
  105 + }
  106 +
  107 + ngOnChanges(changes: SimpleChanges): void {
  108 + for (const propName of Object.keys(changes)) {
  109 + const change = changes[propName];
  110 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  111 + if (propName === 'isOpen') {
  112 + this.triggerOpenClose();
  113 + }
  114 + }
  115 + }
  116 + }
  117 +
  118 + private onFabToolbarResize() {
  119 + if (this.isOpen) {
  120 + this.triggerOpenClose(true);
  121 + }
  122 + }
  123 +
  124 + private triggerOpenClose(disableAnimation?: boolean): void {
  125 + const el = this.el.nativeElement;
  126 + const element = $(this.el.nativeElement);
  127 + if (disableAnimation) {
  128 + element.removeClass('mat-animation');
  129 + } else {
  130 + element.addClass('mat-animation');
  131 + }
  132 + const backgroundElement: HTMLElement = el.querySelector('.mat-fab-toolbar-background');
  133 + const triggerElement: HTMLElement = el.querySelector('mat-fab-trigger button');
  134 + const toolbarElement: HTMLElement = el.querySelector('mat-toolbar');
  135 + const iconElement: HTMLElement = el.querySelector('mat-fab-trigger button mat-icon');
  136 + const actions = element.find('mat-fab-actions').children();
  137 + if (triggerElement && backgroundElement) {
  138 + const width = el.offsetWidth;
  139 + const height = el.offsetHeight;
  140 + const scale = 2 * (width / triggerElement.offsetWidth);
  141 +
  142 + backgroundElement.style.borderRadius = width + 'px';
  143 +
  144 + if (this.isOpen) {
  145 + element.addClass('mat-is-open');
  146 + toolbarElement.style.pointerEvents = 'inherit';
  147 +
  148 + backgroundElement.style.width = triggerElement.offsetWidth + 'px';
  149 + backgroundElement.style.height = triggerElement.offsetHeight + 'px';
  150 + backgroundElement.style.transform = 'scale(' + scale + ')';
  151 +
  152 + backgroundElement.style.transitionDelay = '0ms';
  153 + if (iconElement) {
  154 + iconElement.style.transitionDelay = disableAnimation ? '0ms' : '.3s';
  155 + }
  156 +
  157 + actions.each((index, action) => {
  158 + action.style.transitionDelay = disableAnimation ? '0ms' : ((actions.length - index) * 25 + 'ms');
  159 + });
  160 +
  161 + } else {
  162 + element.removeClass('mat-is-open');
  163 + toolbarElement.style.pointerEvents = 'none';
  164 +
  165 + backgroundElement.style.transform = 'scale(1)';
  166 +
  167 + backgroundElement.style.top = '0';
  168 +
  169 + if (element.hasClass('mat-right')) {
  170 + backgroundElement.style.left = '0';
  171 + backgroundElement.style.right = null;
  172 + }
  173 +
  174 + if (element.hasClass('mat-left')) {
  175 + backgroundElement.style.right = '0';
  176 + backgroundElement.style.left = null;
  177 + }
  178 +
  179 + backgroundElement.style.transitionDelay = disableAnimation ? '0ms' : '200ms';
  180 +
  181 + actions.each((index, action) => {
  182 + action.style.transitionDelay = (disableAnimation ? 0 : 200) + (index * 25) + 'ms';
  183 + });
  184 + }
  185 + }
  186 + }
  187 +
  188 +}
... ...
  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 { EntityType } from '@shared/models/entity-type.models';
  18 +
  19 +export enum AliasFilterType {
  20 + singleEntity = 'singleEntity',
  21 + entityList = 'entityList',
  22 + entityName = 'entityName',
  23 + stateEntity = 'stateEntity',
  24 + assetType = 'assetType',
  25 + deviceType = 'deviceType',
  26 + entityViewType = 'entityViewType',
  27 + relationsQuery = 'relationsQuery',
  28 + assetSearchQuery = 'assetSearchQuery',
  29 + deviceSearchQuery = 'deviceSearchQuery',
  30 + entityViewSearchQuery = 'entityViewSearchQuery'
  31 +}
  32 +
  33 +export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>(
  34 + [
  35 + [ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ],
  36 + [ AliasFilterType.entityList, 'alias.filter-type-entity-list' ],
  37 + [ AliasFilterType.entityName, 'alias.filter-type-entity-name' ],
  38 + [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ],
  39 + [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ],
  40 + [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ],
  41 + [ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ],
  42 + [ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ],
  43 + [ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ],
  44 + [ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ],
  45 + [ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ]
  46 + ]
  47 +);
  48 +
  49 +export interface EntityAliasFilter {
  50 + type: AliasFilterType;
  51 + entityType: EntityType;
  52 + resolveMultiple: boolean;
  53 + entityList?: string[];
  54 + entityNameFilter?: string;
  55 + [key: string]: any;
  56 + // TODO:
  57 +
  58 +}
  59 +
  60 +export interface EntityAlias {
  61 + id: string;
  62 + alias: string;
  63 + filter: EntityAliasFilter;
  64 + [key: string]: any;
  65 + // TODO:
  66 +}
  67 +
  68 +export interface EntityAliases {
  69 + [id: string]: EntityAlias;
  70 +}
... ...
... ... @@ -14,22 +14,26 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {BaseData} from '@shared/models/base-data';
18   -import {DashboardId} from '@shared/models/id/dashboard-id';
19   -import {TenantId} from '@shared/models/id/tenant-id';
20   -import {ShortCustomerInfo} from '@shared/models/customer.model';
  17 +import { BaseData } from '@shared/models/base-data';
  18 +import { DashboardId } from '@shared/models/id/dashboard-id';
  19 +import { TenantId } from '@shared/models/id/tenant-id';
  20 +import { ShortCustomerInfo } from '@shared/models/customer.model';
  21 +import { Widget } from './widget.models';
  22 +import { Timewindow } from '@shared/models/time/time.models';
  23 +import { EntityType } from '@shared/models/entity-type.models';
  24 +import { EntityAlias, EntityAliases } from './alias.models';
21 25
22 26 export interface DashboardInfo extends BaseData<DashboardId> {
23   - tenantId: TenantId;
24   - title: string;
25   - assignedCustomers: Array<ShortCustomerInfo>;
  27 + tenantId?: TenantId;
  28 + title?: string;
  29 + assignedCustomers?: Array<ShortCustomerInfo>;
26 30 }
27 31
28 32 export interface WidgetLayout {
29 33 sizeX: number;
30 34 sizeY: number;
31   - mobileHeight: number;
32   - mobileOrder: number;
  35 + mobileHeight?: number;
  36 + mobileOrder?: number;
33 37 col: number;
34 38 row: number;
35 39 }
... ... @@ -38,13 +42,59 @@ export interface WidgetLayouts {
38 42 [id: string]: WidgetLayout;
39 43 }
40 44
  45 +export interface GridSettings {
  46 + backgroundColor?: string;
  47 + color?: string;
  48 + columns?: number;
  49 + margins?: [number, number];
  50 + backgroundSizeMode?: string;
  51 + [key: string]: any;
  52 + // TODO:
  53 +}
  54 +
  55 +export interface DashboardLayout {
  56 + widgets: WidgetLayouts;
  57 + gridSettings: GridSettings;
  58 +}
  59 +
  60 +export declare type DashboardLayoutId = 'main' | 'right';
  61 +
  62 +export interface DashboardStateLayouts {
  63 + main?: DashboardLayout;
  64 + right?: DashboardLayout;
  65 +}
  66 +
  67 +export interface DashboardState {
  68 + name: string;
  69 + root: boolean;
  70 + layouts: DashboardStateLayouts;
  71 +}
  72 +
  73 +export declare type StateControllerId = 'entity' | 'default' | string;
  74 +
  75 +export interface DashboardSettings {
  76 + stateControllerId?: StateControllerId;
  77 + showTitle?: boolean;
  78 + showDashboardsSelect?: boolean;
  79 + showEntitiesSelect?: boolean;
  80 + showDashboardTimewindow?: boolean;
  81 + showDashboardExport?: boolean;
  82 + toolbarAlwaysOpen?: boolean;
  83 + titleColor?: string;
  84 +}
  85 +
41 86 export interface DashboardConfiguration {
  87 + timewindow?: Timewindow;
  88 + settings?: DashboardSettings;
  89 + widgets?: {[id: string]: Widget } | Widget[];
  90 + states?: {[id: string]: DashboardState };
  91 + entityAliases?: EntityAliases;
42 92 [key: string]: any;
43 93 // TODO:
44 94 }
45 95
46 96 export interface Dashboard extends DashboardInfo {
47   - configuration: DashboardConfiguration;
  97 + configuration?: DashboardConfiguration;
48 98 }
49 99
50 100 export function isPublicDashboard(dashboard: DashboardInfo): boolean {
... ...
  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 { BaseData } from '@shared/models/base-data';
  18 +import { EntityType } from '@shared/models/entity-type.models';
  19 +import { EntityId } from '@shared/models/id/entity-id';
  20 +
  21 +export interface EntityInfo {
  22 + origEntity?: BaseData<EntityId>;
  23 + name?: string;
  24 + label?: string;
  25 + entityType?: EntityType;
  26 + id?: string;
  27 + entityDescription?: string;
  28 +}
... ...
... ... @@ -38,6 +38,8 @@ export interface WidgetTypeTemplate {
38 38
39 39 export interface WidgetTypeData {
40 40 name: string;
  41 + icon: string;
  42 + isMdiIcon?: boolean;
41 43 template: WidgetTypeTemplate;
42 44 }
43 45
... ... @@ -47,6 +49,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
47 49 widgetType.timeseries,
48 50 {
49 51 name: 'widget.timeseries',
  52 + icon: 'timeline',
50 53 template: {
51 54 bundleAlias: 'charts',
52 55 alias: 'basic_timeseries'
... ... @@ -57,6 +60,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
57 60 widgetType.latest,
58 61 {
59 62 name: 'widget.latest-values',
  63 + icon: 'track_changes',
60 64 template: {
61 65 bundleAlias: 'cards',
62 66 alias: 'attributes_card'
... ... @@ -67,6 +71,8 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
67 71 widgetType.rpc,
68 72 {
69 73 name: 'widget.rpc',
  74 + icon: 'mdi:developer-board',
  75 + isMdiIcon: true,
70 76 template: {
71 77 bundleAlias: 'gpio_widgets',
72 78 alias: 'basic_gpio_control'
... ... @@ -77,6 +83,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
77 83 widgetType.alarm,
78 84 {
79 85 name: 'widget.alarm',
  86 + icon: 'error',
80 87 template: {
81 88 bundleAlias: 'alarm_widgets',
82 89 alias: 'alarms_table'
... ... @@ -87,6 +94,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
87 94 widgetType.static,
88 95 {
89 96 name: 'widget.static',
  97 + icon: 'font_download',
90 98 template: {
91 99 bundleAlias: 'cards',
92 100 alias: 'html_card'
... ... @@ -214,7 +222,7 @@ export enum DatasourceType {
214 222 }
215 223
216 224 export interface Datasource {
217   - type: DatasourceType;
  225 + type?: DatasourceType | any;
218 226 name?: string;
219 227 dataKeys?: Array<DataKey>;
220 228 entityType?: EntityType;
... ... @@ -333,7 +341,7 @@ export interface WidgetConfig {
333 341
334 342 export interface Widget {
335 343 id?: string;
336   - typeId: WidgetTypeId;
  344 + typeId?: WidgetTypeId;
337 345 isSystemType: boolean;
338 346 bundleAlias: string;
339 347 typeAlias: string;
... ...
... ... @@ -14,9 +14,9 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -export type WindowMessageType = 'widgetException';
  17 +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated';
18 18
19 19 export interface WindowMessage {
20 20 type: WindowMessageType;
21   - data: any;
  21 + data?: any;
22 22 }
... ...
... ... @@ -14,12 +14,12 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {NgModule} from '@angular/core';
18   -import {CommonModule, DatePipe} from '@angular/common';
19   -import {FooterComponent} from './components/footer.component';
20   -import {LogoComponent} from './components/logo.component';
21   -import {TbSnackBarComponent, ToastDirective} from './components/toast.directive';
22   -import {BreadcrumbComponent} from '@app/shared/components/breadcrumb.component';
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule, DatePipe } from '@angular/common';
  19 +import { FooterComponent } from './components/footer.component';
  20 +import { LogoComponent } from './components/logo.component';
  21 +import { TbSnackBarComponent, ToastDirective } from './components/toast.directive';
  22 +import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component';
23 23
24 24 import {
25 25 MatAutocompleteModule,
... ... @@ -51,43 +51,49 @@ import {
51 51 MatToolbarModule,
52 52 MatTooltipModule
53 53 } from '@angular/material';
54   -import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core';
55   -import {GridsterModule} from 'angular-gridster2';
56   -import {FlexLayoutModule} from '@angular/flex-layout';
57   -import {FormsModule, ReactiveFormsModule} from '@angular/forms';
58   -import {RouterModule} from '@angular/router';
59   -import {ShareModule as ShareButtonsModule} from '@ngx-share/core';
60   -import {UserMenuComponent} from '@shared/components/user-menu.component';
61   -import {NospacePipe} from './pipe/nospace.pipe';
62   -import {TranslateModule} from '@ngx-translate/core';
63   -import {TbCheckboxComponent} from '@shared/components/tb-checkbox.component';
64   -import {HelpComponent} from '@shared/components/help.component';
65   -import {TbAnchorComponent} from '@shared/components/tb-anchor.component';
66   -import {MillisecondsToTimeStringPipe} from '@shared/pipe/milliseconds-to-time-string.pipe';
67   -import {TimewindowComponent} from '@shared/components/time/timewindow.component';
68   -import {OverlayModule} from '@angular/cdk/overlay';
69   -import {TimewindowPanelComponent} from '@shared/components/time/timewindow-panel.component';
70   -import {TimeintervalComponent} from '@shared/components/time/timeinterval.component';
71   -import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component';
72   -import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe';
73   -import {ClipboardModule} from 'ngx-clipboard';
  54 +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
  55 +import { GridsterModule } from 'angular-gridster2';
  56 +import { FlexLayoutModule } from '@angular/flex-layout';
  57 +import { FormsModule, ReactiveFormsModule } from '@angular/forms';
  58 +import { RouterModule } from '@angular/router';
  59 +import { ShareModule as ShareButtonsModule } from '@ngx-share/core';
  60 +import { HotkeyModule } from 'angular2-hotkeys';
  61 +import { UserMenuComponent } from '@shared/components/user-menu.component';
  62 +import { NospacePipe } from './pipe/nospace.pipe';
  63 +import { TranslateModule } from '@ngx-translate/core';
  64 +import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
  65 +import { HelpComponent } from '@shared/components/help.component';
  66 +import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
  67 +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
  68 +import { TimewindowComponent } from '@shared/components/time/timewindow.component';
  69 +import { OverlayModule } from '@angular/cdk/overlay';
  70 +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
  71 +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component';
  72 +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component';
  73 +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
  74 +import { ClipboardModule } from 'ngx-clipboard';
74 75 import { ValueInputComponent } from '@shared/components/value-input.component';
75   -import {FullscreenDirective} from '@shared/components/fullscreen.directive';
76   -import {HighlightPipe} from '@shared/pipe/highlight.pipe';
77   -import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
78   -import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component';
79   -import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component';
80   -import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component';
81   -import {EntityListComponent} from '@shared/components/entity/entity-list.component';
82   -import {EntityTypeSelectComponent} from './components/entity/entity-type-select.component';
83   -import {EntitySelectComponent} from './components/entity/entity-select.component';
84   -import {DatetimeComponent} from '@shared/components/time/datetime.component';
85   -import {EntityKeysListComponent} from './components/entity/entity-keys-list.component';
86   -import {SocialSharePanelComponent} from './components/socialshare-panel.component';
  76 +import { FullscreenDirective } from '@shared/components/fullscreen.directive';
  77 +import { HighlightPipe } from '@shared/pipe/highlight.pipe';
  78 +import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
  79 +import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component';
  80 +import { EntitySubTypeSelectComponent } from './components/entity/entity-subtype-select.component';
  81 +import { EntityAutocompleteComponent } from './components/entity/entity-autocomplete.component';
  82 +import { EntityListComponent } from '@shared/components/entity/entity-list.component';
  83 +import { EntityTypeSelectComponent } from './components/entity/entity-type-select.component';
  84 +import { EntitySelectComponent } from './components/entity/entity-select.component';
  85 +import { DatetimeComponent } from '@shared/components/time/datetime.component';
  86 +import { EntityKeysListComponent } from './components/entity/entity-keys-list.component';
  87 +import { SocialSharePanelComponent } from './components/socialshare-panel.component';
87 88 import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
88 89 import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
89 90 import { JsonObjectEditComponent } from './components/json-object-edit.component';
90 91 import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
  92 +import { CircularProgressDirective } from './components/circular-progress.directive';
  93 +import { MatSpinner } from '@angular/material/progress-spinner';
  94 +import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component';
  95 +import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
  96 +import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
91 97
92 98 @NgModule({
93 99 providers: [
... ... @@ -100,6 +106,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
100 106 TbSnackBarComponent,
101 107 TbAnchorComponent,
102 108 TimewindowPanelComponent,
  109 + DashboardSelectPanelComponent,
  110 + MatSpinner
103 111 ],
104 112 declarations: [
105 113 FooterComponent,
... ... @@ -107,6 +115,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
107 115 FooterFabButtonsComponent,
108 116 ToastDirective,
109 117 FullscreenDirective,
  118 + CircularProgressDirective,
110 119 TbAnchorComponent,
111 120 HelpComponent,
112 121 TbCheckboxComponent,
... ... @@ -116,6 +125,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
116 125 TimewindowComponent,
117 126 TimewindowPanelComponent,
118 127 TimeintervalComponent,
  128 + DashboardSelectComponent,
  129 + DashboardSelectPanelComponent,
119 130 DatetimePeriodComponent,
120 131 DatetimeComponent,
121 132 ValueInputComponent,
... ... @@ -131,6 +142,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
131 142 RelationTypeAutocompleteComponent,
132 143 SocialSharePanelComponent,
133 144 JsonObjectEditComponent,
  145 + FabTriggerDirective,
  146 + FabActionsDirective,
  147 + FabToolbarComponent,
134 148 NospacePipe,
135 149 MillisecondsToTimeStringPipe,
136 150 EnumToArrayPipe,
... ... @@ -176,7 +190,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
176 190 FormsModule,
177 191 ReactiveFormsModule,
178 192 OverlayModule,
179   - ShareButtonsModule
  193 + ShareButtonsModule,
  194 + HotkeyModule
180 195 ],
181 196 exports: [
182 197 FooterComponent,
... ... @@ -184,6 +199,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
184 199 FooterFabButtonsComponent,
185 200 ToastDirective,
186 201 FullscreenDirective,
  202 + CircularProgressDirective,
187 203 TbAnchorComponent,
188 204 HelpComponent,
189 205 TbCheckboxComponent,
... ... @@ -192,6 +208,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
192 208 TimewindowComponent,
193 209 TimewindowPanelComponent,
194 210 TimeintervalComponent,
  211 + DashboardSelectComponent,
195 212 DatetimePeriodComponent,
196 213 DatetimeComponent,
197 214 DashboardAutocompleteComponent,
... ... @@ -206,6 +223,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
206 223 RelationTypeAutocompleteComponent,
207 224 SocialSharePanelComponent,
208 225 JsonObjectEditComponent,
  226 + FabTriggerDirective,
  227 + FabActionsDirective,
  228 + FabToolbarComponent,
209 229 ValueInputComponent,
210 230 MatButtonModule,
211 231 MatCheckboxModule,
... ... @@ -244,6 +264,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
244 264 ReactiveFormsModule,
245 265 OverlayModule,
246 266 ShareButtonsModule,
  267 + HotkeyModule,
247 268 NospacePipe,
248 269 MillisecondsToTimeStringPipe,
249 270 EnumToArrayPipe,
... ...
... ... @@ -169,6 +169,25 @@ section.tb-header-buttons {
169 169 }
170 170 }
171 171
  172 +section.tb-footer-buttons {
  173 + position: fixed;
  174 + right: 20px;
  175 + bottom: 20px;
  176 + z-index: 30;
  177 + pointer-events: none;
  178 +
  179 + .tb-btn-footer {
  180 + margin: 6px 8px;
  181 + position: relative !important;
  182 + display: inline-block !important;
  183 + animation: tbMoveFromBottomFade .3s ease both;
  184 + &.tb-hide {
  185 + animation: tbMoveToBottomFade .3s ease both;
  186 + }
  187 + }
  188 +}
  189 +
  190 +
172 191 .tb-details-buttons {
173 192 button {
174 193 margin: 6px 8px;
... ... @@ -293,7 +312,7 @@ pre.tb-highlight {
293 312 }
294 313
295 314 .tb-fullscreen-parent {
296   - background: #fff;
  315 + background: #eee;
297 316 }
298 317
299 318 mat-label {
... ...
... ... @@ -166,9 +166,52 @@ $tb-dark-theme: get-tb-dark-theme(
166 166 $tb-accent
167 167 );
168 168
  169 +@mixin mat-fab-toolbar-theme($theme) {
  170 + $primary: map-get($theme, primary);
  171 + $accent: map-get($theme, accent);
  172 + $warn: map-get($theme, warn);
  173 + $background: map-get($theme, background);
  174 + $foreground: map-get($theme, foreground);
  175 +
  176 + mat-fab-toolbar {
  177 + .mat-fab-toolbar-background {
  178 + background: mat-color($background, app-bar);
  179 + color: mat-color($foreground, text);
  180 + }
  181 + &.mat-primary {
  182 + .mat-fab-toolbar-background {
  183 + @include _mat-toolbar-color($primary);
  184 + }
  185 + }
  186 + &.mat-accent {
  187 + .mat-fab-toolbar-background {
  188 + @include _mat-toolbar-color($accent);
  189 + }
  190 + }
  191 + &.mat-warn {
  192 + .mat-fab-toolbar-background {
  193 + @include _mat-toolbar-color($warn);
  194 + }
  195 + }
  196 + }
  197 +}
  198 +
  199 +@mixin tb-components-theme($theme) {
  200 + $primary: map-get($theme, primary);
  201 +
  202 + mat-toolbar{
  203 + &.mat-hue-3 {
  204 + background-color: mat-color($primary, 'A100');
  205 + }
  206 + }
  207 +
  208 + @include mat-fab-toolbar-theme($tb-theme);
  209 +}
  210 +
169 211 .tb-default {
170 212 @include angular-material-theme($tb-theme);
171 213 @include mat-datetimepicker-theme($tb-theme);
  214 + @include tb-components-theme($tb-theme);
172 215 }
173 216
174 217 .tb-dark {
... ... @@ -388,11 +431,13 @@ $tb-dark-theme: get-tb-dark-theme(
388 431 width: 32px;
389 432 height: 32px;
390 433 line-height: 32px;
  434 + padding: 0 !important;
391 435 }
392 436 &.tb-mat-96 {
393 437 width: 96px;
394 438 height: 96px;
395 439 line-height: 96px;
  440 + padding: 0 !important;
396 441 }
397 442 }
398 443
... ... @@ -549,6 +594,27 @@ $tb-dark-theme: get-tb-dark-theme(
549 594 right: 0;
550 595 }
551 596
  597 + .tb-progress-cover {
  598 + position: absolute;
  599 + top: 0;
  600 + right: 0;
  601 + bottom: 0;
  602 + left: 0;
  603 + z-index: 6;
  604 + background-color: #eee;
  605 + opacity: 1;
  606 + }
  607 +
  608 + .mat-button.tb-fullscreen-button-style,
  609 + .tb-fullscreen-button-style {
  610 + background: #ccc;
  611 + opacity: .85;
  612 +
  613 + mat-icon {
  614 + color: #666;
  615 + }
  616 + }
  617 +
552 618 span.no-data-found {
553 619 position: relative;
554 620 display: flex;
... ...
... ... @@ -2,7 +2,7 @@
2 2 "extends": "../tsconfig.json",
3 3 "compilerOptions": {
4 4 "outDir": "../out-tsc/app",
5   - "types": ["node", "jquery", "flot", "tinycolor2"]
  5 + "types": ["node", "jquery", "flot", "tinycolor2", "js-beautify"]
6 6 },
7 7 "exclude": [
8 8 "test.ts",
... ...
  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 +interface SplitOptions {
  18 + sizes?: number[];
  19 + minSize?: number[] | number;
  20 + gutterSize?: number;
  21 + snapOffset?: number;
  22 + direction?: 'horizontal' | 'vertical';
  23 + cursor?: 'col-resize' | 'row-resize';
  24 + gutter?: (index: number, direction: string) => HTMLElement;
  25 + elementStyle?: (dimension: string, elementSize: number, gutterSize: number) => any;
  26 + gutterStyle?: (dimension: string, gutterSize: number) => any;
  27 + onDrag?: () => void;
  28 + onDragStart?: () => void;
  29 + onDragEnd?: () => void;
  30 +}
  31 +
  32 +interface SplitObject {
  33 + setSizes: (sizes: number[]) => void;
  34 + getSizes: () => number[];
  35 + collapse: (index: number) => void;
  36 + destroy: () => void;
  37 +}
  38 +
  39 +declare function Split(elements: HTMLElement | string[], options?: SplitOptions): SplitObject;
... ...
... ... @@ -14,7 +14,8 @@
14 14 "typeRoots": [
15 15 "node_modules/@types",
16 16 "src/typings/jquery.typings.d.ts",
17   - "src/typings/jquery.flot.typings.d.ts"
  17 + "src/typings/jquery.flot.typings.d.ts",
  18 + "src/typings/split.js.typings.d.ts"
18 19 ],
19 20 "paths": {
20 21 "@app/*": ["src/app/*"],
... ...