Commit 700293d5f9c51f5062cb28bd9d1d37afbc83cb4c
1 parent
d29a8731
Implement Widget Editor. Dashboard page initial implementation.
Showing
55 changed files
with
3915 additions
and
128 deletions
Too many changes to show.
To preserve performance only 55 of 74 files are displayed.
... | ... | @@ -47,6 +47,10 @@ |
47 | 47 | "node_modules/flot/src/plugins/jquery.flot.stack.js", |
48 | 48 | "node_modules/flot.curvedlines/curvedLines.js", |
49 | 49 | "node_modules/tinycolor2/dist/tinycolor-min.js", |
50 | + "node_modules/split.js/dist/split.js", | |
51 | + "node_modules/js-beautify/js/lib/beautify.js", | |
52 | + "node_modules/js-beautify/js/lib/beautify-css.js", | |
53 | + "node_modules/js-beautify/js/lib/beautify-html.js", | |
50 | 54 | "node_modules/ace-builds/src-min/ace.js", |
51 | 55 | "node_modules/ace-builds/src-min/ext-language_tools.js", |
52 | 56 | "node_modules/ace-builds/src-min/ext-searchbox.js", | ... | ... |
... | ... | @@ -1170,12 +1170,23 @@ |
1170 | 1170 | "@types/sizzle": "*" |
1171 | 1171 | } |
1172 | 1172 | }, |
1173 | + "@types/js-beautify": { | |
1174 | + "version": "1.8.1", | |
1175 | + "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.8.1.tgz", | |
1176 | + "integrity": "sha512-B1Br8yE27obcYvFx5ECZswT/947aAFNb9lHqnkUOhtOfvJqaa6Axibo4T+5G6iQlUfjgSd8am9R/9j9UBfRlrw==", | |
1177 | + "dev": true | |
1178 | + }, | |
1173 | 1179 | "@types/minimatch": { |
1174 | 1180 | "version": "3.0.3", |
1175 | 1181 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", |
1176 | 1182 | "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", |
1177 | 1183 | "dev": true |
1178 | 1184 | }, |
1185 | + "@types/mousetrap": { | |
1186 | + "version": "1.6.3", | |
1187 | + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz", | |
1188 | + "integrity": "sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew==" | |
1189 | + }, | |
1179 | 1190 | "@types/node": { |
1180 | 1191 | "version": "10.14.15", |
1181 | 1192 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", |
... | ... | @@ -1434,6 +1445,11 @@ |
1434 | 1445 | "through": ">=2.2.7 <3" |
1435 | 1446 | } |
1436 | 1447 | }, |
1448 | + "abbrev": { | |
1449 | + "version": "1.1.1", | |
1450 | + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", | |
1451 | + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" | |
1452 | + }, | |
1437 | 1453 | "accepts": { |
1438 | 1454 | "version": "1.3.7", |
1439 | 1455 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", |
... | ... | @@ -1523,6 +1539,15 @@ |
1523 | 1539 | "tslib": "^1.9.0" |
1524 | 1540 | } |
1525 | 1541 | }, |
1542 | + "angular2-hotkeys": { | |
1543 | + "version": "2.1.5", | |
1544 | + "resolved": "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz", | |
1545 | + "integrity": "sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA==", | |
1546 | + "requires": { | |
1547 | + "@types/mousetrap": "^1.6.0", | |
1548 | + "mousetrap": "^1.6.0" | |
1549 | + } | |
1550 | + }, | |
1526 | 1551 | "ansi-colors": { |
1527 | 1552 | "version": "3.2.4", |
1528 | 1553 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", |
... | ... | @@ -1955,8 +1980,7 @@ |
1955 | 1980 | "balanced-match": { |
1956 | 1981 | "version": "1.0.0", |
1957 | 1982 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", |
1958 | - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", | |
1959 | - "dev": true | |
1983 | + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" | |
1960 | 1984 | }, |
1961 | 1985 | "base": { |
1962 | 1986 | "version": "0.11.2", |
... | ... | @@ -2143,7 +2167,6 @@ |
2143 | 2167 | "version": "1.1.11", |
2144 | 2168 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", |
2145 | 2169 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", |
2146 | - "dev": true, | |
2147 | 2170 | "requires": { |
2148 | 2171 | "balanced-match": "^1.0.0", |
2149 | 2172 | "concat-map": "0.0.1" |
... | ... | @@ -2854,8 +2877,7 @@ |
2854 | 2877 | "commander": { |
2855 | 2878 | "version": "2.20.0", |
2856 | 2879 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", |
2857 | - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", | |
2858 | - "dev": true | |
2880 | + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" | |
2859 | 2881 | }, |
2860 | 2882 | "commondir": { |
2861 | 2883 | "version": "1.0.1", |
... | ... | @@ -3107,8 +3129,7 @@ |
3107 | 3129 | "concat-map": { |
3108 | 3130 | "version": "0.0.1", |
3109 | 3131 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |
3110 | - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", | |
3111 | - "dev": true | |
3132 | + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" | |
3112 | 3133 | }, |
3113 | 3134 | "concat-stream": { |
3114 | 3135 | "version": "1.6.2", |
... | ... | @@ -3122,6 +3143,15 @@ |
3122 | 3143 | "typedarray": "^0.0.6" |
3123 | 3144 | } |
3124 | 3145 | }, |
3146 | + "config-chain": { | |
3147 | + "version": "1.1.12", | |
3148 | + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", | |
3149 | + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", | |
3150 | + "requires": { | |
3151 | + "ini": "^1.3.4", | |
3152 | + "proto-list": "~1.2.1" | |
3153 | + } | |
3154 | + }, | |
3125 | 3155 | "connect": { |
3126 | 3156 | "version": "3.7.0", |
3127 | 3157 | "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", |
... | ... | @@ -3762,6 +3792,17 @@ |
3762 | 3792 | "safer-buffer": "^2.1.0" |
3763 | 3793 | } |
3764 | 3794 | }, |
3795 | + "editorconfig": { | |
3796 | + "version": "0.15.3", | |
3797 | + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", | |
3798 | + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", | |
3799 | + "requires": { | |
3800 | + "commander": "^2.19.0", | |
3801 | + "lru-cache": "^4.1.5", | |
3802 | + "semver": "^5.6.0", | |
3803 | + "sigmund": "^1.0.1" | |
3804 | + } | |
3805 | + }, | |
3765 | 3806 | "ee-first": { |
3766 | 3807 | "version": "1.1.1", |
3767 | 3808 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", |
... | ... | @@ -4574,8 +4615,7 @@ |
4574 | 4615 | "fs.realpath": { |
4575 | 4616 | "version": "1.0.0", |
4576 | 4617 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", |
4577 | - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", | |
4578 | - "dev": true | |
4618 | + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" | |
4579 | 4619 | }, |
4580 | 4620 | "fsevents": { |
4581 | 4621 | "version": "1.2.9", |
... | ... | @@ -5183,7 +5223,6 @@ |
5183 | 5223 | "version": "7.1.3", |
5184 | 5224 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", |
5185 | 5225 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", |
5186 | - "dev": true, | |
5187 | 5226 | "requires": { |
5188 | 5227 | "fs.realpath": "^1.0.0", |
5189 | 5228 | "inflight": "^1.0.4", |
... | ... | @@ -5709,7 +5748,6 @@ |
5709 | 5748 | "version": "1.0.6", |
5710 | 5749 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", |
5711 | 5750 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", |
5712 | - "dev": true, | |
5713 | 5751 | "requires": { |
5714 | 5752 | "once": "^1.3.0", |
5715 | 5753 | "wrappy": "1" |
... | ... | @@ -5718,14 +5756,12 @@ |
5718 | 5756 | "inherits": { |
5719 | 5757 | "version": "2.0.4", |
5720 | 5758 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
5721 | - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", | |
5722 | - "dev": true | |
5759 | + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" | |
5723 | 5760 | }, |
5724 | 5761 | "ini": { |
5725 | 5762 | "version": "1.3.5", |
5726 | 5763 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", |
5727 | - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", | |
5728 | - "dev": true | |
5764 | + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" | |
5729 | 5765 | }, |
5730 | 5766 | "inquirer": { |
5731 | 5767 | "version": "6.5.0", |
... | ... | @@ -6444,6 +6480,18 @@ |
6444 | 6480 | } |
6445 | 6481 | } |
6446 | 6482 | }, |
6483 | + "js-beautify": { | |
6484 | + "version": "1.10.2", | |
6485 | + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz", | |
6486 | + "integrity": "sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==", | |
6487 | + "requires": { | |
6488 | + "config-chain": "^1.1.12", | |
6489 | + "editorconfig": "^0.15.3", | |
6490 | + "glob": "^7.1.3", | |
6491 | + "mkdirp": "~0.5.1", | |
6492 | + "nopt": "~4.0.1" | |
6493 | + } | |
6494 | + }, | |
6447 | 6495 | "js-tokens": { |
6448 | 6496 | "version": "3.0.2", |
6449 | 6497 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", |
... | ... | @@ -6918,7 +6966,6 @@ |
6918 | 6966 | "version": "4.1.5", |
6919 | 6967 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", |
6920 | 6968 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", |
6921 | - "dev": true, | |
6922 | 6969 | "requires": { |
6923 | 6970 | "pseudomap": "^1.0.2", |
6924 | 6971 | "yallist": "^2.1.2" |
... | ... | @@ -7258,7 +7305,6 @@ |
7258 | 7305 | "version": "3.0.4", |
7259 | 7306 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", |
7260 | 7307 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", |
7261 | - "dev": true, | |
7262 | 7308 | "requires": { |
7263 | 7309 | "brace-expansion": "^1.1.7" |
7264 | 7310 | } |
... | ... | @@ -7368,7 +7414,6 @@ |
7368 | 7414 | "version": "0.5.1", |
7369 | 7415 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", |
7370 | 7416 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", |
7371 | - "dev": true, | |
7372 | 7417 | "requires": { |
7373 | 7418 | "minimist": "0.0.8" |
7374 | 7419 | }, |
... | ... | @@ -7376,8 +7421,7 @@ |
7376 | 7421 | "minimist": { |
7377 | 7422 | "version": "0.0.8", |
7378 | 7423 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", |
7379 | - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", | |
7380 | - "dev": true | |
7424 | + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" | |
7381 | 7425 | } |
7382 | 7426 | } |
7383 | 7427 | }, |
... | ... | @@ -7386,6 +7430,11 @@ |
7386 | 7430 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", |
7387 | 7431 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" |
7388 | 7432 | }, |
7433 | + "mousetrap": { | |
7434 | + "version": "1.6.3", | |
7435 | + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", | |
7436 | + "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" | |
7437 | + }, | |
7389 | 7438 | "move-concurrently": { |
7390 | 7439 | "version": "1.0.1", |
7391 | 7440 | "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", |
... | ... | @@ -7563,6 +7612,15 @@ |
7563 | 7612 | "semver": "^5.3.0" |
7564 | 7613 | } |
7565 | 7614 | }, |
7615 | + "nopt": { | |
7616 | + "version": "4.0.1", | |
7617 | + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", | |
7618 | + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", | |
7619 | + "requires": { | |
7620 | + "abbrev": "1", | |
7621 | + "osenv": "^0.1.4" | |
7622 | + } | |
7623 | + }, | |
7566 | 7624 | "normalize-package-data": { |
7567 | 7625 | "version": "2.5.0", |
7568 | 7626 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", |
... | ... | @@ -7801,7 +7859,6 @@ |
7801 | 7859 | "version": "1.4.0", |
7802 | 7860 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", |
7803 | 7861 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", |
7804 | - "dev": true, | |
7805 | 7862 | "requires": { |
7806 | 7863 | "wrappy": "1" |
7807 | 7864 | } |
... | ... | @@ -7877,8 +7934,7 @@ |
7877 | 7934 | "os-homedir": { |
7878 | 7935 | "version": "1.0.2", |
7879 | 7936 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", |
7880 | - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", | |
7881 | - "dev": true | |
7937 | + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" | |
7882 | 7938 | }, |
7883 | 7939 | "os-locale": { |
7884 | 7940 | "version": "3.1.0", |
... | ... | @@ -7894,14 +7950,12 @@ |
7894 | 7950 | "os-tmpdir": { |
7895 | 7951 | "version": "1.0.2", |
7896 | 7952 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", |
7897 | - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", | |
7898 | - "dev": true | |
7953 | + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" | |
7899 | 7954 | }, |
7900 | 7955 | "osenv": { |
7901 | 7956 | "version": "0.1.5", |
7902 | 7957 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", |
7903 | 7958 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", |
7904 | - "dev": true, | |
7905 | 7959 | "requires": { |
7906 | 7960 | "os-homedir": "^1.0.0", |
7907 | 7961 | "os-tmpdir": "^1.0.0" |
... | ... | @@ -8222,8 +8276,7 @@ |
8222 | 8276 | "path-is-absolute": { |
8223 | 8277 | "version": "1.0.1", |
8224 | 8278 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", |
8225 | - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", | |
8226 | - "dev": true | |
8279 | + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" | |
8227 | 8280 | }, |
8228 | 8281 | "path-is-inside": { |
8229 | 8282 | "version": "1.0.2", |
... | ... | @@ -8465,6 +8518,11 @@ |
8465 | 8518 | "retry": "^0.10.0" |
8466 | 8519 | } |
8467 | 8520 | }, |
8521 | + "proto-list": { | |
8522 | + "version": "1.2.4", | |
8523 | + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", | |
8524 | + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" | |
8525 | + }, | |
8468 | 8526 | "protoduck": { |
8469 | 8527 | "version": "5.0.1", |
8470 | 8528 | "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", |
... | ... | @@ -8612,8 +8670,7 @@ |
8612 | 8670 | "pseudomap": { |
8613 | 8671 | "version": "1.0.2", |
8614 | 8672 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", |
8615 | - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", | |
8616 | - "dev": true | |
8673 | + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" | |
8617 | 8674 | }, |
8618 | 8675 | "psl": { |
8619 | 8676 | "version": "1.3.0", |
... | ... | @@ -9208,8 +9265,7 @@ |
9208 | 9265 | "semver": { |
9209 | 9266 | "version": "5.6.0", |
9210 | 9267 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", |
9211 | - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", | |
9212 | - "dev": true | |
9268 | + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" | |
9213 | 9269 | }, |
9214 | 9270 | "semver-dsl": { |
9215 | 9271 | "version": "1.0.1", |
... | ... | @@ -9408,6 +9464,11 @@ |
9408 | 9464 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", |
9409 | 9465 | "dev": true |
9410 | 9466 | }, |
9467 | + "sigmund": { | |
9468 | + "version": "1.0.1", | |
9469 | + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", | |
9470 | + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" | |
9471 | + }, | |
9411 | 9472 | "signal-exit": { |
9412 | 9473 | "version": "3.0.2", |
9413 | 9474 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", |
... | ... | @@ -9920,6 +9981,11 @@ |
9920 | 9981 | "extend-shallow": "^3.0.0" |
9921 | 9982 | } |
9922 | 9983 | }, |
9984 | + "split.js": { | |
9985 | + "version": "1.5.11", | |
9986 | + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.5.11.tgz", | |
9987 | + "integrity": "sha512-ec0sAbWnaMGpNHWo1ZgIlF3Mx7GzSyaO0GlcEBZGIFZQwYPPkbDV6JRpDmpzIshVig7USREuEPudy0ygQaskXg==" | |
9988 | + }, | |
9923 | 9989 | "sprintf-js": { |
9924 | 9990 | "version": "1.0.3", |
9925 | 9991 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", |
... | ... | @@ -11121,8 +11187,7 @@ |
11121 | 11187 | "wrappy": { |
11122 | 11188 | "version": "1.0.2", |
11123 | 11189 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
11124 | - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", | |
11125 | - "dev": true | |
11190 | + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" | |
11126 | 11191 | }, |
11127 | 11192 | "ws": { |
11128 | 11193 | "version": "3.3.3", |
... | ... | @@ -11180,8 +11245,7 @@ |
11180 | 11245 | "yallist": { |
11181 | 11246 | "version": "2.1.2", |
11182 | 11247 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", |
11183 | - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", | |
11184 | - "dev": true | |
11248 | + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" | |
11185 | 11249 | }, |
11186 | 11250 | "yargs": { |
11187 | 11251 | "version": "12.0.5", | ... | ... |
... | ... | @@ -33,6 +33,7 @@ |
33 | 33 | "@ngx-translate/http-loader": "^4.0.0", |
34 | 34 | "ace-builds": "^1.4.5", |
35 | 35 | "angular-gridster2": "^8.1.0", |
36 | + "angular2-hotkeys": "^2.1.5", | |
36 | 37 | "base64-js": "^1.3.1", |
37 | 38 | "compass-sass-mixins": "^0.12.7", |
38 | 39 | "core-js": "^3.1.4", |
... | ... | @@ -44,6 +45,7 @@ |
44 | 45 | "javascript-detect-element-resize": "^0.5.3", |
45 | 46 | "jquery": "^3.4.1", |
46 | 47 | "jquery.terminal": "^2.8.0", |
48 | + "js-beautify": "^1.10.2", | |
47 | 49 | "material-design-icons": "^3.0.1", |
48 | 50 | "messageformat": "^2.3.0", |
49 | 51 | "moment": "^2.24.0", |
... | ... | @@ -51,6 +53,7 @@ |
51 | 53 | "ngx-translate-messageformat-compiler": "^4.5.0", |
52 | 54 | "rxjs": "~6.5.2", |
53 | 55 | "screenfull": "^4.2.1", |
56 | + "split.js": "^1.5.11", | |
54 | 57 | "tinycolor2": "^1.4.1", |
55 | 58 | "tslib": "^1.10.0", |
56 | 59 | "typeface-roboto": "^0.0.75", |
... | ... | @@ -66,6 +69,7 @@ |
66 | 69 | "@types/jasmine": "~3.4.0", |
67 | 70 | "@types/jasminewd2": "~2.0.6", |
68 | 71 | "@types/jquery": "^3.3.31", |
72 | + "@types/js-beautify": "^1.8.1", | |
69 | 73 | "@types/node": "~10.14.15", |
70 | 74 | "@types/tinycolor2": "^1.4.2", |
71 | 75 | "codelyzer": "~5.1.0", | ... | ... |
... | ... | @@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module'; |
24 | 24 | import { HomeModule } from './modules/home/home.module'; |
25 | 25 | |
26 | 26 | import { AppComponent } from './app.component'; |
27 | +import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module'; | |
27 | 28 | |
28 | 29 | @NgModule({ |
29 | 30 | declarations: [ |
... | ... | @@ -35,7 +36,8 @@ import { AppComponent } from './app.component'; |
35 | 36 | AppRoutingModule, |
36 | 37 | CoreModule, |
37 | 38 | LoginModule, |
38 | - HomeModule | |
39 | + HomeModule, | |
40 | + DashboardRoutingModule | |
39 | 41 | ], |
40 | 42 | providers: [], |
41 | 43 | bootstrap: [AppComponent] | ... | ... |
... | ... | @@ -14,19 +14,26 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { IAliasController, AliasInfo } from '@core/api/widget-api.models'; | |
17 | +import { IAliasController, AliasInfo, IStateController } from '@core/api/widget-api.models'; | |
18 | 18 | import { Observable, of, Subject } from 'rxjs'; |
19 | 19 | import { Datasource } from '@app/shared/models/widget.models'; |
20 | 20 | import { deepClone } from '@core/utils'; |
21 | +import { EntityService } from '@core/http/entity.service'; | |
22 | +import { UtilsService } from '@core/services/utils.service'; | |
23 | +import { EntityAliases } from '@shared/models/alias.models'; | |
24 | +import { EntityInfo } from '@shared/models/entity.models'; | |
25 | +import * as equal from 'deep-equal'; | |
21 | 26 | |
22 | 27 | export class DummyAliasController implements IAliasController { |
23 | 28 | |
24 | 29 | entityAliasesChanged: Observable<Array<string>>; |
30 | + entityAliasResolved: Observable<string>; | |
25 | 31 | |
26 | 32 | [key: string]: any | null; |
27 | 33 | |
28 | 34 | constructor() { |
29 | 35 | this.entityAliasesChanged = new Subject<Array<string>>().asObservable(); |
36 | + this.entityAliasResolved = new Subject<string>().asObservable(); | |
30 | 37 | } |
31 | 38 | |
32 | 39 | getAliasInfo(aliasId): Observable<AliasInfo> { |
... | ... | @@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController { |
36 | 43 | resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { |
37 | 44 | return of(deepClone(datasources)); |
38 | 45 | } |
46 | + | |
47 | + getEntityAliases(): EntityAliases { | |
48 | + return undefined; | |
49 | + } | |
50 | + | |
51 | + getInstantAliasInfo(aliasId: string): AliasInfo { | |
52 | + return undefined; | |
53 | + } | |
54 | + | |
55 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) { | |
56 | + } | |
57 | + | |
58 | + updateEntityAliases(entityAliases: EntityAliases) { | |
59 | + } | |
60 | + | |
61 | +} | |
62 | + | |
63 | +export class AliasController implements IAliasController { | |
64 | + | |
65 | + private entityAliasesChangedSubject = new Subject<Array<string>>(); | |
66 | + entityAliasesChanged: Observable<Array<string>> = this.entityAliasesChangedSubject.asObservable(); | |
67 | + | |
68 | + private entityAliasResolvedSubject = new Subject<string>(); | |
69 | + entityAliasResolved: Observable<string> = this.entityAliasResolvedSubject.asObservable(); | |
70 | + | |
71 | + entityAliases: EntityAliases; | |
72 | + | |
73 | + resolvedAliases: {[aliasId: string]: AliasInfo} = {}; | |
74 | + | |
75 | + [key: string]: any | null; | |
76 | + | |
77 | + constructor(private utils: UtilsService, | |
78 | + private entityService: EntityService, | |
79 | + private stateController: IStateController, | |
80 | + private origEntityAliases: EntityAliases) { | |
81 | + this.entityAliases = deepClone(this.origEntityAliases); | |
82 | + } | |
83 | + | |
84 | + getAliasInfo(aliasId: string): Observable<AliasInfo> { | |
85 | + return of(null); | |
86 | + } | |
87 | + | |
88 | + resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { | |
89 | + return of(deepClone(datasources)); | |
90 | + } | |
91 | + | |
92 | + getEntityAliases(): EntityAliases { | |
93 | + return this.entityAliases; | |
94 | + } | |
95 | + | |
96 | + getInstantAliasInfo(aliasId: string): AliasInfo { | |
97 | + return this.resolvedAliases[aliasId]; | |
98 | + } | |
99 | + | |
100 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) { | |
101 | + const aliasInfo = this.resolvedAliases[aliasId]; | |
102 | + if (aliasInfo) { | |
103 | + const prevCurrentEntity = aliasInfo.currentEntity; | |
104 | + if (!equal(currentEntity, prevCurrentEntity)) { | |
105 | + aliasInfo.currentEntity = currentEntity; | |
106 | + this.entityAliasesChangedSubject.next([aliasId]); | |
107 | + } | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + updateEntityAliases(entityAliases: EntityAliases) { | |
112 | + } | |
113 | + | |
39 | 114 | } | ... | ... |
... | ... | @@ -34,6 +34,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; |
34 | 34 | import { HttpErrorResponse } from '@angular/common/http'; |
35 | 35 | import { DatasourceService } from '@core/api/datasource.service'; |
36 | 36 | import { RafService } from '@core/services/raf.service'; |
37 | +import { EntityAliases } from '@shared/models/alias.models'; | |
38 | +import { EntityInfo } from '@app/shared/models/entity.models'; | |
37 | 39 | |
38 | 40 | export interface TimewindowFunctions { |
39 | 41 | onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; |
... | ... | @@ -66,20 +68,24 @@ export interface WidgetActionsApi { |
66 | 68 | } |
67 | 69 | |
68 | 70 | export interface AliasInfo { |
71 | + alias?: string; | |
69 | 72 | stateEntity?: boolean; |
70 | - currentEntity?: { | |
71 | - id: string; | |
72 | - entityType: EntityType; | |
73 | - name?: string; | |
74 | - }; | |
73 | + currentEntity?: EntityInfo; | |
74 | + selectedId?: string; | |
75 | + resolvedEntities?: Array<EntityInfo>; | |
75 | 76 | [key: string]: any | null; |
76 | 77 | // TODO: |
77 | 78 | } |
78 | 79 | |
79 | 80 | export interface IAliasController { |
80 | 81 | entityAliasesChanged: Observable<Array<string>>; |
81 | - getAliasInfo(aliasId): Observable<AliasInfo>; | |
82 | + entityAliasResolved: Observable<string>; | |
83 | + getAliasInfo(aliasId: string): Observable<AliasInfo>; | |
84 | + getInstantAliasInfo(aliasId: string): AliasInfo; | |
82 | 85 | resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; |
86 | + getEntityAliases(): EntityAliases; | |
87 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); | |
88 | + updateEntityAliases(entityAliases: EntityAliases); | |
83 | 89 | [key: string]: any | null; |
84 | 90 | // TODO: |
85 | 91 | } |
... | ... | @@ -97,17 +103,14 @@ export interface StateParams { |
97 | 103 | } |
98 | 104 | |
99 | 105 | export interface IStateController { |
100 | - getStateParams: () => StateParams; | |
101 | - openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void; | |
102 | - updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; | |
106 | + getStateParams?: () => StateParams; | |
107 | + openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void; | |
108 | + updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; | |
109 | + openRightLayout: () => void; | |
110 | + preserveState?: () => void; | |
103 | 111 | // TODO: |
104 | 112 | } |
105 | 113 | |
106 | -export interface EntityInfo { | |
107 | - entityId: EntityId; | |
108 | - entityName: string; | |
109 | -} | |
110 | - | |
111 | 114 | export interface SubscriptionInfo { |
112 | 115 | type: DatasourceType; |
113 | 116 | name?: string; |
... | ... | @@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions { |
171 | 174 | // TODO: |
172 | 175 | } |
173 | 176 | |
177 | +export interface SubscriptionEntityInfo { | |
178 | + entityId: EntityId; | |
179 | + entityName: string; | |
180 | +} | |
181 | + | |
174 | 182 | export interface IWidgetSubscription { |
175 | 183 | |
176 | 184 | id: string; |
... | ... | @@ -201,7 +209,7 @@ export interface IWidgetSubscription { |
201 | 209 | rpcErrorText?: string; |
202 | 210 | rpcRejection?: HttpErrorResponse; |
203 | 211 | |
204 | - getFirstEntityInfo(): EntityInfo; | |
212 | + getFirstEntityInfo(): SubscriptionEntityInfo; | |
205 | 213 | |
206 | 214 | onAliasesChanged(aliasIds: Array<string>): boolean; |
207 | 215 | ... | ... |
... | ... | @@ -15,8 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - EntityInfo, | |
19 | - IWidgetSubscription, | |
18 | + IWidgetSubscription, SubscriptionEntityInfo, | |
20 | 19 | WidgetSubscriptionCallbacks, |
21 | 20 | WidgetSubscriptionContext, |
22 | 21 | WidgetSubscriptionOptions |
... | ... | @@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
339 | 338 | this.onDataUpdated(); |
340 | 339 | } |
341 | 340 | |
342 | - getFirstEntityInfo(): EntityInfo { | |
341 | + getFirstEntityInfo(): SubscriptionEntityInfo { | |
343 | 342 | return undefined; |
344 | 343 | } |
345 | 344 | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 | +} | ... | ... |