Commit 700293d5f9c51f5062cb28bd9d1d37afbc83cb4c
1 parent
d29a8731
Implement Widget Editor. Dashboard page initial implementation.
Showing
74 changed files
with
4959 additions
and
184 deletions
... | ... | @@ -47,6 +47,10 @@ |
47 | 47 | "node_modules/flot/src/plugins/jquery.flot.stack.js", |
48 | 48 | "node_modules/flot.curvedlines/curvedLines.js", |
49 | 49 | "node_modules/tinycolor2/dist/tinycolor-min.js", |
50 | + "node_modules/split.js/dist/split.js", | |
51 | + "node_modules/js-beautify/js/lib/beautify.js", | |
52 | + "node_modules/js-beautify/js/lib/beautify-css.js", | |
53 | + "node_modules/js-beautify/js/lib/beautify-html.js", | |
50 | 54 | "node_modules/ace-builds/src-min/ace.js", |
51 | 55 | "node_modules/ace-builds/src-min/ext-language_tools.js", |
52 | 56 | "node_modules/ace-builds/src-min/ext-searchbox.js", | ... | ... |
... | ... | @@ -1170,12 +1170,23 @@ |
1170 | 1170 | "@types/sizzle": "*" |
1171 | 1171 | } |
1172 | 1172 | }, |
1173 | + "@types/js-beautify": { | |
1174 | + "version": "1.8.1", | |
1175 | + "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.8.1.tgz", | |
1176 | + "integrity": "sha512-B1Br8yE27obcYvFx5ECZswT/947aAFNb9lHqnkUOhtOfvJqaa6Axibo4T+5G6iQlUfjgSd8am9R/9j9UBfRlrw==", | |
1177 | + "dev": true | |
1178 | + }, | |
1173 | 1179 | "@types/minimatch": { |
1174 | 1180 | "version": "3.0.3", |
1175 | 1181 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", |
1176 | 1182 | "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", |
1177 | 1183 | "dev": true |
1178 | 1184 | }, |
1185 | + "@types/mousetrap": { | |
1186 | + "version": "1.6.3", | |
1187 | + "resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz", | |
1188 | + "integrity": "sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew==" | |
1189 | + }, | |
1179 | 1190 | "@types/node": { |
1180 | 1191 | "version": "10.14.15", |
1181 | 1192 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", |
... | ... | @@ -1434,6 +1445,11 @@ |
1434 | 1445 | "through": ">=2.2.7 <3" |
1435 | 1446 | } |
1436 | 1447 | }, |
1448 | + "abbrev": { | |
1449 | + "version": "1.1.1", | |
1450 | + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", | |
1451 | + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" | |
1452 | + }, | |
1437 | 1453 | "accepts": { |
1438 | 1454 | "version": "1.3.7", |
1439 | 1455 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", |
... | ... | @@ -1523,6 +1539,15 @@ |
1523 | 1539 | "tslib": "^1.9.0" |
1524 | 1540 | } |
1525 | 1541 | }, |
1542 | + "angular2-hotkeys": { | |
1543 | + "version": "2.1.5", | |
1544 | + "resolved": "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz", | |
1545 | + "integrity": "sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA==", | |
1546 | + "requires": { | |
1547 | + "@types/mousetrap": "^1.6.0", | |
1548 | + "mousetrap": "^1.6.0" | |
1549 | + } | |
1550 | + }, | |
1526 | 1551 | "ansi-colors": { |
1527 | 1552 | "version": "3.2.4", |
1528 | 1553 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", |
... | ... | @@ -1955,8 +1980,7 @@ |
1955 | 1980 | "balanced-match": { |
1956 | 1981 | "version": "1.0.0", |
1957 | 1982 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", |
1958 | - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", | |
1959 | - "dev": true | |
1983 | + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" | |
1960 | 1984 | }, |
1961 | 1985 | "base": { |
1962 | 1986 | "version": "0.11.2", |
... | ... | @@ -2143,7 +2167,6 @@ |
2143 | 2167 | "version": "1.1.11", |
2144 | 2168 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", |
2145 | 2169 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", |
2146 | - "dev": true, | |
2147 | 2170 | "requires": { |
2148 | 2171 | "balanced-match": "^1.0.0", |
2149 | 2172 | "concat-map": "0.0.1" |
... | ... | @@ -2854,8 +2877,7 @@ |
2854 | 2877 | "commander": { |
2855 | 2878 | "version": "2.20.0", |
2856 | 2879 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", |
2857 | - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", | |
2858 | - "dev": true | |
2880 | + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" | |
2859 | 2881 | }, |
2860 | 2882 | "commondir": { |
2861 | 2883 | "version": "1.0.1", |
... | ... | @@ -3107,8 +3129,7 @@ |
3107 | 3129 | "concat-map": { |
3108 | 3130 | "version": "0.0.1", |
3109 | 3131 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", |
3110 | - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", | |
3111 | - "dev": true | |
3132 | + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" | |
3112 | 3133 | }, |
3113 | 3134 | "concat-stream": { |
3114 | 3135 | "version": "1.6.2", |
... | ... | @@ -3122,6 +3143,15 @@ |
3122 | 3143 | "typedarray": "^0.0.6" |
3123 | 3144 | } |
3124 | 3145 | }, |
3146 | + "config-chain": { | |
3147 | + "version": "1.1.12", | |
3148 | + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", | |
3149 | + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", | |
3150 | + "requires": { | |
3151 | + "ini": "^1.3.4", | |
3152 | + "proto-list": "~1.2.1" | |
3153 | + } | |
3154 | + }, | |
3125 | 3155 | "connect": { |
3126 | 3156 | "version": "3.7.0", |
3127 | 3157 | "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", |
... | ... | @@ -3762,6 +3792,17 @@ |
3762 | 3792 | "safer-buffer": "^2.1.0" |
3763 | 3793 | } |
3764 | 3794 | }, |
3795 | + "editorconfig": { | |
3796 | + "version": "0.15.3", | |
3797 | + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", | |
3798 | + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", | |
3799 | + "requires": { | |
3800 | + "commander": "^2.19.0", | |
3801 | + "lru-cache": "^4.1.5", | |
3802 | + "semver": "^5.6.0", | |
3803 | + "sigmund": "^1.0.1" | |
3804 | + } | |
3805 | + }, | |
3765 | 3806 | "ee-first": { |
3766 | 3807 | "version": "1.1.1", |
3767 | 3808 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", |
... | ... | @@ -4574,8 +4615,7 @@ |
4574 | 4615 | "fs.realpath": { |
4575 | 4616 | "version": "1.0.0", |
4576 | 4617 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", |
4577 | - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", | |
4578 | - "dev": true | |
4618 | + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" | |
4579 | 4619 | }, |
4580 | 4620 | "fsevents": { |
4581 | 4621 | "version": "1.2.9", |
... | ... | @@ -5183,7 +5223,6 @@ |
5183 | 5223 | "version": "7.1.3", |
5184 | 5224 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", |
5185 | 5225 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", |
5186 | - "dev": true, | |
5187 | 5226 | "requires": { |
5188 | 5227 | "fs.realpath": "^1.0.0", |
5189 | 5228 | "inflight": "^1.0.4", |
... | ... | @@ -5709,7 +5748,6 @@ |
5709 | 5748 | "version": "1.0.6", |
5710 | 5749 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", |
5711 | 5750 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", |
5712 | - "dev": true, | |
5713 | 5751 | "requires": { |
5714 | 5752 | "once": "^1.3.0", |
5715 | 5753 | "wrappy": "1" |
... | ... | @@ -5718,14 +5756,12 @@ |
5718 | 5756 | "inherits": { |
5719 | 5757 | "version": "2.0.4", |
5720 | 5758 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", |
5721 | - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", | |
5722 | - "dev": true | |
5759 | + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" | |
5723 | 5760 | }, |
5724 | 5761 | "ini": { |
5725 | 5762 | "version": "1.3.5", |
5726 | 5763 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", |
5727 | - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", | |
5728 | - "dev": true | |
5764 | + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" | |
5729 | 5765 | }, |
5730 | 5766 | "inquirer": { |
5731 | 5767 | "version": "6.5.0", |
... | ... | @@ -6444,6 +6480,18 @@ |
6444 | 6480 | } |
6445 | 6481 | } |
6446 | 6482 | }, |
6483 | + "js-beautify": { | |
6484 | + "version": "1.10.2", | |
6485 | + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz", | |
6486 | + "integrity": "sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==", | |
6487 | + "requires": { | |
6488 | + "config-chain": "^1.1.12", | |
6489 | + "editorconfig": "^0.15.3", | |
6490 | + "glob": "^7.1.3", | |
6491 | + "mkdirp": "~0.5.1", | |
6492 | + "nopt": "~4.0.1" | |
6493 | + } | |
6494 | + }, | |
6447 | 6495 | "js-tokens": { |
6448 | 6496 | "version": "3.0.2", |
6449 | 6497 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", |
... | ... | @@ -6918,7 +6966,6 @@ |
6918 | 6966 | "version": "4.1.5", |
6919 | 6967 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", |
6920 | 6968 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", |
6921 | - "dev": true, | |
6922 | 6969 | "requires": { |
6923 | 6970 | "pseudomap": "^1.0.2", |
6924 | 6971 | "yallist": "^2.1.2" |
... | ... | @@ -7258,7 +7305,6 @@ |
7258 | 7305 | "version": "3.0.4", |
7259 | 7306 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", |
7260 | 7307 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", |
7261 | - "dev": true, | |
7262 | 7308 | "requires": { |
7263 | 7309 | "brace-expansion": "^1.1.7" |
7264 | 7310 | } |
... | ... | @@ -7368,7 +7414,6 @@ |
7368 | 7414 | "version": "0.5.1", |
7369 | 7415 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", |
7370 | 7416 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", |
7371 | - "dev": true, | |
7372 | 7417 | "requires": { |
7373 | 7418 | "minimist": "0.0.8" |
7374 | 7419 | }, |
... | ... | @@ -7376,8 +7421,7 @@ |
7376 | 7421 | "minimist": { |
7377 | 7422 | "version": "0.0.8", |
7378 | 7423 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", |
7379 | - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", | |
7380 | - "dev": true | |
7424 | + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" | |
7381 | 7425 | } |
7382 | 7426 | } |
7383 | 7427 | }, |
... | ... | @@ -7386,6 +7430,11 @@ |
7386 | 7430 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", |
7387 | 7431 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" |
7388 | 7432 | }, |
7433 | + "mousetrap": { | |
7434 | + "version": "1.6.3", | |
7435 | + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz", | |
7436 | + "integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA==" | |
7437 | + }, | |
7389 | 7438 | "move-concurrently": { |
7390 | 7439 | "version": "1.0.1", |
7391 | 7440 | "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", |
... | ... | @@ -7563,6 +7612,15 @@ |
7563 | 7612 | "semver": "^5.3.0" |
7564 | 7613 | } |
7565 | 7614 | }, |
7615 | + "nopt": { | |
7616 | + "version": "4.0.1", | |
7617 | + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", | |
7618 | + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", | |
7619 | + "requires": { | |
7620 | + "abbrev": "1", | |
7621 | + "osenv": "^0.1.4" | |
7622 | + } | |
7623 | + }, | |
7566 | 7624 | "normalize-package-data": { |
7567 | 7625 | "version": "2.5.0", |
7568 | 7626 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", |
... | ... | @@ -7801,7 +7859,6 @@ |
7801 | 7859 | "version": "1.4.0", |
7802 | 7860 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", |
7803 | 7861 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", |
7804 | - "dev": true, | |
7805 | 7862 | "requires": { |
7806 | 7863 | "wrappy": "1" |
7807 | 7864 | } |
... | ... | @@ -7877,8 +7934,7 @@ |
7877 | 7934 | "os-homedir": { |
7878 | 7935 | "version": "1.0.2", |
7879 | 7936 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", |
7880 | - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", | |
7881 | - "dev": true | |
7937 | + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" | |
7882 | 7938 | }, |
7883 | 7939 | "os-locale": { |
7884 | 7940 | "version": "3.1.0", |
... | ... | @@ -7894,14 +7950,12 @@ |
7894 | 7950 | "os-tmpdir": { |
7895 | 7951 | "version": "1.0.2", |
7896 | 7952 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", |
7897 | - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", | |
7898 | - "dev": true | |
7953 | + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" | |
7899 | 7954 | }, |
7900 | 7955 | "osenv": { |
7901 | 7956 | "version": "0.1.5", |
7902 | 7957 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", |
7903 | 7958 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", |
7904 | - "dev": true, | |
7905 | 7959 | "requires": { |
7906 | 7960 | "os-homedir": "^1.0.0", |
7907 | 7961 | "os-tmpdir": "^1.0.0" |
... | ... | @@ -8222,8 +8276,7 @@ |
8222 | 8276 | "path-is-absolute": { |
8223 | 8277 | "version": "1.0.1", |
8224 | 8278 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", |
8225 | - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", | |
8226 | - "dev": true | |
8279 | + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" | |
8227 | 8280 | }, |
8228 | 8281 | "path-is-inside": { |
8229 | 8282 | "version": "1.0.2", |
... | ... | @@ -8465,6 +8518,11 @@ |
8465 | 8518 | "retry": "^0.10.0" |
8466 | 8519 | } |
8467 | 8520 | }, |
8521 | + "proto-list": { | |
8522 | + "version": "1.2.4", | |
8523 | + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", | |
8524 | + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" | |
8525 | + }, | |
8468 | 8526 | "protoduck": { |
8469 | 8527 | "version": "5.0.1", |
8470 | 8528 | "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", |
... | ... | @@ -8612,8 +8670,7 @@ |
8612 | 8670 | "pseudomap": { |
8613 | 8671 | "version": "1.0.2", |
8614 | 8672 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", |
8615 | - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", | |
8616 | - "dev": true | |
8673 | + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" | |
8617 | 8674 | }, |
8618 | 8675 | "psl": { |
8619 | 8676 | "version": "1.3.0", |
... | ... | @@ -9208,8 +9265,7 @@ |
9208 | 9265 | "semver": { |
9209 | 9266 | "version": "5.6.0", |
9210 | 9267 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", |
9211 | - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", | |
9212 | - "dev": true | |
9268 | + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" | |
9213 | 9269 | }, |
9214 | 9270 | "semver-dsl": { |
9215 | 9271 | "version": "1.0.1", |
... | ... | @@ -9408,6 +9464,11 @@ |
9408 | 9464 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", |
9409 | 9465 | "dev": true |
9410 | 9466 | }, |
9467 | + "sigmund": { | |
9468 | + "version": "1.0.1", | |
9469 | + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", | |
9470 | + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" | |
9471 | + }, | |
9411 | 9472 | "signal-exit": { |
9412 | 9473 | "version": "3.0.2", |
9413 | 9474 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", |
... | ... | @@ -9920,6 +9981,11 @@ |
9920 | 9981 | "extend-shallow": "^3.0.0" |
9921 | 9982 | } |
9922 | 9983 | }, |
9984 | + "split.js": { | |
9985 | + "version": "1.5.11", | |
9986 | + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.5.11.tgz", | |
9987 | + "integrity": "sha512-ec0sAbWnaMGpNHWo1ZgIlF3Mx7GzSyaO0GlcEBZGIFZQwYPPkbDV6JRpDmpzIshVig7USREuEPudy0ygQaskXg==" | |
9988 | + }, | |
9923 | 9989 | "sprintf-js": { |
9924 | 9990 | "version": "1.0.3", |
9925 | 9991 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", |
... | ... | @@ -11121,8 +11187,7 @@ |
11121 | 11187 | "wrappy": { |
11122 | 11188 | "version": "1.0.2", |
11123 | 11189 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", |
11124 | - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", | |
11125 | - "dev": true | |
11190 | + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" | |
11126 | 11191 | }, |
11127 | 11192 | "ws": { |
11128 | 11193 | "version": "3.3.3", |
... | ... | @@ -11180,8 +11245,7 @@ |
11180 | 11245 | "yallist": { |
11181 | 11246 | "version": "2.1.2", |
11182 | 11247 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", |
11183 | - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", | |
11184 | - "dev": true | |
11248 | + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" | |
11185 | 11249 | }, |
11186 | 11250 | "yargs": { |
11187 | 11251 | "version": "12.0.5", | ... | ... |
... | ... | @@ -33,6 +33,7 @@ |
33 | 33 | "@ngx-translate/http-loader": "^4.0.0", |
34 | 34 | "ace-builds": "^1.4.5", |
35 | 35 | "angular-gridster2": "^8.1.0", |
36 | + "angular2-hotkeys": "^2.1.5", | |
36 | 37 | "base64-js": "^1.3.1", |
37 | 38 | "compass-sass-mixins": "^0.12.7", |
38 | 39 | "core-js": "^3.1.4", |
... | ... | @@ -44,6 +45,7 @@ |
44 | 45 | "javascript-detect-element-resize": "^0.5.3", |
45 | 46 | "jquery": "^3.4.1", |
46 | 47 | "jquery.terminal": "^2.8.0", |
48 | + "js-beautify": "^1.10.2", | |
47 | 49 | "material-design-icons": "^3.0.1", |
48 | 50 | "messageformat": "^2.3.0", |
49 | 51 | "moment": "^2.24.0", |
... | ... | @@ -51,6 +53,7 @@ |
51 | 53 | "ngx-translate-messageformat-compiler": "^4.5.0", |
52 | 54 | "rxjs": "~6.5.2", |
53 | 55 | "screenfull": "^4.2.1", |
56 | + "split.js": "^1.5.11", | |
54 | 57 | "tinycolor2": "^1.4.1", |
55 | 58 | "tslib": "^1.10.0", |
56 | 59 | "typeface-roboto": "^0.0.75", |
... | ... | @@ -66,6 +69,7 @@ |
66 | 69 | "@types/jasmine": "~3.4.0", |
67 | 70 | "@types/jasminewd2": "~2.0.6", |
68 | 71 | "@types/jquery": "^3.3.31", |
72 | + "@types/js-beautify": "^1.8.1", | |
69 | 73 | "@types/node": "~10.14.15", |
70 | 74 | "@types/tinycolor2": "^1.4.2", |
71 | 75 | "codelyzer": "~5.1.0", | ... | ... |
... | ... | @@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module'; |
24 | 24 | import { HomeModule } from './modules/home/home.module'; |
25 | 25 | |
26 | 26 | import { AppComponent } from './app.component'; |
27 | +import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module'; | |
27 | 28 | |
28 | 29 | @NgModule({ |
29 | 30 | declarations: [ |
... | ... | @@ -35,7 +36,8 @@ import { AppComponent } from './app.component'; |
35 | 36 | AppRoutingModule, |
36 | 37 | CoreModule, |
37 | 38 | LoginModule, |
38 | - HomeModule | |
39 | + HomeModule, | |
40 | + DashboardRoutingModule | |
39 | 41 | ], |
40 | 42 | providers: [], |
41 | 43 | bootstrap: [AppComponent] | ... | ... |
... | ... | @@ -14,19 +14,26 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { IAliasController, AliasInfo } from '@core/api/widget-api.models'; | |
17 | +import { IAliasController, AliasInfo, IStateController } from '@core/api/widget-api.models'; | |
18 | 18 | import { Observable, of, Subject } from 'rxjs'; |
19 | 19 | import { Datasource } from '@app/shared/models/widget.models'; |
20 | 20 | import { deepClone } from '@core/utils'; |
21 | +import { EntityService } from '@core/http/entity.service'; | |
22 | +import { UtilsService } from '@core/services/utils.service'; | |
23 | +import { EntityAliases } from '@shared/models/alias.models'; | |
24 | +import { EntityInfo } from '@shared/models/entity.models'; | |
25 | +import * as equal from 'deep-equal'; | |
21 | 26 | |
22 | 27 | export class DummyAliasController implements IAliasController { |
23 | 28 | |
24 | 29 | entityAliasesChanged: Observable<Array<string>>; |
30 | + entityAliasResolved: Observable<string>; | |
25 | 31 | |
26 | 32 | [key: string]: any | null; |
27 | 33 | |
28 | 34 | constructor() { |
29 | 35 | this.entityAliasesChanged = new Subject<Array<string>>().asObservable(); |
36 | + this.entityAliasResolved = new Subject<string>().asObservable(); | |
30 | 37 | } |
31 | 38 | |
32 | 39 | getAliasInfo(aliasId): Observable<AliasInfo> { |
... | ... | @@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController { |
36 | 43 | resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { |
37 | 44 | return of(deepClone(datasources)); |
38 | 45 | } |
46 | + | |
47 | + getEntityAliases(): EntityAliases { | |
48 | + return undefined; | |
49 | + } | |
50 | + | |
51 | + getInstantAliasInfo(aliasId: string): AliasInfo { | |
52 | + return undefined; | |
53 | + } | |
54 | + | |
55 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) { | |
56 | + } | |
57 | + | |
58 | + updateEntityAliases(entityAliases: EntityAliases) { | |
59 | + } | |
60 | + | |
61 | +} | |
62 | + | |
63 | +export class AliasController implements IAliasController { | |
64 | + | |
65 | + private entityAliasesChangedSubject = new Subject<Array<string>>(); | |
66 | + entityAliasesChanged: Observable<Array<string>> = this.entityAliasesChangedSubject.asObservable(); | |
67 | + | |
68 | + private entityAliasResolvedSubject = new Subject<string>(); | |
69 | + entityAliasResolved: Observable<string> = this.entityAliasResolvedSubject.asObservable(); | |
70 | + | |
71 | + entityAliases: EntityAliases; | |
72 | + | |
73 | + resolvedAliases: {[aliasId: string]: AliasInfo} = {}; | |
74 | + | |
75 | + [key: string]: any | null; | |
76 | + | |
77 | + constructor(private utils: UtilsService, | |
78 | + private entityService: EntityService, | |
79 | + private stateController: IStateController, | |
80 | + private origEntityAliases: EntityAliases) { | |
81 | + this.entityAliases = deepClone(this.origEntityAliases); | |
82 | + } | |
83 | + | |
84 | + getAliasInfo(aliasId: string): Observable<AliasInfo> { | |
85 | + return of(null); | |
86 | + } | |
87 | + | |
88 | + resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { | |
89 | + return of(deepClone(datasources)); | |
90 | + } | |
91 | + | |
92 | + getEntityAliases(): EntityAliases { | |
93 | + return this.entityAliases; | |
94 | + } | |
95 | + | |
96 | + getInstantAliasInfo(aliasId: string): AliasInfo { | |
97 | + return this.resolvedAliases[aliasId]; | |
98 | + } | |
99 | + | |
100 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) { | |
101 | + const aliasInfo = this.resolvedAliases[aliasId]; | |
102 | + if (aliasInfo) { | |
103 | + const prevCurrentEntity = aliasInfo.currentEntity; | |
104 | + if (!equal(currentEntity, prevCurrentEntity)) { | |
105 | + aliasInfo.currentEntity = currentEntity; | |
106 | + this.entityAliasesChangedSubject.next([aliasId]); | |
107 | + } | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + updateEntityAliases(entityAliases: EntityAliases) { | |
112 | + } | |
113 | + | |
39 | 114 | } | ... | ... |
... | ... | @@ -34,6 +34,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models'; |
34 | 34 | import { HttpErrorResponse } from '@angular/common/http'; |
35 | 35 | import { DatasourceService } from '@core/api/datasource.service'; |
36 | 36 | import { RafService } from '@core/services/raf.service'; |
37 | +import { EntityAliases } from '@shared/models/alias.models'; | |
38 | +import { EntityInfo } from '@app/shared/models/entity.models'; | |
37 | 39 | |
38 | 40 | export interface TimewindowFunctions { |
39 | 41 | onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; |
... | ... | @@ -66,20 +68,24 @@ export interface WidgetActionsApi { |
66 | 68 | } |
67 | 69 | |
68 | 70 | export interface AliasInfo { |
71 | + alias?: string; | |
69 | 72 | stateEntity?: boolean; |
70 | - currentEntity?: { | |
71 | - id: string; | |
72 | - entityType: EntityType; | |
73 | - name?: string; | |
74 | - }; | |
73 | + currentEntity?: EntityInfo; | |
74 | + selectedId?: string; | |
75 | + resolvedEntities?: Array<EntityInfo>; | |
75 | 76 | [key: string]: any | null; |
76 | 77 | // TODO: |
77 | 78 | } |
78 | 79 | |
79 | 80 | export interface IAliasController { |
80 | 81 | entityAliasesChanged: Observable<Array<string>>; |
81 | - getAliasInfo(aliasId): Observable<AliasInfo>; | |
82 | + entityAliasResolved: Observable<string>; | |
83 | + getAliasInfo(aliasId: string): Observable<AliasInfo>; | |
84 | + getInstantAliasInfo(aliasId: string): AliasInfo; | |
82 | 85 | resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; |
86 | + getEntityAliases(): EntityAliases; | |
87 | + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); | |
88 | + updateEntityAliases(entityAliases: EntityAliases); | |
83 | 89 | [key: string]: any | null; |
84 | 90 | // TODO: |
85 | 91 | } |
... | ... | @@ -97,17 +103,14 @@ export interface StateParams { |
97 | 103 | } |
98 | 104 | |
99 | 105 | export interface IStateController { |
100 | - getStateParams: () => StateParams; | |
101 | - openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void; | |
102 | - updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; | |
106 | + getStateParams?: () => StateParams; | |
107 | + openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void; | |
108 | + updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; | |
109 | + openRightLayout: () => void; | |
110 | + preserveState?: () => void; | |
103 | 111 | // TODO: |
104 | 112 | } |
105 | 113 | |
106 | -export interface EntityInfo { | |
107 | - entityId: EntityId; | |
108 | - entityName: string; | |
109 | -} | |
110 | - | |
111 | 114 | export interface SubscriptionInfo { |
112 | 115 | type: DatasourceType; |
113 | 116 | name?: string; |
... | ... | @@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions { |
171 | 174 | // TODO: |
172 | 175 | } |
173 | 176 | |
177 | +export interface SubscriptionEntityInfo { | |
178 | + entityId: EntityId; | |
179 | + entityName: string; | |
180 | +} | |
181 | + | |
174 | 182 | export interface IWidgetSubscription { |
175 | 183 | |
176 | 184 | id: string; |
... | ... | @@ -201,7 +209,7 @@ export interface IWidgetSubscription { |
201 | 209 | rpcErrorText?: string; |
202 | 210 | rpcRejection?: HttpErrorResponse; |
203 | 211 | |
204 | - getFirstEntityInfo(): EntityInfo; | |
212 | + getFirstEntityInfo(): SubscriptionEntityInfo; | |
205 | 213 | |
206 | 214 | onAliasesChanged(aliasIds: Array<string>): boolean; |
207 | 215 | ... | ... |
... | ... | @@ -15,8 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - EntityInfo, | |
19 | - IWidgetSubscription, | |
18 | + IWidgetSubscription, SubscriptionEntityInfo, | |
20 | 19 | WidgetSubscriptionCallbacks, |
21 | 20 | WidgetSubscriptionContext, |
22 | 21 | WidgetSubscriptionOptions |
... | ... | @@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
339 | 338 | this.onDataUpdated(); |
340 | 339 | } |
341 | 340 | |
342 | - getFirstEntityInfo(): EntityInfo { | |
341 | + getFirstEntityInfo(): SubscriptionEntityInfo { | |
343 | 342 | return undefined; |
344 | 343 | } |
345 | 344 | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<mat-select fxHide.xs fxHide.sm fxHide.md | |
19 | + [required]="required" | |
20 | + [disabled]="disabled" | |
21 | + [(ngModel)]="dashboardId" | |
22 | + (ngModelChange)="dashboardIdChanged()"> | |
23 | + <mat-option *ngFor="let dashboard of dashboards$ | async" [value]="dashboard.id.id"> | |
24 | + {{dashboard.title}} | |
25 | + </mat-option> | |
26 | +</mat-select> | |
27 | +<section fxHide.gt-md class="tb-dashboard-select"> | |
28 | + <button mat-button mat-icon-button | |
29 | + cdkOverlayOrigin #dashboardSelectPanelOrigin="cdkOverlayOrigin" | |
30 | + (click)="openDashboardSelectPanel()" | |
31 | + matTooltip="{{ 'dashboard.select-dashboard' | translate }}" | |
32 | + [matTooltipPosition]="tooltipPosition"> | |
33 | + <mat-icon>dashboards</mat-icon> | |
34 | + </button> | |
35 | +</section> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + min-width: 52px; | |
18 | + | |
19 | + mat-select { | |
20 | + max-width: 300px; | |
21 | + pointer-events: all; | |
22 | + } | |
23 | + | |
24 | + .tb-dashboard-select { | |
25 | + min-height: 32px; | |
26 | + | |
27 | + span { | |
28 | + pointer-events: all; | |
29 | + cursor: pointer; | |
30 | + } | |
31 | + } | |
32 | +} | |
33 | + | |
34 | +:host ::ng-deep { | |
35 | + mat-select { | |
36 | + .mat-select-value { | |
37 | + max-width: 282px; | |
38 | + } | |
39 | + } | |
40 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; | |
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | +import { Observable, of } from 'rxjs'; | |
20 | +import { PageLink } from '@shared/models/page/page-link'; | |
21 | +import { map, share } from 'rxjs/operators'; | |
22 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | |
23 | +import { DashboardInfo } from '@app/shared/models/dashboard.models'; | |
24 | +import { DashboardService } from '@core/http/dashboard.service'; | |
25 | +import { Store } from '@ngrx/store'; | |
26 | +import { AppState } from '@app/core/core.state'; | |
27 | +import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; | |
28 | +import { Authority } from '@shared/models/authority.enum'; | |
29 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
30 | +import { TooltipPosition } from '@angular/material/tooltip'; | |
31 | +import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; | |
32 | +import { BreakpointObserver } from '@angular/cdk/layout'; | |
33 | +import { DOCUMENT } from '@angular/common'; | |
34 | +import { WINDOW } from '@core/services/window.service'; | |
35 | +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; | |
36 | +import { | |
37 | + DASHBOARD_SELECT_PANEL_DATA, | |
38 | + DashboardSelectPanelComponent, | |
39 | + DashboardSelectPanelData | |
40 | +} from './dashboard-select-panel.component'; | |
41 | + | |
42 | +@Component({ | |
43 | + selector: 'tb-dashboard-select', | |
44 | + templateUrl: './dashboard-select.component.html', | |
45 | + styleUrls: ['./dashboard-select.component.scss'], | |
46 | + providers: [{ | |
47 | + provide: NG_VALUE_ACCESSOR, | |
48 | + useExisting: forwardRef(() => DashboardSelectComponent), | |
49 | + multi: true | |
50 | + }] | |
51 | +}) | |
52 | +export class DashboardSelectComponent implements ControlValueAccessor, OnInit { | |
53 | + | |
54 | + @Input() | |
55 | + dashboardsScope: 'customer' | 'tenant'; | |
56 | + | |
57 | + @Input() | |
58 | + customerId: string; | |
59 | + | |
60 | + @Input() | |
61 | + tooltipPosition: TooltipPosition = 'above'; | |
62 | + | |
63 | + private requiredValue: boolean; | |
64 | + get required(): boolean { | |
65 | + return this.requiredValue; | |
66 | + } | |
67 | + @Input() | |
68 | + set required(value: boolean) { | |
69 | + this.requiredValue = coerceBooleanProperty(value); | |
70 | + } | |
71 | + | |
72 | + @Input() | |
73 | + disabled: boolean; | |
74 | + | |
75 | + dashboards$: Observable<Array<DashboardInfo>>; | |
76 | + | |
77 | + dashboardId: string | null; | |
78 | + | |
79 | + @ViewChild('dashboardSelectPanelOrigin', {static: false}) dashboardSelectPanelOrigin: CdkOverlayOrigin; | |
80 | + | |
81 | + private propagateChange = (v: any) => { }; | |
82 | + | |
83 | + constructor(private store: Store<AppState>, | |
84 | + private dashboardService: DashboardService, | |
85 | + private overlay: Overlay, | |
86 | + private breakpointObserver: BreakpointObserver, | |
87 | + private viewContainerRef: ViewContainerRef, | |
88 | + @Inject(DOCUMENT) private document: Document, | |
89 | + @Inject(WINDOW) private window: Window) { | |
90 | + } | |
91 | + | |
92 | + registerOnChange(fn: any): void { | |
93 | + this.propagateChange = fn; | |
94 | + } | |
95 | + | |
96 | + registerOnTouched(fn: any): void { | |
97 | + } | |
98 | + | |
99 | + ngOnInit() { | |
100 | + | |
101 | + const pageLink = new PageLink(100); | |
102 | + | |
103 | + this.dashboards$ = this.getDashboards(pageLink).pipe( | |
104 | + map((pageData) => pageData.data), | |
105 | + share() | |
106 | + ); | |
107 | + } | |
108 | + | |
109 | + setDisabledState(isDisabled: boolean): void { | |
110 | + this.disabled = isDisabled; | |
111 | + } | |
112 | + | |
113 | + writeValue(value: string | null): void { | |
114 | + this.dashboardId = value; | |
115 | + } | |
116 | + | |
117 | + dashboardIdChanged() { | |
118 | + this.updateView(); | |
119 | + } | |
120 | + | |
121 | + openDashboardSelectPanel() { | |
122 | + if (this.disabled) { | |
123 | + return; | |
124 | + } | |
125 | + const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150; | |
126 | + const panelWidth = 300; | |
127 | + const position = this.overlay.position(); | |
128 | + const config = new OverlayConfig({ | |
129 | + panelClass: 'tb-dashboard-select-panel', | |
130 | + backdropClass: 'cdk-overlay-transparent-backdrop', | |
131 | + hasBackdrop: true, | |
132 | + }); | |
133 | + const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement; | |
134 | + const offset = el.getBoundingClientRect(); | |
135 | + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; | |
136 | + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; | |
137 | + const bottomY = offset.bottom - scrollTop; | |
138 | + const leftX = offset.left - scrollLeft; | |
139 | + let originX; | |
140 | + let originY; | |
141 | + let overlayX; | |
142 | + let overlayY; | |
143 | + const wHeight = this.document.documentElement.clientHeight; | |
144 | + const wWidth = this.document.documentElement.clientWidth; | |
145 | + if (bottomY + panelHeight > wHeight) { | |
146 | + originY = 'top'; | |
147 | + overlayY = 'bottom'; | |
148 | + } else { | |
149 | + originY = 'bottom'; | |
150 | + overlayY = 'top'; | |
151 | + } | |
152 | + if (leftX + panelWidth > wWidth) { | |
153 | + originX = 'end'; | |
154 | + overlayX = 'end'; | |
155 | + } else { | |
156 | + originX = 'start'; | |
157 | + overlayX = 'start'; | |
158 | + } | |
159 | + const connectedPosition: ConnectedPosition = { | |
160 | + originX, | |
161 | + originY, | |
162 | + overlayX, | |
163 | + overlayY | |
164 | + }; | |
165 | + config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef) | |
166 | + .withPositions([connectedPosition]); | |
167 | + const overlayRef = this.overlay.create(config); | |
168 | + overlayRef.backdropClick().subscribe(() => { | |
169 | + overlayRef.dispose(); | |
170 | + }); | |
171 | + | |
172 | + const injector = this._createDashboardSelectPanelInjector( | |
173 | + overlayRef, | |
174 | + { | |
175 | + dashboards$: this.dashboards$, | |
176 | + dashboardId: this.dashboardId, | |
177 | + onDashboardSelected: (dashboardId) => { | |
178 | + overlayRef.dispose(); | |
179 | + this.dashboardId = dashboardId; | |
180 | + this.updateView(); | |
181 | + } | |
182 | + } | |
183 | + ); | |
184 | + overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector)); | |
185 | + } | |
186 | + | |
187 | + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector { | |
188 | + const injectionTokens = new WeakMap<any, any>([ | |
189 | + [DASHBOARD_SELECT_PANEL_DATA, data], | |
190 | + [OverlayRef, overlayRef] | |
191 | + ]); | |
192 | + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); | |
193 | + } | |
194 | + | |
195 | + private updateView() { | |
196 | + this.propagateChange(this.dashboardId); | |
197 | + } | |
198 | + | |
199 | + private getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> { | |
200 | + let dashboardsObservable: Observable<PageData<DashboardInfo>>; | |
201 | + const authUser = getCurrentAuthUser(this.store); | |
202 | + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { | |
203 | + if (this.customerId) { | |
204 | + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); | |
205 | + } else { | |
206 | + dashboardsObservable = of(emptyPageData()); | |
207 | + } | |
208 | + } else { | |
209 | + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true); | |
210 | + } | |
211 | + return dashboardsObservable; | |
212 | + } | |
213 | + | |
214 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div class="mat-fab-toolbar-wrapper"> | |
19 | + <div class="mat-fab-toolbar-content"> | |
20 | + <ng-content></ng-content> | |
21 | + </div> | |
22 | +</div> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +$font-size: 10px !default; | |
17 | +@function rem($multiplier) { | |
18 | + @return $multiplier * $font-size; | |
19 | +} | |
20 | + | |
21 | +$button-fab-width: rem(5.600) !default; | |
22 | +$button-fab-height: rem(5.600) !default; | |
23 | +$button-fab-padding: rem(1.60) !default; | |
24 | +$icon-button-margin: rem(0.600) !default; | |
25 | +$z-index-fab: 20 !default; | |
26 | + | |
27 | +$swift-ease-in-duration: 0.3s !default; | |
28 | +$swift-ease-in-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2) !default; | |
29 | +$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default; | |
30 | + | |
31 | +@mixin rtl-prop($ltr-prop, $rtl-prop, $value, $reset-value) { | |
32 | + #{$ltr-prop}: $value; | |
33 | + [dir=rtl] & { | |
34 | + #{$ltr-prop}: $reset-value; | |
35 | + #{$rtl-prop}: $value; | |
36 | + } | |
37 | +} | |
38 | + | |
39 | +@mixin fab-position($spot, $top: auto, $right: auto, $bottom: auto, $left: auto) { | |
40 | + &.mat-fab-#{$spot} { | |
41 | + top: $top; | |
42 | + right: $right; | |
43 | + bottom: $bottom; | |
44 | + left: $left; | |
45 | + position: absolute; | |
46 | + } | |
47 | +} | |
48 | + | |
49 | +@mixin fab-all-positions() { | |
50 | + @include fab-position(bottom-right, auto, ($button-fab-width - $button-fab-padding)/2, ($button-fab-height - $button-fab-padding)/2, auto); | |
51 | + @include fab-position(bottom-left, auto, auto, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2); | |
52 | + @include fab-position(top-right, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2, auto, auto); | |
53 | + @include fab-position(top-left, ($button-fab-height - $button-fab-padding)/2, auto, auto, ($button-fab-width - $button-fab-padding)/2); | |
54 | +} | |
55 | + | |
56 | +mat-fab-toolbar { | |
57 | + $icon-delay: 200ms; | |
58 | + @include fab-all-positions(); | |
59 | + display: block; | |
60 | + | |
61 | + .mat-fab-toolbar-wrapper { | |
62 | + display: block; | |
63 | + position: relative; | |
64 | + overflow: hidden; | |
65 | + | |
66 | + height: $button-fab-width + ($icon-button-margin * 2); | |
67 | + } | |
68 | + | |
69 | + mat-fab-trigger { | |
70 | + position: absolute; | |
71 | + z-index: $z-index-fab; | |
72 | + | |
73 | + button { | |
74 | + overflow: visible !important; | |
75 | + opacity: .5; | |
76 | + } | |
77 | + | |
78 | + .mat-fab-toolbar-background { | |
79 | + display: block; | |
80 | + position: absolute; | |
81 | + z-index: $z-index-fab + 1; | |
82 | + opacity: 1; | |
83 | + } | |
84 | + | |
85 | + mat-icon { | |
86 | + position: relative; | |
87 | + z-index: $z-index-fab + 2; | |
88 | + | |
89 | + opacity: 1; | |
90 | + | |
91 | + } | |
92 | + | |
93 | + } | |
94 | + | |
95 | + &.mat-left { | |
96 | + mat-fab-trigger { | |
97 | + @include rtl-prop(right, left, 0, auto); | |
98 | + } | |
99 | + | |
100 | + .mat-toolbar-tools { | |
101 | + flex-direction: row-reverse; | |
102 | + | |
103 | + > .mat-button:first-child { | |
104 | + @include rtl-prop(margin-right, margin-left, 0.6rem, auto) | |
105 | + } | |
106 | + | |
107 | + > .mat-button:first-child { | |
108 | + @include rtl-prop(margin-left, margin-right, -0.8rem, auto); | |
109 | + } | |
110 | + | |
111 | + | |
112 | + > .mat-button:last-child { | |
113 | + @include rtl-prop(margin-right, margin-left, 8px, auto); | |
114 | + } | |
115 | + | |
116 | + } | |
117 | + } | |
118 | + | |
119 | + &.mat-right { | |
120 | + mat-fab-trigger { | |
121 | + @include rtl-prop(left, right, 0, auto); | |
122 | + } | |
123 | + | |
124 | + .mat-toolbar-tools { | |
125 | + flex-direction: row; | |
126 | + } | |
127 | + } | |
128 | + | |
129 | + mat-toolbar { | |
130 | + padding: 0 !important; | |
131 | + background-color: transparent !important; | |
132 | + pointer-events: none; | |
133 | + position: relative; | |
134 | + z-index: $z-index-fab + 3; | |
135 | + | |
136 | + .mat-toolbar-tools { | |
137 | + padding: 0 20px !important; | |
138 | + margin-top: 3px; | |
139 | + } | |
140 | + | |
141 | + .mat-fab-action-item { | |
142 | + opacity: 0; | |
143 | + transform: scale(0); | |
144 | + } | |
145 | + } | |
146 | + | |
147 | + &.mat-is-open { | |
148 | + mat-fab-trigger > button { | |
149 | + box-shadow: none; | |
150 | + opacity: 1; | |
151 | + | |
152 | + mat-icon { | |
153 | + opacity: 0; | |
154 | + } | |
155 | + } | |
156 | + | |
157 | + .mat-fab-action-item { | |
158 | + opacity: 1; | |
159 | + transform: scale(1); | |
160 | + } | |
161 | + } | |
162 | + | |
163 | + &.mat-animation { | |
164 | + mat-fab-trigger { | |
165 | + button { | |
166 | + transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s; | |
167 | + } | |
168 | + .mat-fab-toolbar-background { | |
169 | + transition: $swift-ease-in; | |
170 | + } | |
171 | + mat-icon { | |
172 | + transition: all $icon-delay ease-in; | |
173 | + } | |
174 | + } | |
175 | + mat-toolbar { | |
176 | + .mat-fab-action-item { | |
177 | + transition: $swift-ease-in; | |
178 | + transition-duration: $swift-ease-in-duration / 2; | |
179 | + } | |
180 | + } | |
181 | + &.mat-is-open { | |
182 | + mat-fab-trigger > button { | |
183 | + transition: opacity .3s cubic-bezier(.55, 0, .55, .2); | |
184 | + } | |
185 | + } | |
186 | + } | |
187 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + ElementRef, | |
20 | + Input, | |
21 | + OnChanges, | |
22 | + OnInit, | |
23 | + Renderer2, | |
24 | + ViewEncapsulation, | |
25 | + SimpleChanges, | |
26 | + Inject, AfterViewInit, AfterViewChecked, Directive, OnDestroy | |
27 | +} from '@angular/core'; | |
28 | +import { PageComponent } from '@shared/components/page.component'; | |
29 | +import { WINDOW } from '@core/services/window.service'; | |
30 | +import { mixinColor, CanColorCtor } from '@angular/material'; | |
31 | + | |
32 | +export declare type FabToolbarDirection = 'left' | 'right'; | |
33 | + | |
34 | +class MatFabToolbarBase { | |
35 | + // tslint:disable-next-line:variable-name | |
36 | + constructor(public _elementRef: ElementRef) {} | |
37 | +} | |
38 | +const MatFabToolbarMixinBase: CanColorCtor & typeof MatFabToolbarBase = mixinColor(MatFabToolbarBase); | |
39 | + | |
40 | +@Directive({ | |
41 | + selector: 'mat-fab-trigger' | |
42 | +}) | |
43 | +export class FabTriggerDirective { | |
44 | + | |
45 | + constructor(private el: ElementRef<HTMLElement>) { | |
46 | + } | |
47 | + | |
48 | +} | |
49 | + | |
50 | +@Directive({ | |
51 | + selector: 'mat-fab-actions' | |
52 | +}) | |
53 | +export class FabActionsDirective implements OnInit { | |
54 | + | |
55 | + constructor(private el: ElementRef<HTMLElement>) { | |
56 | + } | |
57 | + | |
58 | + ngOnInit(): void { | |
59 | + const element = $(this.el.nativeElement); | |
60 | + const children = element.children(); | |
61 | + children.wrap('<div class="mat-fab-action-item">'); | |
62 | + } | |
63 | + | |
64 | +} | |
65 | + | |
66 | +@Component({ | |
67 | + selector: 'mat-fab-toolbar', | |
68 | + templateUrl: './fab-toolbar.component.html', | |
69 | + styleUrls: ['./fab-toolbar.component.scss'], | |
70 | + inputs: ['color'], | |
71 | + encapsulation: ViewEncapsulation.None | |
72 | +}) | |
73 | +export class FabToolbarComponent extends MatFabToolbarMixinBase implements OnInit, OnDestroy, AfterViewInit, OnChanges { | |
74 | + | |
75 | + @Input() | |
76 | + isOpen: boolean; | |
77 | + | |
78 | + @Input() | |
79 | + direction: FabToolbarDirection; | |
80 | + | |
81 | + fabToolbarResizeListener = this.onFabToolbarResize.bind(this); | |
82 | + | |
83 | + constructor(private el: ElementRef<HTMLElement>, | |
84 | + @Inject(WINDOW) private window: Window) { | |
85 | + super(el); | |
86 | + } | |
87 | + | |
88 | + ngOnInit(): void { | |
89 | + const element = $(this.el.nativeElement); | |
90 | + element.addClass('mat-fab-toolbar'); | |
91 | + element.find('mat-fab-trigger').find('button') | |
92 | + .prepend('<div class="mat-fab-toolbar-background"></div>'); | |
93 | + element.addClass(`mat-${this.direction}`); | |
94 | + // @ts-ignore | |
95 | + addResizeListener(this.el.nativeElement, this.fabToolbarResizeListener); | |
96 | + } | |
97 | + | |
98 | + ngOnDestroy(): void { | |
99 | + // @ts-ignore | |
100 | + removeResizeListener(this.el.nativeElement, this.fabToolbarResizeListener); | |
101 | + } | |
102 | + | |
103 | + ngAfterViewInit(): void { | |
104 | + this.triggerOpenClose(true); | |
105 | + } | |
106 | + | |
107 | + ngOnChanges(changes: SimpleChanges): void { | |
108 | + for (const propName of Object.keys(changes)) { | |
109 | + const change = changes[propName]; | |
110 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | |
111 | + if (propName === 'isOpen') { | |
112 | + this.triggerOpenClose(); | |
113 | + } | |
114 | + } | |
115 | + } | |
116 | + } | |
117 | + | |
118 | + private onFabToolbarResize() { | |
119 | + if (this.isOpen) { | |
120 | + this.triggerOpenClose(true); | |
121 | + } | |
122 | + } | |
123 | + | |
124 | + private triggerOpenClose(disableAnimation?: boolean): void { | |
125 | + const el = this.el.nativeElement; | |
126 | + const element = $(this.el.nativeElement); | |
127 | + if (disableAnimation) { | |
128 | + element.removeClass('mat-animation'); | |
129 | + } else { | |
130 | + element.addClass('mat-animation'); | |
131 | + } | |
132 | + const backgroundElement: HTMLElement = el.querySelector('.mat-fab-toolbar-background'); | |
133 | + const triggerElement: HTMLElement = el.querySelector('mat-fab-trigger button'); | |
134 | + const toolbarElement: HTMLElement = el.querySelector('mat-toolbar'); | |
135 | + const iconElement: HTMLElement = el.querySelector('mat-fab-trigger button mat-icon'); | |
136 | + const actions = element.find('mat-fab-actions').children(); | |
137 | + if (triggerElement && backgroundElement) { | |
138 | + const width = el.offsetWidth; | |
139 | + const height = el.offsetHeight; | |
140 | + const scale = 2 * (width / triggerElement.offsetWidth); | |
141 | + | |
142 | + backgroundElement.style.borderRadius = width + 'px'; | |
143 | + | |
144 | + if (this.isOpen) { | |
145 | + element.addClass('mat-is-open'); | |
146 | + toolbarElement.style.pointerEvents = 'inherit'; | |
147 | + | |
148 | + backgroundElement.style.width = triggerElement.offsetWidth + 'px'; | |
149 | + backgroundElement.style.height = triggerElement.offsetHeight + 'px'; | |
150 | + backgroundElement.style.transform = 'scale(' + scale + ')'; | |
151 | + | |
152 | + backgroundElement.style.transitionDelay = '0ms'; | |
153 | + if (iconElement) { | |
154 | + iconElement.style.transitionDelay = disableAnimation ? '0ms' : '.3s'; | |
155 | + } | |
156 | + | |
157 | + actions.each((index, action) => { | |
158 | + action.style.transitionDelay = disableAnimation ? '0ms' : ((actions.length - index) * 25 + 'ms'); | |
159 | + }); | |
160 | + | |
161 | + } else { | |
162 | + element.removeClass('mat-is-open'); | |
163 | + toolbarElement.style.pointerEvents = 'none'; | |
164 | + | |
165 | + backgroundElement.style.transform = 'scale(1)'; | |
166 | + | |
167 | + backgroundElement.style.top = '0'; | |
168 | + | |
169 | + if (element.hasClass('mat-right')) { | |
170 | + backgroundElement.style.left = '0'; | |
171 | + backgroundElement.style.right = null; | |
172 | + } | |
173 | + | |
174 | + if (element.hasClass('mat-left')) { | |
175 | + backgroundElement.style.right = '0'; | |
176 | + backgroundElement.style.left = null; | |
177 | + } | |
178 | + | |
179 | + backgroundElement.style.transitionDelay = disableAnimation ? '0ms' : '200ms'; | |
180 | + | |
181 | + actions.each((index, action) => { | |
182 | + action.style.transitionDelay = (disableAnimation ? 0 : 200) + (index * 25) + 'ms'; | |
183 | + }); | |
184 | + } | |
185 | + } | |
186 | + } | |
187 | + | |
188 | +} | ... | ... |
ui-ngx/src/app/shared/models/alias.models.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { EntityType } from '@shared/models/entity-type.models'; | |
18 | + | |
19 | +export enum AliasFilterType { | |
20 | + singleEntity = 'singleEntity', | |
21 | + entityList = 'entityList', | |
22 | + entityName = 'entityName', | |
23 | + stateEntity = 'stateEntity', | |
24 | + assetType = 'assetType', | |
25 | + deviceType = 'deviceType', | |
26 | + entityViewType = 'entityViewType', | |
27 | + relationsQuery = 'relationsQuery', | |
28 | + assetSearchQuery = 'assetSearchQuery', | |
29 | + deviceSearchQuery = 'deviceSearchQuery', | |
30 | + entityViewSearchQuery = 'entityViewSearchQuery' | |
31 | +} | |
32 | + | |
33 | +export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>( | |
34 | + [ | |
35 | + [ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ], | |
36 | + [ AliasFilterType.entityList, 'alias.filter-type-entity-list' ], | |
37 | + [ AliasFilterType.entityName, 'alias.filter-type-entity-name' ], | |
38 | + [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], | |
39 | + [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], | |
40 | + [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], | |
41 | + [ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ], | |
42 | + [ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ], | |
43 | + [ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ], | |
44 | + [ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ], | |
45 | + [ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ] | |
46 | + ] | |
47 | +); | |
48 | + | |
49 | +export interface EntityAliasFilter { | |
50 | + type: AliasFilterType; | |
51 | + entityType: EntityType; | |
52 | + resolveMultiple: boolean; | |
53 | + entityList?: string[]; | |
54 | + entityNameFilter?: string; | |
55 | + [key: string]: any; | |
56 | + // TODO: | |
57 | + | |
58 | +} | |
59 | + | |
60 | +export interface EntityAlias { | |
61 | + id: string; | |
62 | + alias: string; | |
63 | + filter: EntityAliasFilter; | |
64 | + [key: string]: any; | |
65 | + // TODO: | |
66 | +} | |
67 | + | |
68 | +export interface EntityAliases { | |
69 | + [id: string]: EntityAlias; | |
70 | +} | ... | ... |
... | ... | @@ -14,22 +14,26 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import {BaseData} from '@shared/models/base-data'; | |
18 | -import {DashboardId} from '@shared/models/id/dashboard-id'; | |
19 | -import {TenantId} from '@shared/models/id/tenant-id'; | |
20 | -import {ShortCustomerInfo} from '@shared/models/customer.model'; | |
17 | +import { BaseData } from '@shared/models/base-data'; | |
18 | +import { DashboardId } from '@shared/models/id/dashboard-id'; | |
19 | +import { TenantId } from '@shared/models/id/tenant-id'; | |
20 | +import { ShortCustomerInfo } from '@shared/models/customer.model'; | |
21 | +import { Widget } from './widget.models'; | |
22 | +import { Timewindow } from '@shared/models/time/time.models'; | |
23 | +import { EntityType } from '@shared/models/entity-type.models'; | |
24 | +import { EntityAlias, EntityAliases } from './alias.models'; | |
21 | 25 | |
22 | 26 | export interface DashboardInfo extends BaseData<DashboardId> { |
23 | - tenantId: TenantId; | |
24 | - title: string; | |
25 | - assignedCustomers: Array<ShortCustomerInfo>; | |
27 | + tenantId?: TenantId; | |
28 | + title?: string; | |
29 | + assignedCustomers?: Array<ShortCustomerInfo>; | |
26 | 30 | } |
27 | 31 | |
28 | 32 | export interface WidgetLayout { |
29 | 33 | sizeX: number; |
30 | 34 | sizeY: number; |
31 | - mobileHeight: number; | |
32 | - mobileOrder: number; | |
35 | + mobileHeight?: number; | |
36 | + mobileOrder?: number; | |
33 | 37 | col: number; |
34 | 38 | row: number; |
35 | 39 | } |
... | ... | @@ -38,13 +42,59 @@ export interface WidgetLayouts { |
38 | 42 | [id: string]: WidgetLayout; |
39 | 43 | } |
40 | 44 | |
45 | +export interface GridSettings { | |
46 | + backgroundColor?: string; | |
47 | + color?: string; | |
48 | + columns?: number; | |
49 | + margins?: [number, number]; | |
50 | + backgroundSizeMode?: string; | |
51 | + [key: string]: any; | |
52 | + // TODO: | |
53 | +} | |
54 | + | |
55 | +export interface DashboardLayout { | |
56 | + widgets: WidgetLayouts; | |
57 | + gridSettings: GridSettings; | |
58 | +} | |
59 | + | |
60 | +export declare type DashboardLayoutId = 'main' | 'right'; | |
61 | + | |
62 | +export interface DashboardStateLayouts { | |
63 | + main?: DashboardLayout; | |
64 | + right?: DashboardLayout; | |
65 | +} | |
66 | + | |
67 | +export interface DashboardState { | |
68 | + name: string; | |
69 | + root: boolean; | |
70 | + layouts: DashboardStateLayouts; | |
71 | +} | |
72 | + | |
73 | +export declare type StateControllerId = 'entity' | 'default' | string; | |
74 | + | |
75 | +export interface DashboardSettings { | |
76 | + stateControllerId?: StateControllerId; | |
77 | + showTitle?: boolean; | |
78 | + showDashboardsSelect?: boolean; | |
79 | + showEntitiesSelect?: boolean; | |
80 | + showDashboardTimewindow?: boolean; | |
81 | + showDashboardExport?: boolean; | |
82 | + toolbarAlwaysOpen?: boolean; | |
83 | + titleColor?: string; | |
84 | +} | |
85 | + | |
41 | 86 | export interface DashboardConfiguration { |
87 | + timewindow?: Timewindow; | |
88 | + settings?: DashboardSettings; | |
89 | + widgets?: {[id: string]: Widget } | Widget[]; | |
90 | + states?: {[id: string]: DashboardState }; | |
91 | + entityAliases?: EntityAliases; | |
42 | 92 | [key: string]: any; |
43 | 93 | // TODO: |
44 | 94 | } |
45 | 95 | |
46 | 96 | export interface Dashboard extends DashboardInfo { |
47 | - configuration: DashboardConfiguration; | |
97 | + configuration?: DashboardConfiguration; | |
48 | 98 | } |
49 | 99 | |
50 | 100 | export function isPublicDashboard(dashboard: DashboardInfo): boolean { | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { BaseData } from '@shared/models/base-data'; | |
18 | +import { EntityType } from '@shared/models/entity-type.models'; | |
19 | +import { EntityId } from '@shared/models/id/entity-id'; | |
20 | + | |
21 | +export interface EntityInfo { | |
22 | + origEntity?: BaseData<EntityId>; | |
23 | + name?: string; | |
24 | + label?: string; | |
25 | + entityType?: EntityType; | |
26 | + id?: string; | |
27 | + entityDescription?: string; | |
28 | +} | ... | ... |
... | ... | @@ -38,6 +38,8 @@ export interface WidgetTypeTemplate { |
38 | 38 | |
39 | 39 | export interface WidgetTypeData { |
40 | 40 | name: string; |
41 | + icon: string; | |
42 | + isMdiIcon?: boolean; | |
41 | 43 | template: WidgetTypeTemplate; |
42 | 44 | } |
43 | 45 | |
... | ... | @@ -47,6 +49,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( |
47 | 49 | widgetType.timeseries, |
48 | 50 | { |
49 | 51 | name: 'widget.timeseries', |
52 | + icon: 'timeline', | |
50 | 53 | template: { |
51 | 54 | bundleAlias: 'charts', |
52 | 55 | alias: 'basic_timeseries' |
... | ... | @@ -57,6 +60,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( |
57 | 60 | widgetType.latest, |
58 | 61 | { |
59 | 62 | name: 'widget.latest-values', |
63 | + icon: 'track_changes', | |
60 | 64 | template: { |
61 | 65 | bundleAlias: 'cards', |
62 | 66 | alias: 'attributes_card' |
... | ... | @@ -67,6 +71,8 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( |
67 | 71 | widgetType.rpc, |
68 | 72 | { |
69 | 73 | name: 'widget.rpc', |
74 | + icon: 'mdi:developer-board', | |
75 | + isMdiIcon: true, | |
70 | 76 | template: { |
71 | 77 | bundleAlias: 'gpio_widgets', |
72 | 78 | alias: 'basic_gpio_control' |
... | ... | @@ -77,6 +83,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( |
77 | 83 | widgetType.alarm, |
78 | 84 | { |
79 | 85 | name: 'widget.alarm', |
86 | + icon: 'error', | |
80 | 87 | template: { |
81 | 88 | bundleAlias: 'alarm_widgets', |
82 | 89 | alias: 'alarms_table' |
... | ... | @@ -87,6 +94,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( |
87 | 94 | widgetType.static, |
88 | 95 | { |
89 | 96 | name: 'widget.static', |
97 | + icon: 'font_download', | |
90 | 98 | template: { |
91 | 99 | bundleAlias: 'cards', |
92 | 100 | alias: 'html_card' |
... | ... | @@ -214,7 +222,7 @@ export enum DatasourceType { |
214 | 222 | } |
215 | 223 | |
216 | 224 | export interface Datasource { |
217 | - type: DatasourceType; | |
225 | + type?: DatasourceType | any; | |
218 | 226 | name?: string; |
219 | 227 | dataKeys?: Array<DataKey>; |
220 | 228 | entityType?: EntityType; |
... | ... | @@ -333,7 +341,7 @@ export interface WidgetConfig { |
333 | 341 | |
334 | 342 | export interface Widget { |
335 | 343 | id?: string; |
336 | - typeId: WidgetTypeId; | |
344 | + typeId?: WidgetTypeId; | |
337 | 345 | isSystemType: boolean; |
338 | 346 | bundleAlias: string; |
339 | 347 | typeAlias: string; | ... | ... |
... | ... | @@ -14,9 +14,9 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -export type WindowMessageType = 'widgetException'; | |
17 | +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated'; | |
18 | 18 | |
19 | 19 | export interface WindowMessage { |
20 | 20 | type: WindowMessageType; |
21 | - data: any; | |
21 | + data?: any; | |
22 | 22 | } | ... | ... |
... | ... | @@ -14,12 +14,12 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import {NgModule} from '@angular/core'; | |
18 | -import {CommonModule, DatePipe} from '@angular/common'; | |
19 | -import {FooterComponent} from './components/footer.component'; | |
20 | -import {LogoComponent} from './components/logo.component'; | |
21 | -import {TbSnackBarComponent, ToastDirective} from './components/toast.directive'; | |
22 | -import {BreadcrumbComponent} from '@app/shared/components/breadcrumb.component'; | |
17 | +import { NgModule } from '@angular/core'; | |
18 | +import { CommonModule, DatePipe } from '@angular/common'; | |
19 | +import { FooterComponent } from './components/footer.component'; | |
20 | +import { LogoComponent } from './components/logo.component'; | |
21 | +import { TbSnackBarComponent, ToastDirective } from './components/toast.directive'; | |
22 | +import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component'; | |
23 | 23 | |
24 | 24 | import { |
25 | 25 | MatAutocompleteModule, |
... | ... | @@ -51,43 +51,49 @@ import { |
51 | 51 | MatToolbarModule, |
52 | 52 | MatTooltipModule |
53 | 53 | } from '@angular/material'; |
54 | -import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core'; | |
55 | -import {GridsterModule} from 'angular-gridster2'; | |
56 | -import {FlexLayoutModule} from '@angular/flex-layout'; | |
57 | -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; | |
58 | -import {RouterModule} from '@angular/router'; | |
59 | -import {ShareModule as ShareButtonsModule} from '@ngx-share/core'; | |
60 | -import {UserMenuComponent} from '@shared/components/user-menu.component'; | |
61 | -import {NospacePipe} from './pipe/nospace.pipe'; | |
62 | -import {TranslateModule} from '@ngx-translate/core'; | |
63 | -import {TbCheckboxComponent} from '@shared/components/tb-checkbox.component'; | |
64 | -import {HelpComponent} from '@shared/components/help.component'; | |
65 | -import {TbAnchorComponent} from '@shared/components/tb-anchor.component'; | |
66 | -import {MillisecondsToTimeStringPipe} from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
67 | -import {TimewindowComponent} from '@shared/components/time/timewindow.component'; | |
68 | -import {OverlayModule} from '@angular/cdk/overlay'; | |
69 | -import {TimewindowPanelComponent} from '@shared/components/time/timewindow-panel.component'; | |
70 | -import {TimeintervalComponent} from '@shared/components/time/timeinterval.component'; | |
71 | -import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component'; | |
72 | -import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe'; | |
73 | -import {ClipboardModule} from 'ngx-clipboard'; | |
54 | +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; | |
55 | +import { GridsterModule } from 'angular-gridster2'; | |
56 | +import { FlexLayoutModule } from '@angular/flex-layout'; | |
57 | +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | |
58 | +import { RouterModule } from '@angular/router'; | |
59 | +import { ShareModule as ShareButtonsModule } from '@ngx-share/core'; | |
60 | +import { HotkeyModule } from 'angular2-hotkeys'; | |
61 | +import { UserMenuComponent } from '@shared/components/user-menu.component'; | |
62 | +import { NospacePipe } from './pipe/nospace.pipe'; | |
63 | +import { TranslateModule } from '@ngx-translate/core'; | |
64 | +import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; | |
65 | +import { HelpComponent } from '@shared/components/help.component'; | |
66 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | |
67 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
68 | +import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | |
69 | +import { OverlayModule } from '@angular/cdk/overlay'; | |
70 | +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; | |
71 | +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; | |
72 | +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; | |
73 | +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; | |
74 | +import { ClipboardModule } from 'ngx-clipboard'; | |
74 | 75 | import { ValueInputComponent } from '@shared/components/value-input.component'; |
75 | -import {FullscreenDirective} from '@shared/components/fullscreen.directive'; | |
76 | -import {HighlightPipe} from '@shared/pipe/highlight.pipe'; | |
77 | -import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; | |
78 | -import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component'; | |
79 | -import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component'; | |
80 | -import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component'; | |
81 | -import {EntityListComponent} from '@shared/components/entity/entity-list.component'; | |
82 | -import {EntityTypeSelectComponent} from './components/entity/entity-type-select.component'; | |
83 | -import {EntitySelectComponent} from './components/entity/entity-select.component'; | |
84 | -import {DatetimeComponent} from '@shared/components/time/datetime.component'; | |
85 | -import {EntityKeysListComponent} from './components/entity/entity-keys-list.component'; | |
86 | -import {SocialSharePanelComponent} from './components/socialshare-panel.component'; | |
76 | +import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | |
77 | +import { HighlightPipe } from '@shared/pipe/highlight.pipe'; | |
78 | +import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; | |
79 | +import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component'; | |
80 | +import { EntitySubTypeSelectComponent } from './components/entity/entity-subtype-select.component'; | |
81 | +import { EntityAutocompleteComponent } from './components/entity/entity-autocomplete.component'; | |
82 | +import { EntityListComponent } from '@shared/components/entity/entity-list.component'; | |
83 | +import { EntityTypeSelectComponent } from './components/entity/entity-type-select.component'; | |
84 | +import { EntitySelectComponent } from './components/entity/entity-select.component'; | |
85 | +import { DatetimeComponent } from '@shared/components/time/datetime.component'; | |
86 | +import { EntityKeysListComponent } from './components/entity/entity-keys-list.component'; | |
87 | +import { SocialSharePanelComponent } from './components/socialshare-panel.component'; | |
87 | 88 | import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; |
88 | 89 | import { EntityListSelectComponent } from './components/entity/entity-list-select.component'; |
89 | 90 | import { JsonObjectEditComponent } from './components/json-object-edit.component'; |
90 | 91 | import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; |
92 | +import { CircularProgressDirective } from './components/circular-progress.directive'; | |
93 | +import { MatSpinner } from '@angular/material/progress-spinner'; | |
94 | +import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; | |
95 | +import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; | |
96 | +import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; | |
91 | 97 | |
92 | 98 | @NgModule({ |
93 | 99 | providers: [ |
... | ... | @@ -100,6 +106,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
100 | 106 | TbSnackBarComponent, |
101 | 107 | TbAnchorComponent, |
102 | 108 | TimewindowPanelComponent, |
109 | + DashboardSelectPanelComponent, | |
110 | + MatSpinner | |
103 | 111 | ], |
104 | 112 | declarations: [ |
105 | 113 | FooterComponent, |
... | ... | @@ -107,6 +115,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
107 | 115 | FooterFabButtonsComponent, |
108 | 116 | ToastDirective, |
109 | 117 | FullscreenDirective, |
118 | + CircularProgressDirective, | |
110 | 119 | TbAnchorComponent, |
111 | 120 | HelpComponent, |
112 | 121 | TbCheckboxComponent, |
... | ... | @@ -116,6 +125,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
116 | 125 | TimewindowComponent, |
117 | 126 | TimewindowPanelComponent, |
118 | 127 | TimeintervalComponent, |
128 | + DashboardSelectComponent, | |
129 | + DashboardSelectPanelComponent, | |
119 | 130 | DatetimePeriodComponent, |
120 | 131 | DatetimeComponent, |
121 | 132 | ValueInputComponent, |
... | ... | @@ -131,6 +142,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
131 | 142 | RelationTypeAutocompleteComponent, |
132 | 143 | SocialSharePanelComponent, |
133 | 144 | JsonObjectEditComponent, |
145 | + FabTriggerDirective, | |
146 | + FabActionsDirective, | |
147 | + FabToolbarComponent, | |
134 | 148 | NospacePipe, |
135 | 149 | MillisecondsToTimeStringPipe, |
136 | 150 | EnumToArrayPipe, |
... | ... | @@ -176,7 +190,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
176 | 190 | FormsModule, |
177 | 191 | ReactiveFormsModule, |
178 | 192 | OverlayModule, |
179 | - ShareButtonsModule | |
193 | + ShareButtonsModule, | |
194 | + HotkeyModule | |
180 | 195 | ], |
181 | 196 | exports: [ |
182 | 197 | FooterComponent, |
... | ... | @@ -184,6 +199,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
184 | 199 | FooterFabButtonsComponent, |
185 | 200 | ToastDirective, |
186 | 201 | FullscreenDirective, |
202 | + CircularProgressDirective, | |
187 | 203 | TbAnchorComponent, |
188 | 204 | HelpComponent, |
189 | 205 | TbCheckboxComponent, |
... | ... | @@ -192,6 +208,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
192 | 208 | TimewindowComponent, |
193 | 209 | TimewindowPanelComponent, |
194 | 210 | TimeintervalComponent, |
211 | + DashboardSelectComponent, | |
195 | 212 | DatetimePeriodComponent, |
196 | 213 | DatetimeComponent, |
197 | 214 | DashboardAutocompleteComponent, |
... | ... | @@ -206,6 +223,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
206 | 223 | RelationTypeAutocompleteComponent, |
207 | 224 | SocialSharePanelComponent, |
208 | 225 | JsonObjectEditComponent, |
226 | + FabTriggerDirective, | |
227 | + FabActionsDirective, | |
228 | + FabToolbarComponent, | |
209 | 229 | ValueInputComponent, |
210 | 230 | MatButtonModule, |
211 | 231 | MatCheckboxModule, |
... | ... | @@ -244,6 +264,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons |
244 | 264 | ReactiveFormsModule, |
245 | 265 | OverlayModule, |
246 | 266 | ShareButtonsModule, |
267 | + HotkeyModule, | |
247 | 268 | NospacePipe, |
248 | 269 | MillisecondsToTimeStringPipe, |
249 | 270 | EnumToArrayPipe, | ... | ... |
104 Bytes
91 Bytes
... | ... | @@ -169,6 +169,25 @@ section.tb-header-buttons { |
169 | 169 | } |
170 | 170 | } |
171 | 171 | |
172 | +section.tb-footer-buttons { | |
173 | + position: fixed; | |
174 | + right: 20px; | |
175 | + bottom: 20px; | |
176 | + z-index: 30; | |
177 | + pointer-events: none; | |
178 | + | |
179 | + .tb-btn-footer { | |
180 | + margin: 6px 8px; | |
181 | + position: relative !important; | |
182 | + display: inline-block !important; | |
183 | + animation: tbMoveFromBottomFade .3s ease both; | |
184 | + &.tb-hide { | |
185 | + animation: tbMoveToBottomFade .3s ease both; | |
186 | + } | |
187 | + } | |
188 | +} | |
189 | + | |
190 | + | |
172 | 191 | .tb-details-buttons { |
173 | 192 | button { |
174 | 193 | margin: 6px 8px; |
... | ... | @@ -293,7 +312,7 @@ pre.tb-highlight { |
293 | 312 | } |
294 | 313 | |
295 | 314 | .tb-fullscreen-parent { |
296 | - background: #fff; | |
315 | + background: #eee; | |
297 | 316 | } |
298 | 317 | |
299 | 318 | mat-label { | ... | ... |
... | ... | @@ -166,9 +166,52 @@ $tb-dark-theme: get-tb-dark-theme( |
166 | 166 | $tb-accent |
167 | 167 | ); |
168 | 168 | |
169 | +@mixin mat-fab-toolbar-theme($theme) { | |
170 | + $primary: map-get($theme, primary); | |
171 | + $accent: map-get($theme, accent); | |
172 | + $warn: map-get($theme, warn); | |
173 | + $background: map-get($theme, background); | |
174 | + $foreground: map-get($theme, foreground); | |
175 | + | |
176 | + mat-fab-toolbar { | |
177 | + .mat-fab-toolbar-background { | |
178 | + background: mat-color($background, app-bar); | |
179 | + color: mat-color($foreground, text); | |
180 | + } | |
181 | + &.mat-primary { | |
182 | + .mat-fab-toolbar-background { | |
183 | + @include _mat-toolbar-color($primary); | |
184 | + } | |
185 | + } | |
186 | + &.mat-accent { | |
187 | + .mat-fab-toolbar-background { | |
188 | + @include _mat-toolbar-color($accent); | |
189 | + } | |
190 | + } | |
191 | + &.mat-warn { | |
192 | + .mat-fab-toolbar-background { | |
193 | + @include _mat-toolbar-color($warn); | |
194 | + } | |
195 | + } | |
196 | + } | |
197 | +} | |
198 | + | |
199 | +@mixin tb-components-theme($theme) { | |
200 | + $primary: map-get($theme, primary); | |
201 | + | |
202 | + mat-toolbar{ | |
203 | + &.mat-hue-3 { | |
204 | + background-color: mat-color($primary, 'A100'); | |
205 | + } | |
206 | + } | |
207 | + | |
208 | + @include mat-fab-toolbar-theme($tb-theme); | |
209 | +} | |
210 | + | |
169 | 211 | .tb-default { |
170 | 212 | @include angular-material-theme($tb-theme); |
171 | 213 | @include mat-datetimepicker-theme($tb-theme); |
214 | + @include tb-components-theme($tb-theme); | |
172 | 215 | } |
173 | 216 | |
174 | 217 | .tb-dark { |
... | ... | @@ -388,11 +431,13 @@ $tb-dark-theme: get-tb-dark-theme( |
388 | 431 | width: 32px; |
389 | 432 | height: 32px; |
390 | 433 | line-height: 32px; |
434 | + padding: 0 !important; | |
391 | 435 | } |
392 | 436 | &.tb-mat-96 { |
393 | 437 | width: 96px; |
394 | 438 | height: 96px; |
395 | 439 | line-height: 96px; |
440 | + padding: 0 !important; | |
396 | 441 | } |
397 | 442 | } |
398 | 443 | |
... | ... | @@ -549,6 +594,27 @@ $tb-dark-theme: get-tb-dark-theme( |
549 | 594 | right: 0; |
550 | 595 | } |
551 | 596 | |
597 | + .tb-progress-cover { | |
598 | + position: absolute; | |
599 | + top: 0; | |
600 | + right: 0; | |
601 | + bottom: 0; | |
602 | + left: 0; | |
603 | + z-index: 6; | |
604 | + background-color: #eee; | |
605 | + opacity: 1; | |
606 | + } | |
607 | + | |
608 | + .mat-button.tb-fullscreen-button-style, | |
609 | + .tb-fullscreen-button-style { | |
610 | + background: #ccc; | |
611 | + opacity: .85; | |
612 | + | |
613 | + mat-icon { | |
614 | + color: #666; | |
615 | + } | |
616 | + } | |
617 | + | |
552 | 618 | span.no-data-found { |
553 | 619 | position: relative; |
554 | 620 | display: flex; | ... | ... |
ui-ngx/src/typings/split.js.typings.d.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +interface SplitOptions { | |
18 | + sizes?: number[]; | |
19 | + minSize?: number[] | number; | |
20 | + gutterSize?: number; | |
21 | + snapOffset?: number; | |
22 | + direction?: 'horizontal' | 'vertical'; | |
23 | + cursor?: 'col-resize' | 'row-resize'; | |
24 | + gutter?: (index: number, direction: string) => HTMLElement; | |
25 | + elementStyle?: (dimension: string, elementSize: number, gutterSize: number) => any; | |
26 | + gutterStyle?: (dimension: string, gutterSize: number) => any; | |
27 | + onDrag?: () => void; | |
28 | + onDragStart?: () => void; | |
29 | + onDragEnd?: () => void; | |
30 | +} | |
31 | + | |
32 | +interface SplitObject { | |
33 | + setSizes: (sizes: number[]) => void; | |
34 | + getSizes: () => number[]; | |
35 | + collapse: (index: number) => void; | |
36 | + destroy: () => void; | |
37 | +} | |
38 | + | |
39 | +declare function Split(elements: HTMLElement | string[], options?: SplitOptions): SplitObject; | ... | ... |
... | ... | @@ -14,7 +14,8 @@ |
14 | 14 | "typeRoots": [ |
15 | 15 | "node_modules/@types", |
16 | 16 | "src/typings/jquery.typings.d.ts", |
17 | - "src/typings/jquery.flot.typings.d.ts" | |
17 | + "src/typings/jquery.flot.typings.d.ts", | |
18 | + "src/typings/split.js.typings.d.ts" | |
18 | 19 | ], |
19 | 20 | "paths": { |
20 | 21 | "@app/*": ["src/app/*"], | ... | ... |