Browse Source

Implement Widget Editor. Dashboard page initial implementation.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
700293d5f9
  1. 4
      ui-ngx/angular.json
  2. 136
      ui-ngx/package-lock.json
  3. 4
      ui-ngx/package.json
  4. 4
      ui-ngx/src/app/app.module.ts
  5. 77
      ui-ngx/src/app/core/api/alias-controller.ts
  6. 38
      ui-ngx/src/app/core/api/widget-api.models.ts
  7. 5
      ui-ngx/src/app/core/api/widget-subscription.ts
  8. 2
      ui-ngx/src/app/core/auth/auth.service.ts
  9. 2
      ui-ngx/src/app/core/core.module.ts
  10. 23
      ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts
  11. 23
      ui-ngx/src/app/core/http/widget.service.ts
  12. 13
      ui-ngx/src/app/core/interceptors/load.selectors.ts
  13. 348
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  14. 7
      ui-ngx/src/app/core/services/utils.service.ts
  15. 4
      ui-ngx/src/app/core/utils.ts
  16. 36
      ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts
  17. 104
      ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts
  18. 43
      ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts
  19. 30
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html
  20. 35
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss
  21. 60
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts
  22. 32
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html
  23. 38
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss
  24. 178
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts
  25. 8
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  26. 5
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  27. 4
      ui-ngx/src/app/modules/home/home.component.html
  28. 2
      ui-ngx/src/app/modules/home/home.component.ts
  29. 5
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  30. 157
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  31. 137
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss
  32. 438
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  33. 48
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts
  34. 50
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts
  35. 35
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html
  36. 185
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss
  37. 47
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts
  38. 24
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  39. 8
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  40. 59
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html
  41. 50
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss
  42. 52
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts
  43. 255
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  44. 186
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  45. 626
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  46. 123
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  47. 43
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  48. 9
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  49. 6
      ui-ngx/src/app/shared/components/breadcrumb.component.html
  50. 18
      ui-ngx/src/app/shared/components/breadcrumb.component.ts
  51. 3
      ui-ngx/src/app/shared/components/breadcrumb.ts
  52. 83
      ui-ngx/src/app/shared/components/circular-progress.directive.ts
  53. 28
      ui-ngx/src/app/shared/components/dashboard-select-panel.component.html
  54. 55
      ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss
  55. 48
      ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts
  56. 35
      ui-ngx/src/app/shared/components/dashboard-select.component.html
  57. 40
      ui-ngx/src/app/shared/components/dashboard-select.component.scss
  58. 214
      ui-ngx/src/app/shared/components/dashboard-select.component.ts
  59. 22
      ui-ngx/src/app/shared/components/fab-toolbar.component.html
  60. 187
      ui-ngx/src/app/shared/components/fab-toolbar.component.scss
  61. 188
      ui-ngx/src/app/shared/components/fab-toolbar.component.ts
  62. 70
      ui-ngx/src/app/shared/models/alias.models.ts
  63. 70
      ui-ngx/src/app/shared/models/dashboard.models.ts
  64. 28
      ui-ngx/src/app/shared/models/entity.models.ts
  65. 12
      ui-ngx/src/app/shared/models/widget.models.ts
  66. 4
      ui-ngx/src/app/shared/models/window-message.model.ts
  67. 99
      ui-ngx/src/app/shared/shared.module.ts
  68. BIN
      ui-ngx/src/assets/split.js/grips/horizontal.png
  69. BIN
      ui-ngx/src/assets/split.js/grips/vertical.png
  70. 21
      ui-ngx/src/styles.scss
  71. 66
      ui-ngx/src/theme.scss
  72. 2
      ui-ngx/src/tsconfig.app.json
  73. 39
      ui-ngx/src/typings/split.js.typings.d.ts
  74. 3
      ui-ngx/tsconfig.json

4
ui-ngx/angular.json

@ -47,6 +47,10 @@
"node_modules/flot/src/plugins/jquery.flot.stack.js", "node_modules/flot/src/plugins/jquery.flot.stack.js",
"node_modules/flot.curvedlines/curvedLines.js", "node_modules/flot.curvedlines/curvedLines.js",
"node_modules/tinycolor2/dist/tinycolor-min.js", "node_modules/tinycolor2/dist/tinycolor-min.js",
"node_modules/split.js/dist/split.js",
"node_modules/js-beautify/js/lib/beautify.js",
"node_modules/js-beautify/js/lib/beautify-css.js",
"node_modules/js-beautify/js/lib/beautify-html.js",
"node_modules/ace-builds/src-min/ace.js", "node_modules/ace-builds/src-min/ace.js",
"node_modules/ace-builds/src-min/ext-language_tools.js", "node_modules/ace-builds/src-min/ext-language_tools.js",
"node_modules/ace-builds/src-min/ext-searchbox.js", "node_modules/ace-builds/src-min/ext-searchbox.js",

136
ui-ngx/package-lock.json

@ -1170,12 +1170,23 @@
"@types/sizzle": "*" "@types/sizzle": "*"
} }
}, },
"@types/js-beautify": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.8.1.tgz",
"integrity": "sha512-B1Br8yE27obcYvFx5ECZswT/947aAFNb9lHqnkUOhtOfvJqaa6Axibo4T+5G6iQlUfjgSd8am9R/9j9UBfRlrw==",
"dev": true
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true "dev": true
}, },
"@types/mousetrap": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz",
"integrity": "sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew=="
},
"@types/node": { "@types/node": {
"version": "10.14.15", "version": "10.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz",
@ -1434,6 +1445,11 @@
"through": ">=2.2.7 <3" "through": ">=2.2.7 <3"
} }
}, },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"accepts": { "accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -1523,6 +1539,15 @@
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }
}, },
"angular2-hotkeys": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz",
"integrity": "sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA==",
"requires": {
"@types/mousetrap": "^1.6.0",
"mousetrap": "^1.6.0"
}
},
"ansi-colors": { "ansi-colors": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@ -1955,8 +1980,7 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"dev": true
}, },
"base": { "base": {
"version": "0.11.2", "version": "0.11.2",
@ -2143,7 +2167,6 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2854,8 +2877,7 @@
"commander": { "commander": {
"version": "2.20.0", "version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
"dev": true
}, },
"commondir": { "commondir": {
"version": "1.0.1", "version": "1.0.1",
@ -3107,8 +3129,7 @@
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"dev": true
}, },
"concat-stream": { "concat-stream": {
"version": "1.6.2", "version": "1.6.2",
@ -3122,6 +3143,15 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"config-chain": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"requires": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"connect": { "connect": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
@ -3762,6 +3792,17 @@
"safer-buffer": "^2.1.0" "safer-buffer": "^2.1.0"
} }
}, },
"editorconfig": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
"integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
"requires": {
"commander": "^2.19.0",
"lru-cache": "^4.1.5",
"semver": "^5.6.0",
"sigmund": "^1.0.1"
}
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -4574,8 +4615,7 @@
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"dev": true
}, },
"fsevents": { "fsevents": {
"version": "1.2.9", "version": "1.2.9",
@ -5183,7 +5223,6 @@
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": { "requires": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@ -5709,7 +5748,6 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": { "requires": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@ -5718,14 +5756,12 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
"dev": true
}, },
"inquirer": { "inquirer": {
"version": "6.5.0", "version": "6.5.0",
@ -6444,6 +6480,18 @@
} }
} }
}, },
"js-beautify": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz",
"integrity": "sha512-ZtBYyNUYJIsBWERnQP0rPN9KjkrDfJcMjuVGcvXOUJrD1zmOGwhRwQ4msG+HJ+Ni/FA7+sRQEMYVzdTQDvnzvQ==",
"requires": {
"config-chain": "^1.1.12",
"editorconfig": "^0.15.3",
"glob": "^7.1.3",
"mkdirp": "~0.5.1",
"nopt": "~4.0.1"
}
},
"js-tokens": { "js-tokens": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -6918,7 +6966,6 @@
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"requires": { "requires": {
"pseudomap": "^1.0.2", "pseudomap": "^1.0.2",
"yallist": "^2.1.2" "yallist": "^2.1.2"
@ -7258,7 +7305,6 @@
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -7368,7 +7414,6 @@
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
}, },
@ -7376,8 +7421,7 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"dev": true
} }
} }
}, },
@ -7386,6 +7430,11 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
}, },
"mousetrap": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.3.tgz",
"integrity": "sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA=="
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -7563,6 +7612,15 @@
"semver": "^5.3.0" "semver": "^5.3.0"
} }
}, },
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -7801,7 +7859,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -7877,8 +7934,7 @@
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
"dev": true
}, },
"os-locale": { "os-locale": {
"version": "3.1.0", "version": "3.1.0",
@ -7894,14 +7950,12 @@
"os-tmpdir": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
"dev": true
}, },
"osenv": { "osenv": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"requires": { "requires": {
"os-homedir": "^1.0.0", "os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0" "os-tmpdir": "^1.0.0"
@ -8222,8 +8276,7 @@
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
"dev": true
}, },
"path-is-inside": { "path-is-inside": {
"version": "1.0.2", "version": "1.0.2",
@ -8465,6 +8518,11 @@
"retry": "^0.10.0" "retry": "^0.10.0"
} }
}, },
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk="
},
"protoduck": { "protoduck": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz",
@ -8612,8 +8670,7 @@
"pseudomap": { "pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
"dev": true
}, },
"psl": { "psl": {
"version": "1.3.0", "version": "1.3.0",
@ -9208,8 +9265,7 @@
"semver": { "semver": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
"dev": true
}, },
"semver-dsl": { "semver-dsl": {
"version": "1.0.1", "version": "1.0.1",
@ -9408,6 +9464,11 @@
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true "dev": true
}, },
"sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA="
},
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@ -9920,6 +9981,11 @@
"extend-shallow": "^3.0.0" "extend-shallow": "^3.0.0"
} }
}, },
"split.js": {
"version": "1.5.11",
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.5.11.tgz",
"integrity": "sha512-ec0sAbWnaMGpNHWo1ZgIlF3Mx7GzSyaO0GlcEBZGIFZQwYPPkbDV6JRpDmpzIshVig7USREuEPudy0ygQaskXg=="
},
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -11121,8 +11187,7 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"dev": true
}, },
"ws": { "ws": {
"version": "3.3.3", "version": "3.3.3",
@ -11180,8 +11245,7 @@
"yallist": { "yallist": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
"dev": true
}, },
"yargs": { "yargs": {
"version": "12.0.5", "version": "12.0.5",

4
ui-ngx/package.json

@ -33,6 +33,7 @@
"@ngx-translate/http-loader": "^4.0.0", "@ngx-translate/http-loader": "^4.0.0",
"ace-builds": "^1.4.5", "ace-builds": "^1.4.5",
"angular-gridster2": "^8.1.0", "angular-gridster2": "^8.1.0",
"angular2-hotkeys": "^2.1.5",
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"compass-sass-mixins": "^0.12.7", "compass-sass-mixins": "^0.12.7",
"core-js": "^3.1.4", "core-js": "^3.1.4",
@ -44,6 +45,7 @@
"javascript-detect-element-resize": "^0.5.3", "javascript-detect-element-resize": "^0.5.3",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"jquery.terminal": "^2.8.0", "jquery.terminal": "^2.8.0",
"js-beautify": "^1.10.2",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"messageformat": "^2.3.0", "messageformat": "^2.3.0",
"moment": "^2.24.0", "moment": "^2.24.0",
@ -51,6 +53,7 @@
"ngx-translate-messageformat-compiler": "^4.5.0", "ngx-translate-messageformat-compiler": "^4.5.0",
"rxjs": "~6.5.2", "rxjs": "~6.5.2",
"screenfull": "^4.2.1", "screenfull": "^4.2.1",
"split.js": "^1.5.11",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"typeface-roboto": "^0.0.75", "typeface-roboto": "^0.0.75",
@ -66,6 +69,7 @@
"@types/jasmine": "~3.4.0", "@types/jasmine": "~3.4.0",
"@types/jasminewd2": "~2.0.6", "@types/jasminewd2": "~2.0.6",
"@types/jquery": "^3.3.31", "@types/jquery": "^3.3.31",
"@types/js-beautify": "^1.8.1",
"@types/node": "~10.14.15", "@types/node": "~10.14.15",
"@types/tinycolor2": "^1.4.2", "@types/tinycolor2": "^1.4.2",
"codelyzer": "~5.1.0", "codelyzer": "~5.1.0",

4
ui-ngx/src/app/app.module.ts

@ -24,6 +24,7 @@ import { LoginModule } from './modules/login/login.module';
import { HomeModule } from './modules/home/home.module'; import { HomeModule } from './modules/home/home.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -35,7 +36,8 @@ import { AppComponent } from './app.component';
AppRoutingModule, AppRoutingModule,
CoreModule, CoreModule,
LoginModule, LoginModule,
HomeModule HomeModule,
DashboardRoutingModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

77
ui-ngx/src/app/core/api/alias-controller.ts

@ -14,19 +14,26 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { IAliasController, AliasInfo } from '@core/api/widget-api.models'; import { IAliasController, AliasInfo, IStateController } from '@core/api/widget-api.models';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { Datasource } from '@app/shared/models/widget.models'; import { Datasource } from '@app/shared/models/widget.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { EntityService } from '@core/http/entity.service';
import { UtilsService } from '@core/services/utils.service';
import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@shared/models/entity.models';
import * as equal from 'deep-equal';
export class DummyAliasController implements IAliasController { export class DummyAliasController implements IAliasController {
entityAliasesChanged: Observable<Array<string>>; entityAliasesChanged: Observable<Array<string>>;
entityAliasResolved: Observable<string>;
[key: string]: any | null; [key: string]: any | null;
constructor() { constructor() {
this.entityAliasesChanged = new Subject<Array<string>>().asObservable(); this.entityAliasesChanged = new Subject<Array<string>>().asObservable();
this.entityAliasResolved = new Subject<string>().asObservable();
} }
getAliasInfo(aliasId): Observable<AliasInfo> { getAliasInfo(aliasId): Observable<AliasInfo> {
@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController {
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> { resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
return of(deepClone(datasources)); return of(deepClone(datasources));
} }
getEntityAliases(): EntityAliases {
return undefined;
}
getInstantAliasInfo(aliasId: string): AliasInfo {
return undefined;
}
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) {
}
updateEntityAliases(entityAliases: EntityAliases) {
}
}
export class AliasController implements IAliasController {
private entityAliasesChangedSubject = new Subject<Array<string>>();
entityAliasesChanged: Observable<Array<string>> = this.entityAliasesChangedSubject.asObservable();
private entityAliasResolvedSubject = new Subject<string>();
entityAliasResolved: Observable<string> = this.entityAliasResolvedSubject.asObservable();
entityAliases: EntityAliases;
resolvedAliases: {[aliasId: string]: AliasInfo} = {};
[key: string]: any | null;
constructor(private utils: UtilsService,
private entityService: EntityService,
private stateController: IStateController,
private origEntityAliases: EntityAliases) {
this.entityAliases = deepClone(this.origEntityAliases);
}
getAliasInfo(aliasId: string): Observable<AliasInfo> {
return of(null);
}
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>> {
return of(deepClone(datasources));
}
getEntityAliases(): EntityAliases {
return this.entityAliases;
}
getInstantAliasInfo(aliasId: string): AliasInfo {
return this.resolvedAliases[aliasId];
}
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo) {
const aliasInfo = this.resolvedAliases[aliasId];
if (aliasInfo) {
const prevCurrentEntity = aliasInfo.currentEntity;
if (!equal(currentEntity, prevCurrentEntity)) {
aliasInfo.currentEntity = currentEntity;
this.entityAliasesChangedSubject.next([aliasId]);
}
}
}
updateEntityAliases(entityAliases: EntityAliases) {
}
} }

38
ui-ngx/src/app/core/api/widget-api.models.ts

@ -34,6 +34,8 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { DatasourceService } from '@core/api/datasource.service'; import { DatasourceService } from '@core/api/datasource.service';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { EntityAliases } from '@shared/models/alias.models';
import { EntityInfo } from '@app/shared/models/entity.models';
export interface TimewindowFunctions { export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -66,20 +68,24 @@ export interface WidgetActionsApi {
} }
export interface AliasInfo { export interface AliasInfo {
alias?: string;
stateEntity?: boolean; stateEntity?: boolean;
currentEntity?: { currentEntity?: EntityInfo;
id: string; selectedId?: string;
entityType: EntityType; resolvedEntities?: Array<EntityInfo>;
name?: string;
};
[key: string]: any | null; [key: string]: any | null;
// TODO: // TODO:
} }
export interface IAliasController { export interface IAliasController {
entityAliasesChanged: Observable<Array<string>>; entityAliasesChanged: Observable<Array<string>>;
getAliasInfo(aliasId): Observable<AliasInfo>; entityAliasResolved: Observable<string>;
getAliasInfo(aliasId: string): Observable<AliasInfo>;
getInstantAliasInfo(aliasId: string): AliasInfo;
resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>; resolveDatasources(datasources: Array<Datasource>): Observable<Array<Datasource>>;
getEntityAliases(): EntityAliases;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
updateEntityAliases(entityAliases: EntityAliases);
[key: string]: any | null; [key: string]: any | null;
// TODO: // TODO:
} }
@ -97,17 +103,14 @@ export interface StateParams {
} }
export interface IStateController { export interface IStateController {
getStateParams: () => StateParams; getStateParams?: () => StateParams;
openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void; openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void;
updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void;
openRightLayout: () => void;
preserveState?: () => void;
// TODO: // TODO:
} }
export interface EntityInfo {
entityId: EntityId;
entityName: string;
}
export interface SubscriptionInfo { export interface SubscriptionInfo {
type: DatasourceType; type: DatasourceType;
name?: string; name?: string;
@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions {
// TODO: // TODO:
} }
export interface SubscriptionEntityInfo {
entityId: EntityId;
entityName: string;
}
export interface IWidgetSubscription { export interface IWidgetSubscription {
id: string; id: string;
@ -201,7 +209,7 @@ export interface IWidgetSubscription {
rpcErrorText?: string; rpcErrorText?: string;
rpcRejection?: HttpErrorResponse; rpcRejection?: HttpErrorResponse;
getFirstEntityInfo(): EntityInfo; getFirstEntityInfo(): SubscriptionEntityInfo;
onAliasesChanged(aliasIds: Array<string>): boolean; onAliasesChanged(aliasIds: Array<string>): boolean;

5
ui-ngx/src/app/core/api/widget-subscription.ts

@ -15,8 +15,7 @@
/// ///
import { import {
EntityInfo, IWidgetSubscription, SubscriptionEntityInfo,
IWidgetSubscription,
WidgetSubscriptionCallbacks, WidgetSubscriptionCallbacks,
WidgetSubscriptionContext, WidgetSubscriptionContext,
WidgetSubscriptionOptions WidgetSubscriptionOptions
@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated(); this.onDataUpdated();
} }
getFirstEntityInfo(): EntityInfo { getFirstEntityInfo(): SubscriptionEntityInfo {
return undefined; return undefined;
} }

2
ui-ngx/src/app/core/auth/auth.service.ts

@ -42,6 +42,8 @@ import {TimeService} from '@core/services/time.service';
}) })
export class AuthService { export class AuthService {
forceFullscreen = false; // TODO:
constructor( constructor(
private store: Store<AppState>, private store: Store<AppState>,
private http: HttpClient, private http: HttpClient,

2
ui-ngx/src/app/core/core.module.ts

@ -39,6 +39,7 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { WINDOW_PROVIDERS } from '@core/services/window.service';
import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component"; import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
import { HotkeyModule } from 'angular2-hotkeys';
export function HttpLoaderFactory(http: HttpClient) { export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
@ -79,6 +80,7 @@ export function HttpLoaderFactory(http: HttpClient) {
useClass: TranslateDefaultCompiler useClass: TranslateDefaultCompiler
} }
}), }),
HotkeyModule.forRoot(),
// ngrx // ngrx
StoreModule.forRoot(reducers, { metaReducers }), StoreModule.forRoot(reducers, { metaReducers }),

23
ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts

@ -28,21 +28,26 @@ import { selectAuth } from '@core/auth/auth.selectors';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isDefined } from '../utils';
export interface HasConfirmForm { export interface HasConfirmForm {
confirmForm(): FormGroup; confirmForm(): FormGroup;
} }
export interface HasDirtyFlag {
isDirty: boolean;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> { export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm & HasDirtyFlag> {
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService) { } private translate: TranslateService) { }
canDeactivate(component: HasConfirmForm, canDeactivate(component: HasConfirmForm & HasDirtyFlag,
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) { state: RouterStateSnapshot) {
@ -54,9 +59,17 @@ export class ConfirmOnExitGuard implements CanDeactivate<HasConfirmForm> {
} }
); );
if (component.confirmForm && auth && auth.isAuthenticated) { if (auth && auth.isAuthenticated) {
const confirmForm = component.confirmForm(); let isDirty = false;
if (confirmForm && confirmForm.dirty) { if (component.confirmForm) {
const confirmForm = component.confirmForm();
if (confirmForm) {
isDirty = confirmForm.dirty;
}
} else if (isDefined(component.isDirty)) {
isDirty = component.isDirty;
}
if (isDirty) {
return this.dialogService.confirm( return this.dialogService.confirm(
this.translate.instant('confirm-on-exit.title'), this.translate.instant('confirm-on-exit.title'),
this.translate.instant('confirm-on-exit.html-message') this.translate.instant('confirm-on-exit.html-message')

23
ui-ngx/src/app/core/http/widget.service.ts

@ -21,10 +21,12 @@ import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetType } from '@shared/models/widget.models'; import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '../services/resources.service'; import { ResourcesService } from '../services/resources.service';
import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models';
import { map } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -70,4 +72,23 @@ export class WidgetService {
return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
defaultHttpOptions(ignoreLoading, ignoreErrors)); defaultHttpOptions(ignoreLoading, ignoreErrors));
} }
public getWidgetTypeById(widgetTypeId: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getWidgetTemplate(widgetTypeParam: widgetType,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetInfo> {
const templateWidgetType = widgetTypesData.get(widgetTypeParam);
return this.getWidgetType(templateWidgetType.template.bundleAlias, templateWidgetType.template.alias, true,
ignoreErrors, ignoreLoading).pipe(
map((result) => {
const widgetInfo = toWidgetInfo(result);
widgetInfo.alias = undefined;
return widgetInfo;
})
);
}
} }

13
ui-ngx/src/app/core/interceptors/load.selectors.ts

@ -14,10 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
import { LoadState } from './load.models'; import { LoadState } from './load.models';
import { AuthUser } from '@shared/models/user.model';
import { take } from 'rxjs/operators';
import { selectAuthUser } from '@core/auth/auth.selectors';
export const selectLoadState = createFeatureSelector<AppState, LoadState>( export const selectLoadState = createFeatureSelector<AppState, LoadState>(
'load' 'load'
@ -32,3 +35,11 @@ export const selectIsLoading = createSelector(
selectLoadState, selectLoadState,
(state: LoadState) => state.isLoading (state: LoadState) => state.isLoading
); );
export function getCurrentIsLoading(store: Store<AppState>): boolean {
let isLoading: boolean;
store.pipe(select(selectIsLoading), take(1)).subscribe(
val => isLoading = val
);
return isLoading;
}

348
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -0,0 +1,348 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { UtilsService } from '@core/services/utils.service';
import { TimeService } from '@core/services/time.service';
import {
Dashboard,
DashboardLayout,
DashboardStateLayouts,
DashboardState,
DashboardConfiguration
} from '@shared/models/dashboard.models';
import { isUndefined, isDefined, isString } from '@core/utils';
import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityAlias, AliasFilterType } from '@app/shared/models/alias.models';
@Injectable({
providedIn: 'root'
})
export class DashboardUtilsService {
constructor(private utils: UtilsService,
private timeService: TimeService) {
}
public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
if (!dashboard.configuration) {
dashboard.configuration = {};
}
if (isUndefined(dashboard.configuration.widgets)) {
dashboard.configuration.widgets = {};
} else if (Array.isArray(dashboard.configuration.widgets)) {
const widgetsMap: {[id: string]: Widget} = {};
dashboard.configuration.widgets.forEach((widget) => {
if (!widget.id) {
widget.id = this.utils.guid();
}
widgetsMap[widget.id] = widget;
});
dashboard.configuration.widgets = widgetsMap;
}
for (const id of Object.keys(dashboard.configuration.widgets)) {
const widget = dashboard.configuration.widgets[id];
dashboard.configuration.widgets[id] = this.validateAndUpdateWidget(widget);
}
if (isUndefined(dashboard.configuration.states)) {
dashboard.configuration.states = {
default: this.createDefaultState(dashboard.title, true)
};
const mainLayout = dashboard.configuration.states.default.layouts.main;
for (const id of Object.keys(dashboard.configuration.widgets)) {
const widget = dashboard.configuration.widgets[id];
mainLayout.widgets[id] = {
sizeX: widget.sizeX,
sizeY: widget.sizeY,
row: widget.row,
col: widget.col
};
if (isDefined(widget.config.mobileHeight)) {
mainLayout.widgets[id].mobileHeight = widget.config.mobileHeight;
}
if (isDefined(widget.config.mobileOrder)) {
mainLayout.widgets[id].mobileOrder = widget.config.mobileOrder;
}
}
} else {
const states = dashboard.configuration.states;
let rootFound = false;
for (const stateId of Object.keys(states)) {
const state = states[stateId];
if (isUndefined(state.root)) {
state.root = false;
} else if (state.root) {
rootFound = true;
}
}
if (!rootFound) {
const firstStateId = Object.keys(states)[0];
states[firstStateId].root = true;
}
}
const datasourcesByAliasId: {[aliasId: string]: Array<Datasource>} = {};
const targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>} = {};
for (const widgetId of Object.keys(dashboard.configuration.widgets)) {
const widget = dashboard.configuration.widgets[widgetId];
widget.config.datasources.forEach((datasource) => {
if (datasource.entityAliasId) {
const aliasId = datasource.entityAliasId;
let aliasDatasources = datasourcesByAliasId[aliasId];
if (!aliasDatasources) {
aliasDatasources = [];
datasourcesByAliasId[aliasId] = aliasDatasources;
}
aliasDatasources.push(datasource);
}
});
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length) {
const aliasId = widget.config.targetDeviceAliasIds[0];
let targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
if (!targetDeviceAliasIdsList) {
targetDeviceAliasIdsList = [];
targetDevicesByAliasId[aliasId] = targetDeviceAliasIdsList;
}
targetDeviceAliasIdsList.push(widget.config.targetDeviceAliasIds);
}
}
dashboard.configuration = this.validateAndUpdateEntityAliases(dashboard.configuration, datasourcesByAliasId, targetDevicesByAliasId);
if (isUndefined(dashboard.configuration.timewindow)) {
dashboard.configuration.timewindow = this.timeService.defaultTimewindow();
}
if (isUndefined(dashboard.configuration.settings)) {
dashboard.configuration.settings = {};
dashboard.configuration.settings.stateControllerId = 'entity';
dashboard.configuration.settings.showTitle = false;
dashboard.configuration.settings.showDashboardsSelect = true;
dashboard.configuration.settings.showEntitiesSelect = true;
dashboard.configuration.settings.showDashboardTimewindow = true;
dashboard.configuration.settings.showDashboardExport = true;
dashboard.configuration.settings.toolbarAlwaysOpen = true;
} else {
if (isUndefined(dashboard.configuration.settings.stateControllerId)) {
dashboard.configuration.settings.stateControllerId = 'entity';
}
}
if (isDefined(dashboard.configuration.gridSettings)) {
const gridSettings = dashboard.configuration.gridSettings;
if (isDefined(gridSettings.showTitle)) {
dashboard.configuration.settings.showTitle = gridSettings.showTitle;
delete gridSettings.showTitle;
}
if (isDefined(gridSettings.titleColor)) {
dashboard.configuration.settings.titleColor = gridSettings.titleColor;
delete gridSettings.titleColor;
}
if (isDefined(gridSettings.showDevicesSelect)) {
dashboard.configuration.settings.showEntitiesSelect = gridSettings.showDevicesSelect;
delete gridSettings.showDevicesSelect;
}
if (isDefined(gridSettings.showEntitiesSelect)) {
dashboard.configuration.settings.showEntitiesSelect = gridSettings.showEntitiesSelect;
delete gridSettings.showEntitiesSelect;
}
if (isDefined(gridSettings.showDashboardTimewindow)) {
dashboard.configuration.settings.showDashboardTimewindow = gridSettings.showDashboardTimewindow;
delete gridSettings.showDashboardTimewindow;
}
if (isDefined(gridSettings.showDashboardExport)) {
dashboard.configuration.settings.showDashboardExport = gridSettings.showDashboardExport;
delete gridSettings.showDashboardExport;
}
dashboard.configuration.states.default.layouts.main.gridSettings = gridSettings;
delete dashboard.configuration.gridSettings;
}
return dashboard;
}
public createSingleWidgetDashboard(widget: Widget): Dashboard {
if (!widget.id) {
widget.id = this.utils.guid();
}
let dashboard: Dashboard = {};
dashboard = this.validateAndUpdateDashboard(dashboard);
dashboard.configuration.widgets[widget.id] = widget;
dashboard.configuration.states.default.layouts.main.widgets[widget.id] = {
sizeX: widget.sizeX,
sizeY: widget.sizeY,
row: widget.row,
col: widget.col,
};
return dashboard;
}
public validateAndUpdateWidget(widget: Widget): Widget {
if (!widget.config) {
widget.config = {};
}
if (!widget.config.datasources) {
widget.config.datasources = [];
}
widget.config.datasources.forEach((datasource) => {
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
}
if (datasource.deviceAliasId) {
datasource.entityAliasId = datasource.deviceAliasId;
delete datasource.deviceAliasId;
}
});
// TODO: Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_timeseries';
}
return widget;
}
public createDefaultLayoutData(): DashboardLayout {
return {
widgets: {},
gridSettings: {
backgroundColor: '#eeeeee',
color: 'rgba(0,0,0,0.870588)',
columns: 24,
margins: [10, 10],
backgroundSizeMode: '100%'
}
};
}
public createDefaultLayouts(): DashboardStateLayouts {
return {
main: this.createDefaultLayoutData()
};
}
public createDefaultState(name: string, root: boolean): DashboardState {
return {
name,
root,
layouts: this.createDefaultLayouts()
};
}
private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {
let entityAlias: EntityAlias;
if (isUndefined(configuration.entityAliases)) {
configuration.entityAliases = {};
if (configuration.deviceAliases) {
const deviceAliases = configuration.deviceAliases;
for (const aliasId of Object.keys(deviceAliases)) {
const deviceAlias = deviceAliases[aliasId];
entityAlias = this.validateAndUpdateDeviceAlias(aliasId, deviceAlias, datasourcesByAliasId, targetDevicesByAliasId);
configuration.entityAliases[entityAlias.id] = entityAlias;
}
delete configuration.deviceAliases;
}
} else {
const entityAliases = configuration.entityAliases;
for (const aliasId of Object.keys(entityAliases)) {
entityAlias = entityAliases[aliasId];
entityAlias = this.validateAndUpdateEntityAlias(aliasId, entityAlias, datasourcesByAliasId, targetDevicesByAliasId);
if (aliasId !== entityAlias.id) {
delete entityAliases[aliasId];
}
entityAliases[entityAlias.id] = entityAlias;
}
}
return configuration;
}
private validateAndUpdateDeviceAlias(aliasId: string,
deviceAlias: any,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): EntityAlias {
aliasId = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
const alias = deviceAlias.alias;
const entityAlias: EntityAlias = {
id: aliasId,
alias,
filter: {
type: null,
entityType: EntityType.DEVICE,
resolveMultiple: false
},
};
if (deviceAlias.deviceFilter) {
entityAlias.filter.type =
deviceAlias.deviceFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList;
if (entityAlias.filter.type === AliasFilterType.entityList) {
entityAlias.filter.entityList = deviceAlias.deviceFilter.deviceList;
} else {
entityAlias.filter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
}
} else {
entityAlias.filter.type = AliasFilterType.entityList;
entityAlias.filter.entityList = [deviceAlias.deviceId];
}
return entityAlias;
}
private validateAndUpdateEntityAlias(aliasId: string, entityAlias: EntityAlias,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): EntityAlias {
entityAlias.id = this.validateAliasId(aliasId, datasourcesByAliasId, targetDevicesByAliasId);
if (!entityAlias.filter) {
entityAlias.filter = {
type: entityAlias.entityFilter.useFilter ? AliasFilterType.entityName : AliasFilterType.entityList,
entityType: entityAlias.entityType,
resolveMultiple: false
};
if (entityAlias.filter.type === AliasFilterType.entityList) {
entityAlias.filter.entityList = entityAlias.entityFilter.entityList;
} else {
entityAlias.filter.entityNameFilter = entityAlias.entityFilter.entityNameFilter;
}
delete entityAlias.entityType;
delete entityAlias.entityFilter;
}
return entityAlias;
}
private validateAliasId(aliasId: string,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): string {
if (!aliasId || !isString(aliasId) || aliasId.length !== 36) {
const newAliasId = this.utils.guid();
const aliasDatasources = datasourcesByAliasId[aliasId];
if (aliasDatasources) {
aliasDatasources.forEach(
(datasource) => {
datasource.entityAliasId = newAliasId;
}
);
}
const targetDeviceAliasIdsList = targetDevicesByAliasId[aliasId];
if (targetDeviceAliasIdsList) {
targetDeviceAliasIdsList.forEach(
(targetDeviceAliasIds) => {
targetDeviceAliasIds[0] = newAliasId;
}
);
}
return newAliasId;
} else {
return aliasId;
}
}
}

7
ui-ngx/src/app/core/services/utils.service.ts

@ -21,11 +21,12 @@ import { isUndefined, isDefined } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo, Widget } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models'; import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -34,7 +35,7 @@ export class UtilsService {
iframeMode = false; iframeMode = false;
widgetEditMode = false; widgetEditMode = false;
editWidgetInfo: any = null; editWidgetInfo: WidgetInfo = null;
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private translate: TranslateService) { private translate: TranslateService) {
@ -87,7 +88,7 @@ export class UtilsService {
type: 'widgetException', type: 'widgetException',
data data
}; };
this.window.parent.postMessage(message, '*'); this.window.parent.postMessage(JSON.stringify(message), '*');
} }
return data; return data;
} }

4
ui-ngx/src/app/core/utils.ts

@ -95,6 +95,10 @@ export function isNumber(value: any): boolean {
return typeof value === 'number'; return typeof value === 'number';
} }
export function isString(value: any): boolean {
return typeof value === 'string';
}
export function objToBase64(obj: any): string { export function objToBase64(obj: any): string {
const json = JSON.stringify(obj); const json = JSON.stringify(obj);
const encoded = utf8Encode(json); const encoded = utf8Encode(json);

36
ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts

@ -0,0 +1,36 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module';
import { DashboardModule } from '@home/pages/dashboard/dashboard.module';
import { DashboardPagesRoutingModule } from './dashboard-pages.routing.module';
@NgModule({
entryComponents: [],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
DashboardModule,
DashboardPagesRoutingModule
]
})
export class DashboardPagesModule { }

104
ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts

@ -0,0 +1,104 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
import {Authority} from '@shared/models/authority.enum';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { widgetTypesBreadcumbLabelFunction } from '@home/pages/widget/widget-library-routing.module';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service';
import { Observable } from 'rxjs';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { map } from 'rxjs/operators';
import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@app/modules/home/pages/dashboard/dashboard-routing.module';
import { UtilsService } from '@core/services/utils.service';
import { Widget } from '@app/shared/models/widget.models';
@Injectable()
export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
constructor(private dashboardService: DashboardService,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) {
}
resolve(route: ActivatedRouteSnapshot): Dashboard {
const editWidgetInfo = this.utils.editWidgetInfo;
const widget: Widget = {
isSystemType: true,
bundleAlias: 'customWidgetBundle',
typeAlias: 'customWidget',
type: editWidgetInfo.type,
title: 'My widget',
sizeX: editWidgetInfo.sizeX * 2,
sizeY: editWidgetInfo.sizeY * 2,
row: 2,
col: 4,
config: JSON.parse(editWidgetInfo.defaultConfig)
};
widget.config.title = widget.config.title || editWidgetInfo.widgetName;
return this.dashboardUtils.createSingleWidgetDashboard(widget);
}
}
const routes: Routes = [
{
path: 'dashboard/:dashboardId',
component: DashboardPageComponent,
data: {
breadcrumb: {
skip: true
},
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'dashboard.dashboard',
widgetEditMode: false,
singlePageMode: true
},
resolve: {
dashboard: DashboardResolver
}
},
{
path: 'widget-editor',
component: DashboardPageComponent,
data: {
breadcrumb: {
skip: true
},
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
widgetEditMode: true,
singlePageMode: true
},
resolve: {
dashboard: WidgetEditorDashboardResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
WidgetEditorDashboardResolver
]
})
export class DashboardPagesRoutingModule { }

43
ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts

@ -0,0 +1,43 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from '@core/guards/auth.guard';
import { StoreModule } from '@ngrx/store';
const routes: Routes = [
{ path: '',
data: {
title: 'dashboard.dashboard',
breadcrumb: {
skip: true
}
},
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: './dashboard-pages.module#DashboardPagesModule'
}
];
@NgModule({
imports: [
StoreModule,
RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule { }

30
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" class="mat-content mat-padding">
<div fxLayout="row" *ngFor="let alias of entityAliasesInfo | keyvalue">
<mat-form-field>
<mat-label>{{alias.value.alias}}</mat-label>
<mat-select matInput [(ngModel)]="alias.value.selectedId"
(ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.selectedId)">
<mat-option *ngFor="let resolvedEntity of alias.value.resolvedEntities" [value]="resolvedEntity.id">
{{resolvedEntity.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

35
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
min-width: 300px;
max-height: 150px;
overflow-x: hidden;
overflow-y: auto;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
@media (min-height: 350px) {
max-height: 250px;
}
.mat-content {
background-color: #fff;
}
}

60
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts

@ -0,0 +1,60 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken, OnInit } from '@angular/core';
import { Timewindow } from '@shared/models/time/time.models';
import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
import { PageComponent } from '@shared/components/page.component';
import { TIMEWINDOW_PANEL_DATA, TimewindowPanelData } from '@shared/components/time/timewindow-panel.component';
import { deepClone } from '@core/utils';
export const ALIASES_ENTITY_SELECT_PANEL_DATA = new InjectionToken<any>('AliasesEntitySelectPanelData');
export interface AliasesEntitySelectPanelData {
aliasController: IAliasController;
}
@Component({
selector: 'tb-aliases-entity-select-panel',
templateUrl: './aliases-entity-select-panel.component.html',
styleUrls: ['./aliases-entity-select-panel.component.scss']
})
export class AliasesEntitySelectPanelComponent {
entityAliasesInfo: {[aliasId: string]: AliasInfo} = {};
constructor(@Inject(ALIASES_ENTITY_SELECT_PANEL_DATA) public data: AliasesEntitySelectPanelData) {
const allEntityAliases = this.data.aliasController.getEntityAliases();
for (const aliasId of Object.keys(allEntityAliases)) {
const aliasInfo = this.data.aliasController.getInstantAliasInfo(aliasId);
if (aliasInfo && !aliasInfo.resolveMultiple && aliasInfo.currentEntity
&& aliasInfo.resolvedEntities.length > 1) {
this.entityAliasesInfo[aliasId] = deepClone(aliasInfo);
this.entityAliasesInfo[aliasId].selectedId = aliasInfo.currentEntity.id;
}
}
}
public currentAliasEntityChanged(aliasId: string, selectedId: string) {
const resolvedEntities = this.entityAliasesInfo[aliasId].resolvedEntities;
const selected = resolvedEntities.find((entity) => entity.id === selectedId);
if (selected) {
this.data.aliasController.updateCurrentAliasEntity(aliasId, selected[0]);
}
}
}

32
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="tb-aliases-entity-select" fxLayout="row" fxLayoutAlign="start center">
<button mat-button mat-icon-button
cdkOverlayOrigin #aliasEntitySelectPanelOrigin="cdkOverlayOrigin"
(click)="openEditMode()"
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">devices_other</mat-icon>
</button>
<span fxHide.xs fxHide.sm fxHide.md
(click)="openEditMode()"
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{displayValue}}
</span>
</section>

38
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
min-width: 52px;
section.tb-aliases-entity-select {
min-height: 32px;
padding: 0 6px;
@media #{$mat-lt-md} {
padding: 0;
}
span {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: pointer;
}
}
}

178
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts

@ -0,0 +1,178 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { TooltipPosition } from '@angular/material';
import { IAliasController } from '@core/api/widget-api.models';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout';
import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import {
ALIASES_ENTITY_SELECT_PANEL_DATA,
AliasesEntitySelectPanelComponent,
AliasesEntitySelectPanelData
} from './aliases-entity-select-panel.component';
@Component({
selector: 'tb-aliases-entity-select',
templateUrl: './aliases-entity-select.component.html',
styleUrls: ['./aliases-entity-select.component.scss']
})
export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
@Input()
aliasController: IAliasController;
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input() disabled: boolean;
@ViewChild('aliasEntitySelectPanelOrigin', {static: false}) aliasEntitySelectPanelOrigin: CdkOverlayOrigin;
displayValue: string;
private rxSubscriptions = new Array<Subscription>();
constructor(private translate: TranslateService,
private overlay: Overlay,
private breakpointObserver: BreakpointObserver,
private viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
}
ngOnInit(): void {
this.rxSubscriptions.push(this.aliasController.entityAliasesChanged.subscribe(
() => {
this.updateDisplayValue();
}
));
this.rxSubscriptions.push(this.aliasController.entityAliasResolved.subscribe(
() => {
this.updateDisplayValue();
}
));
}
ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
openEditMode() {
if (this.disabled) {
return;
}
const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
const panelWidth = 300;
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-aliases-entity-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
});
const el = this.aliasEntitySelectPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.aliasEntitySelectPanelOrigin.elementRef)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createAliasesEntitySelectPanelInjector(
overlayRef,
{
aliasController: this.aliasController
}
);
overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector));
}
private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[ALIASES_ENTITY_SELECT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
}
private updateDisplayValue() {
let displayValue;
let singleValue = true;
let currentAliasId;
const entityAliases = this.aliasController.getEntityAliases();
for (const aliasId of Object.keys(entityAliases)) {
const entityAlias = entityAliases[aliasId];
if (!entityAlias.filter.resolveMultiple) {
const resolvedAlias = this.aliasController.getInstantAliasInfo(aliasId);
if (resolvedAlias && resolvedAlias.currentEntity) {
if (!currentAliasId) {
currentAliasId = aliasId;
} else {
singleValue = false;
break;
}
}
}
}
if (singleValue && currentAliasId) {
const aliasInfo = this.aliasController.getInstantAliasInfo(currentAliasId);
displayValue = aliasInfo.currentEntity.name;
} else {
displayValue = this.translate.instant('entity.entities');
}
this.displayValue = displayValue;
}
}

8
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -38,6 +38,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
import { WidgetComponent } from '@home/components/widget/widget.component'; import { WidgetComponent } from '@home/components/widget/widget.component';
import { WidgetComponentService } from './widget/widget-component.service'; import { WidgetComponentService } from './widget/widget-component.service';
import { LegendComponent } from '@home/components/widget/legend.component'; import { LegendComponent } from '@home/components/widget/legend.component';
import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
@ -48,7 +50,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent EditAttributeValuePanelComponent,
AliasesEntitySelectPanelComponent
], ],
declarations: declarations:
[ [
@ -69,6 +72,8 @@ import { LegendComponent } from '@home/components/widget/legend.component';
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
AliasesEntitySelectPanelComponent,
AliasesEntitySelectComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent LegendComponent
@ -89,6 +94,7 @@ import { LegendComponent } from '@home/components/widget/legend.component';
AlarmTableComponent, AlarmTableComponent,
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent LegendComponent

5
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -61,10 +61,9 @@ import {
WidgetTypeInstance WidgetTypeInstance
} from '@home/models/widget-component.models'; } from '@home/models/widget-component.models';
import { import {
EntityInfo,
IWidgetSubscription, IWidgetSubscription,
StateObject, StateObject,
StateParams, StateParams, SubscriptionEntityInfo,
SubscriptionInfo, SubscriptionInfo,
WidgetSubscriptionContext, WidgetSubscriptionContext,
WidgetSubscriptionOptions WidgetSubscriptionOptions
@ -1065,7 +1064,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
} }
private getActiveEntityInfo(): EntityInfo { private getActiveEntityInfo(): SubscriptionEntityInfo {
let entityInfo = this.widgetContext.activeEntityInfo; let entityInfo = this.widgetContext.activeEntityInfo;
if (!entityInfo) { if (!entityInfo) {
for (const id of Object.keys(this.widgetContext.subscriptions)) { for (const id of Object.keys(this.widgetContext.subscriptions)) {

4
ui-ngx/src/app/modules/home/home.component.html

@ -38,7 +38,7 @@
<button mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()"> <button mat-button mat-icon-button id="main" fxHide.gt-sm (click)="sidenav.toggle()">
<mat-icon class="material-icons">menu</mat-icon> <mat-icon class="material-icons">menu</mat-icon>
</button> </button>
<div fxFlex tb-breadcrumb class="mat-toolbar-tools"> <div fxFlex tb-breadcrumb [activeComponent]="activeComponent" class="mat-toolbar-tools">
</div> </div>
<button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()"> <button *ngIf="fullscreenEnabled" mat-button mat-icon-button fxHide.xs fxHide.sm (click)="toggleFullscreen()">
<mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> <mat-icon class="material-icons">{{ isFullscreen() ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
@ -49,7 +49,7 @@
*ngIf="isLoading$ | async"> *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div fxFlex fxLayout="column" tb-toast class="tb-main-content"> <div fxFlex fxLayout="column" tb-toast class="tb-main-content">
<router-outlet></router-outlet> <router-outlet (activate)="activeComponent = $event;"></router-outlet>
</div> </div>
</div> </div>
</mat-sidenav-content> </mat-sidenav-content>

2
ui-ngx/src/app/modules/home/home.component.ts

@ -40,6 +40,8 @@ import { MatSidenav } from '@angular/material';
}) })
export class HomeComponent extends PageComponent implements OnInit { export class HomeComponent extends PageComponent implements OnInit {
activeComponent: any;
sidenavMode = 'side'; sidenavMode = 'side';
sidenavOpened = true; sidenavOpened = true;

5
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -31,12 +31,11 @@ import {
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import { import {
EntityInfo,
IAliasController, IAliasController,
IStateController, IStateController,
IWidgetSubscription, IWidgetSubscription,
IWidgetUtils, IWidgetUtils,
RpcApi, RpcApi, SubscriptionEntityInfo,
TimewindowFunctions, TimewindowFunctions,
WidgetActionsApi, WidgetActionsApi,
WidgetSubscriptionApi WidgetSubscriptionApi
@ -85,7 +84,7 @@ export interface WidgetContext {
actionsApi?: WidgetActionsApi; actionsApi?: WidgetActionsApi;
stateController?: IStateController; stateController?: IStateController;
aliasController?: IAliasController; aliasController?: IAliasController;
activeEntityInfo?: EntityInfo; activeEntityInfo?: SubscriptionEntityInfo;
widgetTitleTemplate?: string; widgetTitleTemplate?: string;
widgetTitle?: string; widgetTitle?: string;
customHeaderActions?: Array<WidgetHeaderAction>; customHeaderActions?: Array<WidgetHeaderAction>;

157
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -0,0 +1,157 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<section class="tb-dashboard-toolbar"
[ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-closed': !toolbarOpened }">
<tb-dashboard-toolbar [fxShow]="!widgetEditMode" [forceFullscreen]="forceFullscreen"
[toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
<div class="tb-dashboard-action-panels" fxLayout="column-reverse" fxLayout.gt-sm="row-reverse"
fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center">
<div class="tb-dashboard-action-panel" fxFlex.md="30" fxLayout="row-reverse"
fxLayoutAlign.gt-sm="start center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
<button [fxShow]="showCloseToolbar()" mat-button mat-icon-button class="close-action"
matTooltip="{{ 'dashboard.close-toolbar' | translate }}"
matTooltipPosition="below"
(click)="closeToolbar()">
<mat-icon>arrow_forward</mat-icon>
</button>
<tb-user-menu *ngIf="isPublicUser() && forceFullscreen" fxHide.xs fxHide.sm displayUserInfo="true">
</tb-user-menu>
<button [fxShow]="showRightLayoutSwitch()" mat-button mat-icon-button
matTooltip="{{ (isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}"
matTooltipPosition="below"
(click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button>
<button [fxShow]="!hideFullscreenButton()" mat-button mat-icon-button
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<button [fxShow]="isEdit || displayExport()" mat-button mat-icon-button
matTooltip="{{'dashboard.export' | translate}}"
matTooltipPosition="below"
(click)="exportDashboard($event)">
<mat-icon>file_download</mat-icon>
</button>
<tb-timewindow [fxShow]="isEdit || displayDashboardTimewindow()"
isToolbar="true"
direction="left"
tooltipPosition="below"
aggregation="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
<tb-aliases-entity-select [fxShow]="!isEdit && displayEntitiesSelect()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-aliases-entity-select>
<button [fxShow]="isEdit" mat-button mat-icon-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"
(click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon>
</button>
<button [fxShow]="isEdit" mat-button mat-icon-button
matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below"
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
</button>
<tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()"
[(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId"
[dashboardsScope]="currentDashboardScope">
</tb-dashboard-select>
</div>
<div class="tb-dashboard-action-panel" fxFlex.md="70" fxLayout="row-reverse"
fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu>
<!-- TODO -->
</div>
</div>
</tb-dashboard-toolbar>
</section>
<section class="tb-dashboard-container tb-absolute-fill"
[ngClass]="{ 'is-fullscreen': forceFullscreen,
'tb-dashboard-toolbar-opened': toolbarOpened,
'tb-dashboard-toolbar-closed': !toolbarOpened }">
<section *ngIf="!widgetEditMode" class="tb-dashboard-title" fxLayout="row" fxLayoutAlign="center center"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
<h3 [fxShow]="!isEdit && displayTitle()">{{ dashboard.title }}</h3>
<mat-form-field [fxShow]="isEdit" class="mat-block" style="height: 30px;">
<mat-label translate [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">dashboard.title</mat-label>
<input matInput class="tb-dashboard-title"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}"
required name="title" [(ngModel)]="dashboard.title">
</mat-form-field>
</section>
<div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}"
[ngClass]="{ 'tb-padded' : !widgetEditMode && (isEdit || displayTitle()), 'tb-shrinked' : isEditingWidget }">
<div [fxShow]="layouts.main.show"
id="tb-main-layout"
[ngStyle]="{width: mainLayoutWidth(),
height: mainLayoutHeight()}">
TODO: MAIN LAYOUT tb-dashboard-layout
</div>
<mat-sidenav-container *ngIf="layouts.right.show"
id="tb-right-layout">
<mat-sidenav
[ngStyle]="{minWidth: rightLayoutWidth(),
maxWidth: rightLayoutWidth(),
height: rightLayoutHeight(),
zIndex: 25}"
disableClose="true"
position="end"
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">
TODO: RIGHT LAYOUT tb-dashboard-layout
</mat-sidenav>
</mat-sidenav-container>
</div>
<!--tb-details-sidenav TODO -->
<!--tb-details-sidenav TODO -->
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<!--md-fab-speed-dial TODO -->
<button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': !isEdit || isAddingWidget}"
[disabled]="isLoading$ | async"
(click)="saveDashboard()"
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>done</mat-icon>
</button>
<button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': isAddingWidget || (isLoading$ | async)}"
[disabled]="isLoading$ | async"
(click)="toggleDashboardEditMode()"
matTooltip="{{ (isEdit ? 'action.decline-changes': 'action.enter-edit-mode') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ isEdit ? 'close' : 'edit' }}</mat-icon>
</button>
</section>
</section>
<section class="tb-powered-by-footer" [ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
<span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ thingsboardVersion }}</a></span>
</section>
</div>

137
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss

@ -0,0 +1,137 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
$toolbar-height: 50px !default;
$fullscreen-toolbar-height: 64px !default;
$mobile-toolbar-height: 84px !default;
tb-dashboard-page {
display: flex;
width: 100%;
height: 100%;
}
div.tb-dashboard-page {
&.mat-content {
background-color: #eee;
}
section.tb-dashboard-title {
position: absolute;
top: 0;
left: 20px;
mat-form-field {
.mat-form-field-infix {
width: 100%;
}
}
input.tb-dashboard-title {
height: 38px;
font-size: 2rem;
font-weight: 500;
letter-spacing: .005em;
}
}
div.tb-padded {
top: 60px;
}
section.tb-padded {
top: 60px;
}
div.tb-shrinked {
width: 40%;
}
section.tb-dashboard-toolbar {
position: absolute;
top: 0;
left: 0;
z-index: 13;
pointer-events: none;
&.tb-dashboard-toolbar-opened {
right: 0;
// transition: right .3s cubic-bezier(.55, 0, .55, .2);
}
&.tb-dashboard-toolbar-closed {
right: 18px;
transition: right .3s cubic-bezier(.55, 0, .55, .2) .2s;
}
}
.tb-dashboard-container {
&.tb-dashboard-toolbar-opened {
&.is-fullscreen {
margin-top: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
margin-top: $fullscreen-toolbar-height;
}
}
&:not(.is-fullscreen) {
margin-top: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
margin-top: $toolbar-height;
}
transition: margin-top .3s cubic-bezier(.55, 0, .55, .2);
}
}
&.tb-dashboard-toolbar-closed {
margin-top: 0;
transition: margin-top .3s cubic-bezier(.55, 0, .55, .2) .2s;
}
.tb-dashboard-layouts {
/*md-backdrop {
z-index: 1;
}*/
#tb-right-layout {
mat-sidenav {
z-index: 1;
}
}
}
}
section.tb-powered-by-footer {
position: absolute;
right: 25px;
bottom: 5px;
z-index: 30;
pointer-events: none;
span {
font-size: 12px;
a {
font-weight: 700;
text-decoration: none;
pointer-events: all;
border: none;
}
}
}
}

438
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -0,0 +1,438 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import { Dashboard, DashboardConfiguration, WidgetLayout } from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, isDefined } from '@app/core/utils';
import {
DashboardContext,
DashboardPageLayoutContext,
DashboardPageLayouts,
DashboardPageScope
} from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Widget } from '@app/shared/models/widget.models';
import { environment as env } from '@env/environment';
import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-dashboard-page',
templateUrl: './dashboard-page.component.html',
styleUrls: ['./dashboard-page.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DashboardPageComponent extends PageComponent implements OnDestroy {
authUser: AuthUser = getCurrentAuthUser(this.store);
dashboard: Dashboard;
dashboardConfiguration: DashboardConfiguration;
prevDashboard: Dashboard;
iframeMode = this.utils.iframeMode;
widgetEditMode: boolean;
singlePageMode: boolean;
forceFullscreen = this.authService.forceFullscreen;
isFullscreen = false;
isEdit = false;
isEditingWidget = false;
isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
forceDashboardMobileMode = false;
isAddingWidget = false;
isToolbarOpened = false;
isRightLayoutOpened = false;
editingWidget: Widget = null;
editingWidgetLayout: WidgetLayout = null;
editingWidgetOriginal: Widget = null;
editingWidgetLayoutOriginal: WidgetLayout = null;
editingWidgetSubtitle: string = null;
editingLayoutCtx: DashboardPageLayoutContext = null;
thingsboardVersion: string = env.tbVersion;
currentDashboardId: string;
currentCustomerId: string;
currentDashboardScope: DashboardPageScope;
layouts: DashboardPageLayouts = {
main: {
show: false,
layoutCtx: {
id: 'main',
widgets: [],
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false
}
},
right: {
show: false,
layoutCtx: {
id: 'right',
widgets: [],
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false
}
}
};
dashboardCtx: DashboardContext = {
dashboard: null,
dashboardTimewindow: null,
state: null,
stateController: {
openRightLayout: this.openRightLayout.bind(this)
},
aliasController: null
};
private rxSubscriptions = new Array<Subscription>();
get toolbarOpened(): boolean {
return !this.widgetEditMode &&
(this.toolbarAlwaysOpen() || this.isToolbarOpened || this.isEdit || this.showRightLayoutSwitch());
}
set toolbarOpened(toolbarOpened: boolean) {
}
get rightLayoutOpened(): boolean {
return !this.isMobile || this.isRightLayoutOpened;
}
set rightLayoutOpened(rightLayoutOpened: boolean) {
}
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
private breakpointObserver: BreakpointObserver,
private route: ActivatedRoute,
private router: Router,
private utils: UtilsService,
private authService: AuthService,
private entityService: EntityService,
private dialogService: DialogService) {
super(store);
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
this.init(data);
}
));
this.rxSubscriptions.push(this.breakpointObserver
.observe(MediaBreakpoints['gt-sm'])
.subscribe((state: BreakpointState) => {
this.isMobile = !state.matches;
}
));
}
private init(data: any) {
this.reset();
this.currentDashboardId = this.route.snapshot.params.dashboardId;
if (this.route.snapshot.params.customerId) {
this.currentCustomerId = this.route.snapshot.params.customerId;
this.currentDashboardScope = 'customer';
} else {
this.currentDashboardScope = this.authUser.authority === Authority.TENANT_ADMIN ? 'tenant' : 'customer';
this.currentCustomerId = this.authUser.customerId;
}
this.dashboard = data.dashboard;
this.dashboardConfiguration = this.dashboard.configuration;
this.widgetEditMode = data.widgetEditMode;
this.singlePageMode = data.singlePageMode;
this.dashboardCtx.dashboard = this.dashboard;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.dashboardCtx.aliasController = new AliasController(this.utils,
this.entityService,
this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases);
if (this.widgetEditMode) {
const message: WindowMessage = {
type: 'widgetEditModeInited'
};
this.window.parent.postMessage(JSON.stringify(message), '*');
}
}
private reset() {
this.dashboard = null;
this.dashboardConfiguration = null;
this.prevDashboard = null;
this.widgetEditMode = false;
this.singlePageMode = false;
this.isFullscreen = false;
this.isEdit = false;
this.isEditingWidget = false;
this.forceDashboardMobileMode = false;
this.isAddingWidget = false;
this.isToolbarOpened = false;
this.isRightLayoutOpened = false;
this.editingWidget = null;
this.editingWidgetLayout = null;
this.editingWidgetOriginal = null;
this.editingWidgetLayoutOriginal = null;
this.editingWidgetSubtitle = null;
this.editingLayoutCtx = null;
this.currentDashboardId = null;
this.currentCustomerId = null;
this.currentDashboardScope = null;
}
ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
public openToolbar() {
this.isToolbarOpened = true;
}
public closeToolbar() {
this.isToolbarOpened = false;
}
public showCloseToolbar() {
return !this.toolbarAlwaysOpen() && !this.isEdit && !this.showRightLayoutSwitch();
}
public hideFullscreenButton(): boolean {
return this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode;
}
public toolbarAlwaysOpen(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.toolbarAlwaysOpen)) {
return this.dashboard.configuration.settings.toolbarAlwaysOpen;
} else {
return true;
}
}
public displayTitle(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showTitle)) {
return this.dashboard.configuration.settings.showTitle;
} else {
return false;
}
}
public displayExport(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showDashboardExport)) {
return this.dashboard.configuration.settings.showDashboardExport;
} else {
return true;
}
}
public displayDashboardTimewindow(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showDashboardTimewindow)) {
return this.dashboard.configuration.settings.showDashboardTimewindow;
} else {
return true;
}
}
public displayDashboardsSelect(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showDashboardsSelect)) {
return this.dashboard.configuration.settings.showDashboardsSelect;
} else {
return true;
}
}
public displayEntitiesSelect(): boolean {
if (this.dashboard.configuration.settings &&
isDefined(this.dashboard.configuration.settings.showEntitiesSelect)) {
return this.dashboard.configuration.settings.showEntitiesSelect;
} else {
return true;
}
}
public showRightLayoutSwitch(): boolean {
return this.isMobile && this.layouts.right.show;
}
public toggleLayouts() {
this.isRightLayoutOpened = !this.isRightLayoutOpened;
}
public openRightLayout() {
this.isRightLayoutOpened = true;
}
public mainLayoutWidth(): string {
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
return '100%';
} else {
return this.layouts.right.show && !this.isMobile ? '50%' : '100%';
}
}
public mainLayoutHeight(): string {
if (!this.isEditingWidget || this.editingLayoutCtx.id === 'main') {
return '100%';
} else {
return '0px';
}
}
public rightLayoutWidth(): string {
if (this.isEditingWidget && this.editingLayoutCtx.id === 'right') {
return '100%';
} else {
return this.isMobile ? '100%' : '50%';
}
}
public rightLayoutHeight(): string {
if (!this.isEditingWidget || this.editingLayoutCtx.id === 'right') {
return '100%';
} else {
return '0px';
}
}
public isPublicUser(): boolean {
return this.authUser.isPublic;
}
public isTenantAdmin(): boolean {
return this.authUser.authority === Authority.TENANT_ADMIN;
}
public isSystemAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN;
}
public exportDashboard($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
public openEntityAliases($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
public openDashboardSettings($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
public currentDashboardIdChanged(dashboardId: string) {
if (!this.widgetEditMode) {
if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) {
this.router.navigateByUrl(`customers/${this.currentCustomerId}/dashboards/${dashboardId}`);
} else {
if (this.singlePageMode) {
this.router.navigateByUrl(`dashboard/${dashboardId}`);
} else {
this.router.navigateByUrl(`dashboards/${dashboardId}`);
}
}
}
}
public toggleDashboardEditMode() {
this.setEditMode(!this.isEdit, true);
}
private setEditMode(isEdit: boolean, revert: boolean) {
this.isEdit = isEdit;
if (this.isEdit) {
// TODO:
// this.dashboardCtx.stateController.preserveState();
this.prevDashboard = deepClone(this.dashboard);
} else {
if (this.widgetEditMode) {
if (revert) {
this.dashboard = this.prevDashboard;
}
} else {
this.resetHighlight();
if (revert) {
this.dashboard = this.prevDashboard;
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.entityAliasesUpdated();
} else {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
}
}
}
}
private resetHighlight() {
for (const l of Object.keys(this.layouts)) {
if (this.layouts[l].layoutCtx) {
if (this.layouts[l].layoutCtx.ctrl) {
this.layouts[l].layoutCtx.ctrl.resetHighlight();
}
}
}
}
private entityAliasesUpdated() {
this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
}
}

48
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard } from '@app/shared/models/dashboard.models';
import { Widget } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
export declare type DashboardPageScope = 'tenant' | 'customer';
export interface DashboardContext {
state: string;
dashboard: Dashboard;
dashboardTimewindow: Timewindow;
aliasController: IAliasController;
stateController: IStateController;
}
export interface DashboardPageLayoutContext {
id: DashboardLayoutId;
widgets: Array<Widget>;
widgetLayouts: {[id: string]: WidgetLayout};
gridSettings: GridSettings;
ignoreLoading: boolean;
}
export interface DashboardPageLayout {
show: boolean;
layoutCtx: DashboardPageLayoutContext;
}
export interface DashboardPageLayouts {
main: DashboardPageLayout;
right: DashboardPageLayout;
}

50
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts

@ -14,12 +14,39 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {NgModule} from '@angular/core'; import { Injectable, NgModule } from '@angular/core';
import {RouterModule, Routes} from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; import {EntitiesTableComponent} from '../../components/entity/entities-table.component';
import {Authority} from '@shared/models/authority.enum'; import {Authority} from '@shared/models/authority.enum';
import {DashboardsTableConfigResolver} from './dashboards-table-config.resolver'; import {DashboardsTableConfigResolver} from './dashboards-table-config.resolver';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { widgetTypesBreadcumbLabelFunction } from '@home/pages/widget/widget-library-routing.module';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service';
import { Observable } from 'rxjs';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { map } from 'rxjs/operators';
@Injectable()
export class DashboardResolver implements Resolve<Dashboard> {
constructor(private dashboardService: DashboardService,
private dashboardUtils: DashboardUtilsService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<Dashboard> {
const dashboardId = route.params.dashboardId;
return this.dashboardService.getDashboard(dashboardId).pipe(
map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard))
);
}
}
export const dashboardBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate, component) => component.dashboard.title);
const routes: Routes = [ const routes: Routes = [
{ {
@ -42,6 +69,22 @@ const routes: Routes = [
resolve: { resolve: {
entitiesTableConfig: DashboardsTableConfigResolver entitiesTableConfig: DashboardsTableConfigResolver
} }
},
{
path: ':dashboardId',
component: DashboardPageComponent,
data: {
breadcrumb: {
labelFunction: dashboardBreadcumbLabelFunction,
icon: 'dashboard'
} as BreadCrumbConfig,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'dashboard.dashboard',
widgetEditMode: false
},
resolve: {
dashboard: DashboardResolver
}
} }
] ]
} }
@ -51,7 +94,8 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
providers: [ providers: [
DashboardsTableConfigResolver DashboardsTableConfigResolver,
DashboardResolver
] ]
}) })
export class DashboardRoutingModule { } export class DashboardRoutingModule { }

35
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-fab-toolbar [isOpen]="toolbarOpened"
color="primary"
direction="left"
[ngClass]="{'is-fullscreen': forceFullscreen, 'mat-elevation-z1': forceFullscreen && toolbarOpened }">
<mat-fab-trigger>
<button mat-button mat-icon-button mat-fab color="primary"
[matTooltip]="!toolbarOpened ? ('dashboard.open-toolbar' | translate) : ''"
matTooltipPosition="below"
(click)="onTriggerClick()">
<mat-icon>more_horiz</mat-icon>
</button>
</mat-fab-trigger>
<mat-toolbar color="primary">
<mat-fab-actions class="mat-toolbar-tools">
<ng-content></ng-content>
</mat-fab-actions>
</mat-toolbar>
</mat-fab-toolbar>

185
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss

@ -0,0 +1,185 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
$toolbar-height: 50px !default;
$fullscreen-toolbar-height: 64px !default;
$mobile-toolbar-height: 80px !default;
$half-mobile-toolbar-height: 40px !default;
$mobile-toolbar-height-total: 84px !default;
tb-dashboard-toolbar {
mat-fab-toolbar {
mat-fab-trigger {
.mat-button {
&.mat-fab {
width: 36px;
height: 36px;
margin: 4px 0 0 4px;
line-height: 36px;
.mat-button-wrapper {
width: 36px;
height: 36px;
padding: 0 6px;
box-sizing: border-box;
}
mat-icon {
display: block;
position: absolute;
top: 25%;
width: 18px;
min-width: 18px;
height: 18px;
min-height: 18px;
margin: 0;
line-height: 18px;
}
}
}
}
&.is-fullscreen {
.mat-fab-toolbar-wrapper {
height: $mobile-toolbar-height-total;
@media #{$mat-gt-sm} {
height: $fullscreen-toolbar-height;
}
mat-toolbar {
height: $mobile-toolbar-height;
min-height: $mobile-toolbar-height;
.mat-toolbar-tools {
height: $mobile-toolbar-height;
min-height: $mobile-toolbar-height;
}
@media #{$mat-gt-sm} {
height: $fullscreen-toolbar-height;
min-height: $fullscreen-toolbar-height;
.mat-toolbar-tools {
height: $fullscreen-toolbar-height;
min-height: $fullscreen-toolbar-height;
}
}
}
}
}
.mat-fab-toolbar-wrapper {
height: $mobile-toolbar-height-total;
@media #{$mat-gt-sm} {
height: $toolbar-height;
}
mat-toolbar {
height: $mobile-toolbar-height;
min-height: $mobile-toolbar-height;
.mat-toolbar-tools {
height: $mobile-toolbar-height;
min-height: $mobile-toolbar-height;
}
@media #{$mat-gt-sm} {
height: $toolbar-height;
min-height: $toolbar-height;
.mat-toolbar-tools {
height: $toolbar-height;
min-height: $toolbar-height;
}
}
mat-fab-actions {
margin-top: 0;
font-size: 16px;
@media #{$mat-lt-md} {
height: $mobile-toolbar-height;
max-height: $mobile-toolbar-height;
}
.close-action {
margin-right: -18px;
}
.mat-fab-action-item {
width: 100%;
height: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
}
.tb-dashboard-action-panels {
height: $mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
}
.tb-dashboard-action-panel {
min-width: 0;
height: $half-mobile-toolbar-height;
@media #{$mat-lt-md} {
mat-menu{
margin: 0;
}
}
@media #{$mat-gt-sm} {
height: 46px;
}
> div {
height: $half-mobile-toolbar-height;
@media #{$mat-gt-sm} {
height: 46px;
}
}
mat-select {
margin: 0;
pointer-events: all;
}
tb-states-component {
pointer-events: all;
}
button.mat-icon-button:not(.tb-mat-32) {
min-width: 40px;
@media #{$mat-lt-md} {
min-width: 28px;
padding: 2px;
margin: 0;
.mat-button-wrapper {
display: block;
line-height: 24px;
}
}
}
}
}
}
}
}
}
}
}

47
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts

@ -0,0 +1,47 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnDestroy, OnInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@Component({
selector: 'tb-dashboard-toolbar',
templateUrl: './dashboard-toolbar.component.html',
styleUrls: ['./dashboard-toolbar.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DashboardToolbarComponent implements OnInit {
@Input()
toolbarOpened: boolean;
@Input()
forceFullscreen: boolean;
@Output()
triggerClick = new EventEmitter<void>();
constructor() {
}
ngOnInit(): void {
}
onTriggerClick() {
this.triggerClick.emit();
}
}

24
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -14,16 +14,18 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {NgModule} from '@angular/core'; import { NgModule } from '@angular/core';
import {CommonModule} from '@angular/common'; import { CommonModule } from '@angular/common';
import {SharedModule} from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import {HomeDialogsModule} from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import {DashboardFormComponent} from '@modules/home/pages/dashboard/dashboard-form.component'; import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component';
import {ManageDashboardCustomersDialogComponent} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component';
import {DashboardRoutingModule} from './dashboard-routing.module'; import { DashboardRoutingModule } from './dashboard-routing.module';
import {MakeDashboardPublicDialogComponent} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component';
import {HomeComponentsModule} from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
import { DashboardToolbarComponent } from './dashboard-toolbar.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
@ -36,7 +38,9 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com
DashboardFormComponent, DashboardFormComponent,
DashboardTabsComponent, DashboardTabsComponent,
ManageDashboardCustomersDialogComponent, ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent MakeDashboardPublicDialogComponent,
DashboardToolbarComponent,
DashboardPageComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

8
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -303,9 +303,11 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
// TODO: if (this.config.componentsData.dashboardScope === 'customer') {
// this.router.navigateByUrl(`customers/${customer.id.id}/users`); this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`);
this.dialogService.todo(); } else {
this.router.navigateByUrl(`dashboards/${dashboard.id.id}`);
}
} }
importDashboard($event: Event) { importDashboard($event: Event) {

59
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html

@ -0,0 +1,59 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form style="width: 800px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>widget.select-widget-type</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="(isLoading$ | async)">
<div fxLayout="column" fxLayoutGap="16px" fxLayout.gt-sm="row" fxLayoutAlign="center center">
<button *ngFor="let type of allWidgetTypes;" class="tb-card-button" mat-button mat-raised-button color="primary"
type="button"
(click)="typeSelected(type)">
<mat-icon *ngIf="!widgetTypesDataMap.get(type).isMdiIcon; else mdiIconBlock" class="tb-mat-96">
{{ widgetTypesDataMap.get(type).icon }}
</mat-icon>
<ng-template #mdiIconBlock>
<mat-icon class="tb-mat-96" [svgIcon]="widgetTypesDataMap.get(type).icon">
</mat-icon>
</ng-template>
<span translate>{{ widgetTypesDataMap.get(type).name }}</span>
</button>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

50
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
button.tb-card-button {
width: 100%;
height: 100%;
max-width: 240px;
.mat-button-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
margin: auto;
}
span {
height: 18px;
min-height: 18px;
max-height: 18px;
padding: 0 0 20px 0;
margin: auto;
font-size: 18px;
font-weight: 400;
line-height: 18px;
white-space: normal;
}
}
&.mat-raised-button.mat-primary {
.mat-ripple-element {
opacity: 0.3;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}

52
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts

@ -0,0 +1,52 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { widgetType, widgetTypesData } from '@shared/models/widget.models';
@Component({
selector: 'tb-select-widget-type-dialog',
templateUrl: './select-widget-type-dialog.component.html',
styleUrls: ['./select-widget-type-dialog.component.scss']
})
export class SelectWidgetTypeDialogComponent extends
DialogComponent<SelectWidgetTypeDialogComponent, widgetType> {
widgetTypes = widgetType;
allWidgetTypes = Object.keys(widgetType);
widgetTypesDataMap = widgetTypesData;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<SelectWidgetTypeDialogComponent, widgetType>) {
super(store, router, dialogRef);
}
cancel(): void {
this.dialogRef.close(null);
}
typeSelected(type: widgetType) {
this.dialogRef.close(type);
}
}

255
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -0,0 +1,255 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div fxFlex fxLayout="column">
<div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
<mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
<mat-form-field floatLabel="always" class="tb-widget-title">
<mat-label></mat-label>
<input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
placeholder="{{ 'widget.title' | translate }}"/>
</mat-form-field>
<mat-form-field>
<mat-select [disabled]="isReadOnly" matInput placeholder="{{ 'widget.type' | translate }}"
[(ngModel)]="widget.type" (ngModelChange)="widetTypeChanged()">
<mat-option *ngFor="let type of allWidgetTypes" [value]="type">
{{ widgetTypesDataMap.get(type).name | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<span fxFlex></span>
<button mat-button fxHide.xs fxHide.sm [disabled]="!iframeWidgetEditModeInited"
(click)="applyWidgetScript()"
matTooltip="{{ 'widget.run' | translate }} (CTRL + Return)"
matTooltipPosition="below">
<mat-icon>play_arrow</mat-icon>
<span translate>action.run</span>
</button>
<button mat-button mat-raised-button
fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || undoDisabled()"
(click)="undoWidget()"
matTooltip="{{ 'widget.undo' | translate }} (CTRL + Q)"
matTooltipPosition="below">
<mat-icon>undo</mat-icon>
<span translate>action.undo</span>
</button>
<button *ngIf="!isReadOnly" mat-button mat-raised-button
fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || saveDisabled()"
(click)="saveWidget()"
[tb-circular-progress]="saveWidgetPending"
matTooltip="{{ 'widget.save' | translate }} (CTRL + S)"
matTooltipPosition="below">
<mat-icon>save</mat-icon>
<span translate>action.save</span>
</button>
<button mat-button mat-raised-button
fxHide.xs fxHide.sm [disabled]="(isLoading$ | async) || saveAsDisabled()"
(click)="saveWidgetAs()"
[tb-circular-progress]="saveWidgetAsPending"
matTooltip="{{ 'widget.saveAs' | translate }} (Shift + CTRL + S)"
matTooltipPosition="below">
<mat-icon>save</mat-icon>
<span translate>action.saveAs</span>
</button>
<button mat-button
fxHide.xs fxHide.sm
(click)="fullscreen = !fullscreen"
matTooltip="{{ 'widget.toggle-fullscreen' | translate }} (Shift + CTRL + F)"
matTooltipPosition="below">
<mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
<span translate>widget.toggle-fullscreen</span>
</button>
<button mat-button mat-icon-button fxHide.gt-sm
[matMenuTriggerFor]="widgetEditMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #widgetEditMenu="matMenu" xPosition="before">
<button mat-menu-item
[disabled]="!iframeWidgetEditModeInited"
(click)="applyWidgetScript()">
<mat-icon>play_arrow</mat-icon>
<span translate>action.run</span>
</button>
<button mat-menu-item
[disabled]="(isLoading$ | async) || undoDisabled()"
(click)="undoWidget()">
<mat-icon>undo</mat-icon>
<span translate>action.undo</span>
</button>
<button *ngIf="!isReadOnly" mat-menu-item
[disabled]="(isLoading$ | async) || saveDisabled()"
(click)="saveWidget()">
<mat-icon>save</mat-icon>
<span translate>action.save</span>
</button>
<button mat-menu-item
[disabled]="(isLoading$ | async) || saveAsDisabled()"
(click)="saveWidgetAs()">
<mat-icon>save</mat-icon>
<span translate>action.saveAs</span>
</button>
</mat-menu>
</mat-toolbar>
<div fxFlex style="position: relative;">
<div class="tb-editor tb-absolute-fill">
<div #topPanel class="tb-split tb-split-vertical">
<div #topLeftPanel class="tb-split tb-content">
<mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;">
<div class="tb-resize-container" style="background-color: #fff;">
<div class="mat-padding">
<div fxFlex fxLayout="row" style="max-height: 40px;"
fxLayoutAlign="start center"
*ngFor="let resource of widget.resources; let i = index" >
<mat-form-field fxFlex class="mat-block resource-field" floatLabel="never"
style="margin: 10px 0px 0px 0px; max-height: 40px;">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button color="primary"
[disabled]="isLoading$ | async"
(click)="removeResource(i)"
matTooltip="{{'widget.remove-resource' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 6px;">
<button mat-button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyHtml()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-button mat-icon-button class="tb-mat-32"
(click)="htmlFullscreen = !htmlFullscreen"
matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #htmlInput></div>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyCss()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-button mat-icon-button class="tb-mat-32"
(click)="cssFullscreen = !cssFullscreen"
matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #cssInput></div>
</div>
</mat-tab>
</mat-tab-group>
</div>
<div #topRightPanel class="tb-split tb-content">
<mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.settings-schema' | translate }}" style="width: 100%; height: 100%;">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-button mat-icon-button class="tb-mat-32"
(click)="jsonSettingsFullscreen = !jsonSettingsFullscreen"
matTooltip="{{(jsonSettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonSettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #settingsJsonInput></div>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}" style="width: 100%; height: 100%;">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyDataKeyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-button mat-icon-button class="tb-mat-32"
(click)="jsonDataKeySettingsFullscreen = !jsonDataKeySettingsFullscreen"
matTooltip="{{(jsonDataKeySettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonDataKeySettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #dataKeySettingsJsonInput></div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>
<div #bottomPanel class="tb-split tb-split-vertical">
<div #javascriptPanel class="tb-split tb-content" tb-toast toastTarget="javascriptPanel">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="javascriptFullscreen">
<div class="tb-editor-area-title-panel">
<label translate>widget.javascript</label>
<button mat-button (click)="beautifyJs()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-button mat-icon-button class="tb-mat-32"
(click)="javascriptFullscreen = !javascriptFullscreen"
matTooltip="{{(javascriptFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ javascriptFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #javascriptInput></div>
</div>
</div>
<div #framePanel class="tb-split tb-content" style="overflow-y: hidden; position: relative;">
<div class="mat-content tb-progress-cover" fxFlex fxLayout="column" fxLayoutAlign="center center"
*ngIf="!iframeWidgetEditModeInited">
<mat-spinner diameter="100" mode="indeterminate" color="warn"></mat-spinner>
</div>
<div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;">
<iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe>
<button mat-button mat-icon-button
class="tb-fullscreen-button-style"
style="position: absolute; top: 10px; left: 10px; bottom: initial;"
(click)="iFrameFullscreen = !iFrameFullscreen"
matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

186
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -0,0 +1,186 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$edit-toolbar-height: 40px !default;
tb-widget-editor {
flex: 1;
display: flex;
flex-direction: column;
}
.tb-editor {
.tb-split {
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
mat-form-field.resource-field {
max-height: 40px;
margin: 10px 0px 0px 0px;
.mat-form-field-wrapper {
padding-bottom: 0;
.mat-form-field-flex {
max-height: 40px;
.mat-form-field-infix {
border: 0;
}
}
.mat-form-field-underline {
bottom: 0;
}
}
}
.ace_editor {
font-size: 14px !important;
}
.tb-content {
border: 1px solid #c0c0c0;
}
.gutter {
background-color: transparent;
background-repeat: no-repeat;
background-position: 50%;
&.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../assets/split.js/grips/vertical.png");
}
&.gutter-vertical {
cursor: row-resize;
background-image: url("../../../../../assets/split.js/grips/horizontal.png");
}
}
.tb-split.tb-split-horizontal,
.gutter.gutter-horizontal {
float: left;
height: 100%;
}
.tb-split.tb-split-vertical {
display: flex;
.tb-split.tb-content {
height: 100%;
}
}
}
.tb-split-vertical {
mat-tab-group {
.mat-tab-body-wrapper {
height: calc(100% - 49px);
mat-tab-body {
height: 100%;
& > div {
height: 100%;
}
}
}
}
}
div.tb-editor-area-title-panel {
position: absolute;
top: 5px;
right: 20px;
z-index: 5;
font-size: .8rem;
font-weight: 500;
label {
padding: 4px;
color: #00acc1;
text-transform: uppercase;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
}
}
.tb-resize-container {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
.ace_editor {
height: 100%;
}
}
mat-toolbar.tb-edit-toolbar {
min-height: $edit-toolbar-height !important;
max-height: $edit-toolbar-height !important;
button.mat-button {
min-width: 65px;
min-height: 30px;
font-size: 12px;
line-height: 30px;
mat-icon {
font-size: 20px;
}
span {
padding-right: 6px;
}
}
mat-form-field {
input {
font-size: 1.2rem;
font-weight: 400;
letter-spacing: .005em;
}
div.mat-form-field-infix {
padding-bottom: 5px;
}
&.tb-widget-title {
min-width: 250px;
}
}
}

626
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -0,0 +1,626 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { PageComponent } from '@shared/components/page.component';
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { WidgetInfo } from '@home/models/widget-component.models';
import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models';
import { ActivatedRoute } from '@angular/router';
import { deepClone } from '@core/utils';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
import { TranslateService } from '@ngx-translate/core';
import { getCurrentIsLoading } from '@app/core/interceptors/load.selectors';
import * as ace from 'ace-builds';
import { css_beautify, html_beautify } from 'js-beautify';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { ExceptionData } from '@shared/models/error.models';
import Timeout = NodeJS.Timeout;
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
@Component({
selector: 'tb-widget-editor',
templateUrl: './widget-editor.component.html',
styleUrls: ['./widget-editor.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WidgetEditorComponent extends PageComponent implements OnInit, OnDestroy, HasDirtyFlag {
@ViewChild('topPanel', {static: true})
topPanelElmRef: ElementRef;
@ViewChild('topLeftPanel', {static: true})
topLeftPanelElmRef: ElementRef;
@ViewChild('topRightPanel', {static: true})
topRightPanelElmRef: ElementRef;
@ViewChild('bottomPanel', {static: true})
bottomPanelElmRef: ElementRef;
@ViewChild('javascriptPanel', {static: true})
javascriptPanelElmRef: ElementRef;
@ViewChild('framePanel', {static: true})
framePanelElmRef: ElementRef;
@ViewChild('htmlInput', {static: true})
htmlInputElmRef: ElementRef;
@ViewChild('cssInput', {static: true})
cssInputElmRef: ElementRef;
@ViewChild('settingsJsonInput', {static: true})
settingsJsonInputElmRef: ElementRef;
@ViewChild('dataKeySettingsJsonInput', {static: true})
dataKeySettingsJsonInputElmRef: ElementRef;
@ViewChild('javascriptInput', {static: true})
javascriptInputElmRef: ElementRef;
@ViewChild('widgetIFrame', {static: true})
widgetIFrameElmRef: ElementRef<HTMLIFrameElement>;
iframe: JQuery<HTMLIFrameElement>;
widgetTypes = widgetType;
allWidgetTypes = Object.keys(widgetType);
widgetTypesDataMap = widgetTypesData;
authUser: AuthUser;
isReadOnly: boolean;
widgetsBundle: WidgetsBundle;
widgetType: WidgetType;
widget: WidgetInfo;
origWidget: WidgetInfo;
isDirty = false;
fullscreen = false;
htmlFullscreen = false;
cssFullscreen = false;
jsonSettingsFullscreen = false;
jsonDataKeySettingsFullscreen = false;
javascriptFullscreen = false;
iFrameFullscreen = false;
aceEditors: ace.Ace.Editor[] = [];
editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {};
htmlEditor: ace.Ace.Editor;
cssEditor: ace.Ace.Editor;
jsonSettingsEditor: ace.Ace.Editor;
dataKeyJsonSettingsEditor: ace.Ace.Editor;
jsEditor: ace.Ace.Editor;
aceResizeListeners: { element: any, resizeListener: any }[] = [];
onWindowMessageListener = this.onWindowMessage.bind(this);
iframeWidgetEditModeInited = false;
saveWidgetPending = false;
saveWidgetAsPending = false;
gotError = false;
errorMarkers: number[] = [];
errorAnnotationId = -1;
saveWidgetTimeout: Timeout;
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
private route: ActivatedRoute,
private widgetService: WidgetService,
private hotkeysService: HotkeysService,
private translate: TranslateService,
private raf: RafService) {
super(store);
this.authUser = getCurrentAuthUser(store);
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType;
this.widget = this.route.snapshot.data.widgetEditorData.widget;
if (this.widgetType) {
const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config);
}
this.origWidget = deepClone(this.widget);
if (!this.widgetType) {
this.isDirty = true;
}
}
ngOnInit(): void {
this.initHotKeys();
this.initSplitLayout();
this.initAceEditors();
this.iframe = $(this.widgetIFrameElmRef.nativeElement);
this.window.addEventListener('message', this.onWindowMessageListener);
this.iframe.attr('data-widget', JSON.stringify(this.widget));
this.iframe.attr('src', '/widget-editor');
}
ngOnDestroy(): void {
this.window.removeEventListener('message', this.onWindowMessageListener);
this.aceResizeListeners.forEach((resizeListener) => {
// @ts-ignore
removeResizeListener(resizeListener.element, resizeListener.resizeListener);
});
}
private initHotKeys(): void {
this.hotkeysService.add(
new Hotkey('ctrl+q', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) {
event.preventDefault();
this.undoWidget();
}
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.undo'))
);
this.hotkeysService.add(
new Hotkey('ctrl+s', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) {
event.preventDefault();
this.saveWidget();
}
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.save'))
);
this.hotkeysService.add(
new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) {
event.preventDefault();
this.saveWidgetAs();
}
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.saveAs'))
);
this.hotkeysService.add(
new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
event.preventDefault();
this.fullscreen = !this.fullscreen;
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.toggle-fullscreen'))
);
this.hotkeysService.add(
new Hotkey('ctrl+enter', (event: KeyboardEvent) => {
event.preventDefault();
this.applyWidgetScript();
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.run'))
);
}
private initSplitLayout() {
Split([this.topPanelElmRef.nativeElement, this.bottomPanelElmRef.nativeElement], {
sizes: [35, 65],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
});
Split([this.topLeftPanelElmRef.nativeElement, this.topRightPanelElmRef.nativeElement], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
});
Split([this.javascriptPanelElmRef.nativeElement, this.framePanelElmRef.nativeElement], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
});
}
private initAceEditors() {
this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html');
this.htmlEditor.on('input', () => {
const editorValue = this.htmlEditor.getValue();
if (this.widget.templateHtml !== editorValue) {
this.widget.templateHtml = editorValue;
this.isDirty = true;
}
});
this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css');
this.cssEditor.on('input', () => {
const editorValue = this.cssEditor.getValue();
if (this.widget.templateCss !== editorValue) {
this.widget.templateCss = editorValue;
this.isDirty = true;
}
});
this.jsonSettingsEditor = this.createAceEditor(this.settingsJsonInputElmRef, 'json');
this.jsonSettingsEditor.on('input', () => {
const editorValue = this.jsonSettingsEditor.getValue();
if (this.widget.settingsSchema !== editorValue) {
this.widget.settingsSchema = editorValue;
this.isDirty = true;
}
});
this.dataKeyJsonSettingsEditor = this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json');
this.dataKeyJsonSettingsEditor.on('input', () => {
const editorValue = this.dataKeyJsonSettingsEditor.getValue();
if (this.widget.dataKeySettingsSchema !== editorValue) {
this.widget.dataKeySettingsSchema = editorValue;
this.isDirty = true;
}
});
this.jsEditor = this.createAceEditor(this.javascriptInputElmRef, 'javascript');
this.jsEditor.on('input', () => {
const editorValue = this.jsEditor.getValue();
if (this.widget.controllerScript !== editorValue) {
this.widget.controllerScript = editorValue;
this.isDirty = true;
}
});
this.jsEditor.on('change', () => {
this.cleanupJsErrors();
});
this.setAceEditorValues();
}
private setAceEditorValues() {
this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1);
this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1);
}
private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor {
const editorElement = editorElementRef.nativeElement;
let editorOptions: Partial<ace.Ace.EditorOptions> = {
mode: `ace/mode/${mode}`,
showGutter: true,
showPrintMargin: true
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
const aceEditor = ace.edit(editorElement, editorOptions);
aceEditor.session.setUseWrapMode(true);
this.aceEditors.push(aceEditor);
const resizeListener = this.onAceEditorResize.bind(this, aceEditor);
// @ts-ignore
addResizeListener(editorElement, resizeListener);
this.aceResizeListeners.push({element: editorElement, resizeListener});
return aceEditor;
}
private onAceEditorResize(aceEditor: ace.Ace.Editor) {
if (this.editorsResizeCafs[aceEditor.id]) {
this.editorsResizeCafs[aceEditor.id]();
delete this.editorsResizeCafs[aceEditor.id];
}
this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => {
aceEditor.resize();
aceEditor.renderer.updateFull();
});
}
private onWindowMessage(event: MessageEvent) {
let message: WindowMessage;
if (event.data) {
try {
message = JSON.parse(event.data);
} catch (e) {}
}
if (message) {
switch (message.type) {
case 'widgetException':
this.onWidgetException(message.data);
break;
case 'widgetEditModeInited':
this.onWidgetEditModeInited();
break;
case 'widgetEditUpdated':
this.onWidgetEditUpdated(message.data);
break;
}
}
}
private onWidgetEditModeInited() {
this.iframeWidgetEditModeInited = true;
if (this.saveWidgetPending || this.saveWidgetAsPending) {
if (!this.saveWidgetTimeout) {
this.saveWidgetTimeout = setTimeout(() => {
if (!this.gotError) {
if (this.saveWidgetPending) {
this.commitSaveWidget();
} else if (this.saveWidgetAsPending) {
this.commitSaveWidgetAs();
}
} else {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('widget.unable-to-save-widget-error'), type: 'error'}));
this.saveWidgetPending = false;
this.saveWidgetAsPending = false;
}
this.saveWidgetTimeout = undefined;
}, 1500);
}
}
}
private onWidgetEditUpdated(widget: Widget) {
this.widget.sizeX = widget.sizeX / 2;
this.widget.sizeY = widget.sizeY / 2;
this.widget.defaultConfig = JSON.stringify(widget.config);
this.iframe.attr('data-widget', JSON.stringify(this.widget));
this.isDirty = true;
}
private onWidgetException(details: ExceptionData) {
if (!this.gotError) {
this.gotError = true;
let errorInfo = 'Error:';
if (details.name) {
errorInfo += ' ' + details.name + ':';
}
if (details.message) {
errorInfo += ' ' + details.message;
}
if (details.lineNumber) {
errorInfo += '<br>Line ' + details.lineNumber;
if (details.columnNumber) {
errorInfo += ' column ' + details.columnNumber;
}
errorInfo += ' of script.';
}
if (!this.saveWidgetPending && !this.saveWidgetAsPending) {
this.store.dispatch(new ActionNotificationShow(
{message: errorInfo, type: 'error', target: 'javascriptPanel'}));
}
if (details.lineNumber) {
const line = details.lineNumber - 1;
let column = 0;
if (details.columnNumber) {
column = details.columnNumber;
}
const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity),
'ace_active-line', 'screenLine');
this.errorMarkers.push(errorMarkerId);
const annotations = this.jsEditor.session.getAnnotations();
const errorAnnotation: ace.Ace.Annotation = {
row: line,
column,
text: details.message,
type: 'error'
};
this.errorAnnotationId = annotations.push(errorAnnotation) - 1;
this.jsEditor.session.setAnnotations(annotations);
}
}
}
private cleanupJsErrors() {
this.store.dispatch(new ActionNotificationHide({}));
this.errorMarkers.forEach((errorMarker) => {
this.jsEditor.session.removeMarker(errorMarker);
});
this.errorMarkers.length = 0;
if (this.errorAnnotationId > -1) {
const annotations = this.jsEditor.session.getAnnotations();
annotations.splice(this.errorAnnotationId, 1);
this.jsEditor.session.setAnnotations(annotations);
this.errorAnnotationId = -1;
}
}
private commitSaveWidget() {
// TODO:
this.saveWidgetPending = false;
}
private commitSaveWidgetAs() {
// TODO:
this.saveWidgetAsPending = false;
}
applyWidgetScript(): void {
this.cleanupJsErrors();
this.gotError = false;
this.iframeWidgetEditModeInited = false;
const config: WidgetConfig = JSON.parse(this.widget.defaultConfig);
config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config);
this.iframe.attr('data-widget', JSON.stringify(this.widget));
this.iframe[0].contentWindow.location.reload(true);
}
undoWidget(): void {
this.widget = deepClone(this.origWidget);
this.setAceEditorValues();
this.isDirty = false;
this.applyWidgetScript();
}
saveWidget(): void {
if (!this.widget.widgetName) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('widget.missing-widget-title-error'), type: 'error'}));
} else {
this.saveWidgetPending = true;
this.applyWidgetScript();
}
}
saveWidgetAs(): void {
this.saveWidgetAsPending = true;
this.applyWidgetScript();
}
undoDisabled(): boolean {
return !this.isDirty
|| !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending;
}
saveDisabled(): boolean {
return this.isReadOnly
|| !this.isDirty
|| !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending;
}
saveAsDisabled(): boolean {
return !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending;
}
beautifyCss(): void {
const res = css_beautify(this.widget.templateCss, {indent_size: 4});
if (this.widget.templateCss !== res) {
this.isDirty = true;
this.widget.templateCss = res;
this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
}
}
beautifyHtml(): void {
const res = html_beautify(this.widget.templateHtml, {indent_size: 4, wrap_line_length: 60});
if (this.widget.templateHtml !== res) {
this.isDirty = true;
this.widget.templateHtml = res;
this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1);
}
}
beautifyJson(): void {
const res = js_beautify(this.widget.settingsSchema, {indent_size: 4});
if (this.widget.settingsSchema !== res) {
this.isDirty = true;
this.widget.settingsSchema = res;
this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
}
}
beautifyDataKeyJson(): void {
const res = js_beautify(this.widget.dataKeySettingsSchema, {indent_size: 4});
if (this.widget.dataKeySettingsSchema !== res) {
this.isDirty = true;
this.widget.dataKeySettingsSchema = res;
this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
}
}
beautifyJs(): void {
const res = js_beautify(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60});
if (this.widget.controllerScript !== res) {
this.isDirty = true;
this.widget.controllerScript = res;
this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1);
}
}
removeResource(index: number) {
if (index > -1) {
if (this.widget.resources.splice(index, 1).length > 0) {
this.isDirty = true;
}
}
}
addResource() {
this.widget.resources.push({url: ''});
this.isDirty = true;
}
widetTypeChanged() {
const config: WidgetConfig = JSON.parse(this.widget.defaultConfig);
if (this.widget.type !== widgetType.rpc &&
this.widget.type !== widgetType.alarm) {
if (config.targetDeviceAliases) {
delete config.targetDeviceAliases;
}
if (config.alarmSource) {
delete config.alarmSource;
}
if (!config.datasources) {
config.datasources = [];
}
if (!config.timewindow) {
config.timewindow = {
realtime: {
timewindowMs: 60000
}
};
}
} else if (this.widget.type === widgetType.rpc) {
if (config.datasources) {
delete config.datasources;
}
if (config.alarmSource) {
delete config.alarmSource;
}
if (config.timewindow) {
delete config.timewindow;
}
if (!config.targetDeviceAliases) {
config.targetDeviceAliases = [];
}
} else { // alarm
if (config.datasources) {
delete config.datasources;
}
if (config.targetDeviceAliases) {
delete config.targetDeviceAliases;
}
if (!config.alarmSource) {
config.alarmSource = {};
}
if (!config.timewindow) {
config.timewindow = {
realtime: {
timewindowMs: 24 * 60 * 60 * 1000
}
};
}
}
this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = true;
}
}

123
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -17,20 +17,24 @@
import { Injectable, NgModule } from '@angular/core'; import { Injectable, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router';
import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import {Authority} from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { User } from '@shared/models/user.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UserService } from '@core/http/user.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators';
import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
export interface WidgetEditorData {
widgetType: WidgetType;
widget: WidgetInfo;
}
@Injectable() @Injectable()
export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
@ -39,13 +43,61 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
} }
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> { resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
const widgetsBundleId = route.params.widgetsBundleId; let widgetsBundleId = route.params.widgetsBundleId;
if (!widgetsBundleId) {
widgetsBundleId = route.parent.params.widgetsBundleId;
}
return this.widgetsService.getWidgetsBundle(widgetsBundleId); return this.widgetsService.getWidgetsBundle(widgetsBundleId);
} }
} }
@Injectable()
export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
constructor(private widgetsService: WidgetService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
const widgetTypeId = route.params.widgetTypeId;
return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe(
map((result) => {
return {
widgetType: result,
widget: toWidgetInfo(result)
};
})
);
}
}
@Injectable()
export class WidgetEditorAddDataResolver implements Resolve<WidgetEditorData> {
constructor(private widgetsService: WidgetService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
let widgetTypeParam = route.params.widgetType as widgetType;
if (!widgetTypeParam) {
widgetTypeParam = widgetType.timeseries;
}
return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe(
map((widget) => {
widget.widgetName = null;
return {
widgetType: null,
widget
};
})
);
}
}
export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title); export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title);
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction =
((route, translate, component) => component ? (component as WidgetEditorComponent).widget.widgetName : '');
export const routes: Routes = [ export const routes: Routes = [
{ {
path: 'widgets-bundles', path: 'widgets-bundles',
@ -69,10 +121,7 @@ export const routes: Routes = [
}, },
{ {
path: ':widgetsBundleId/widgetTypes', path: ':widgetsBundleId/widgetTypes',
component: WidgetLibraryComponent,
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widget-library',
breadcrumb: { breadcrumb: {
labelFunction: widgetTypesBreadcumbLabelFunction, labelFunction: widgetTypesBreadcumbLabelFunction,
icon: 'now_widgets' icon: 'now_widgets'
@ -80,7 +129,49 @@ export const routes: Routes = [
}, },
resolve: { resolve: {
widgetsBundle: WidgetsBundleResolver widgetsBundle: WidgetsBundleResolver
} },
children: [
{
path: '',
component: WidgetLibraryComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widget-library'
}
},
{
path: ':widgetTypeId',
component: WidgetEditorComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
breadcrumb: {
labelFunction: widgetEditorBreadcumbLabelFunction,
icon: 'insert_chart'
} as BreadCrumbConfig
},
resolve: {
widgetEditorData: WidgetEditorDataResolver
}
},
{
path: 'add/:widgetType',
component: WidgetEditorComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
breadcrumb: {
labelFunction: widgetEditorBreadcumbLabelFunction,
icon: 'insert_chart'
} as BreadCrumbConfig
},
resolve: {
widgetEditorData: WidgetEditorAddDataResolver
}
}
]
} }
] ]
} }
@ -91,7 +182,9 @@ export const routes: Routes = [
exports: [RouterModule], exports: [RouterModule],
providers: [ providers: [
WidgetsBundlesTableConfigResolver, WidgetsBundlesTableConfigResolver,
WidgetsBundleResolver WidgetsBundleResolver,
WidgetEditorDataResolver,
WidgetEditorAddDataResolver
] ]
}) })
export class WidgetLibraryRoutingModule { } export class WidgetLibraryRoutingModule { }

43
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -34,6 +34,13 @@ import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-componen
import { IAliasController } from '@app/core/api/widget-api.models'; import { IAliasController } from '@app/core/api/widget-api.models';
import { toWidgetInfo } from '@home/models/widget-component.models'; import { toWidgetInfo } from '@home/models/widget-component.models';
import { DummyAliasController } from '@core/api/alias-controller'; import { DummyAliasController } from '@core/api/alias-controller';
import {
DeviceCredentialsDialogComponent,
DeviceCredentialsDialogData
} from '@home/pages/device/device-credentials-dialog.component';
import { DeviceCredentials } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
@Component({ @Component({
selector: 'tb-widget-library', selector: 'tb-widget-library',
@ -83,8 +90,10 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private widgetService: WidgetService, private widgetService: WidgetService,
private dialogService: DialogService) { private dialogService: DialogService,
private dialog: MatDialog) {
super(store); super(store);
this.authUser = getCurrentAuthUser(store); this.authUser = getCurrentAuthUser(store);
@ -163,33 +172,43 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
} }
importWidgetType($event: Event): void { importWidgetType($event: Event): void {
if (event) { if ($event) {
event.stopPropagation(); $event.stopPropagation();
} }
this.dialogService.todo(); this.dialogService.todo();
} }
openWidgetType($event: Event, widget?: Widget): void { openWidgetType($event: Event, widget?: Widget): void {
if (event) { if ($event) {
event.stopPropagation(); $event.stopPropagation();
} }
if (widget) { if (widget) {
this.dialogService.todo(); this.router.navigate([widget.typeId.id], {relativeTo: this.route});
} else { } else {
this.dialogService.todo(); this.dialog.open<SelectWidgetTypeDialogComponent, any,
widgetType>(SelectWidgetTypeDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(type) => {
if (type) {
this.router.navigate(['add', type], {relativeTo: this.route});
}
}
);
} }
} }
exportWidgetType($event: Event, widget: Widget): void { exportWidgetType($event: Event, widget: Widget): void {
if (event) { if ($event) {
event.stopPropagation(); $event.stopPropagation();
} }
this.dialogService.todo(); this.dialogService.todo();
} }
removeWidgetType($event: Event, widget: Widget): void { removeWidgetType($event: Event, widget: Widget): void {
if (event) { if ($event) {
event.stopPropagation(); $event.stopPropagation();
} }
this.dialogService.todo(); this.dialogService.todo();
} }

9
ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts

@ -21,14 +21,19 @@ import {WidgetsBundleComponent} from '@modules/home/pages/widget/widgets-bundle.
import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module'; import {WidgetLibraryRoutingModule} from '@modules/home/pages/widget/widget-library-routing.module';
import {HomeComponentsModule} from '@modules/home/components/home-components.module'; import {HomeComponentsModule} from '@modules/home/components/home-components.module';
import { WidgetLibraryComponent } from './widget-library.component'; import { WidgetLibraryComponent } from './widget-library.component';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
WidgetsBundleComponent WidgetsBundleComponent,
SelectWidgetTypeDialogComponent
], ],
declarations: [ declarations: [
WidgetsBundleComponent, WidgetsBundleComponent,
WidgetLibraryComponent WidgetLibraryComponent,
WidgetEditorComponent,
SelectWidgetTypeDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

6
ui-ngx/src/app/shared/components/breadcrumb.component.html

@ -17,7 +17,7 @@
--> -->
<div fxFlex class="tb-breadcrumb" fxLayout="row"> <div fxFlex class="tb-breadcrumb" fxLayout="row">
<h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb"> <h1 fxFlex fxHide.gt-sm *ngIf="lastBreadcrumb$ | async; let breadcrumb">
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
</h1> </h1>
<span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast"> <span fxHide.xs fxHide.sm *ngFor="let breadcrumb of breadcrumbs$ | async; last as isLast;" [ngSwitch]="isLast">
<a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams"> <a *ngSwitchCase="false" [routerLink]="breadcrumb.link" [queryParams]="breadcrumb.queryParams">
@ -26,7 +26,7 @@
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
{{ breadcrumb.icon }} {{ breadcrumb.icon }}
</mat-icon> </mat-icon>
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
</a> </a>
<span *ngSwitchCase="true"> <span *ngSwitchCase="true">
<mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon"> <mat-icon *ngIf="breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
@ -34,7 +34,7 @@
<mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons"> <mat-icon *ngIf="!breadcrumb.isMdiIcon" class="material-icons">
{{ breadcrumb.icon }} {{ breadcrumb.icon }}
</mat-icon> </mat-icon>
{{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}
</span> </span>
<span class="divider" [fxHide]="isLast"> > </span> <span class="divider" [fxHide]="isLast"> > </span>
</span> </span>

18
ui-ngx/src/app/shared/components/breadcrumb.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs'; import { BehaviorSubject, Subject } from 'rxjs';
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
@ -28,6 +28,13 @@ import { TranslateService } from '@ngx-translate/core';
}) })
export class BreadcrumbComponent implements OnInit, OnDestroy { export class BreadcrumbComponent implements OnInit, OnDestroy {
activeComponentValue: any;
@Input()
set activeComponent(activeComponent: any) {
this.activeComponentValue = activeComponent;
}
breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot));
routerEventsSubscription = this.router.events.pipe( routerEventsSubscription = this.router.events.pipe(
@ -61,9 +68,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig;
if (breadcrumbConfig && !breadcrumbConfig.skip) { if (breadcrumbConfig && !breadcrumbConfig.skip) {
let label; let label;
let labelFunction;
let ignoreTranslate; let ignoreTranslate;
if (breadcrumbConfig.labelFunction) { if (breadcrumbConfig.labelFunction) {
label = breadcrumbConfig.labelFunction(route, this.translate); labelFunction = () => {
return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue);
};
ignoreTranslate = true; ignoreTranslate = true;
} else { } else {
label = breadcrumbConfig.label || 'home.home'; label = breadcrumbConfig.label || 'home.home';
@ -71,10 +81,11 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
} }
const icon = breadcrumbConfig.icon || 'home'; const icon = breadcrumbConfig.icon || 'home';
const isMdiIcon = icon.startsWith('mdi:'); const isMdiIcon = icon.startsWith('mdi:');
const link = [ '/' + route.url.join('') ]; const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ];
const queryParams = route.queryParams; const queryParams = route.queryParams;
const breadcrumb = { const breadcrumb = {
label, label,
labelFunction,
ignoreTranslate, ignoreTranslate,
icon, icon,
isMdiIcon, isMdiIcon,
@ -89,5 +100,4 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
} }
return newBreadcrumbs; return newBreadcrumbs;
} }
} }

3
ui-ngx/src/app/shared/components/breadcrumb.ts

@ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core';
export interface BreadCrumb { export interface BreadCrumb {
label: string; label: string;
labelFunction?: () => string;
ignoreTranslate: boolean; ignoreTranslate: boolean;
icon: string; icon: string;
isMdiIcon: boolean; isMdiIcon: boolean;
@ -26,7 +27,7 @@ export interface BreadCrumb {
queryParams: Params; queryParams: Params;
} }
export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService) => string; export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService, component: any) => string;
export interface BreadCrumbConfig { export interface BreadCrumbConfig {
labelFunction: BreadCrumbLabelFunction; labelFunction: BreadCrumbLabelFunction;

83
ui-ngx/src/app/shared/components/circular-progress.directive.ts

@ -0,0 +1,83 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Directive, ElementRef, ViewContainerRef, ComponentFactoryResolver, ComponentRef, Input } from '@angular/core';
import { Overlay } from '@angular/cdk/overlay';
import { MatProgressBar, MatSpinner } from '@angular/material';
@Directive({
selector: '[tb-circular-progress]'
})
export class CircularProgressDirective {
showProgressValue = false;
children: JQuery<any>;
cssWidth: any;
@Input('tb-circular-progress')
set showProgress(showProgress: boolean) {
if (this.showProgressValue !== showProgress) {
const element = this.elementRef.nativeElement;
this.showProgressValue = showProgress;
this.spinnerRef.instance._elementRef.nativeElement.style.display = showProgress ? 'block' : 'none';
if (showProgress) {
this.cssWidth = $(element).prop('style').width;
if (!this.cssWidth) {
$(element).css('width', '');
const width = $(element).prop('offsetWidth');
$(element).css('width', width + 'px');
}
this.children = $(element).children();
$(element).empty();
$(element).append($(this.spinnerRef.instance._elementRef.nativeElement));
} else {
$(element).empty();
$(element).append(this.children);
if (this.cssWidth) {
$(element).css('width', this.cssWidth);
} else {
$(element).css('width', '');
}
}
}
}
spinnerRef: ComponentRef<MatSpinner>;
constructor(private elementRef: ElementRef,
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef) {
this.createCircularProgress();
}
createCircularProgress() {
this.elementRef.nativeElement.style.position = 'relative';
const factory = this.componentFactoryResolver.resolveComponentFactory(MatSpinner);
this.spinnerRef = this.viewContainerRef.createComponent(factory, 0);
this.spinnerRef.instance.mode = 'indeterminate';
this.spinnerRef.instance.diameter = 20;
const el = this.spinnerRef.instance._elementRef.nativeElement;
el.style.margin = 'auto';
el.style.position = 'absolute';
el.style.left = '0';
el.style.right = '0';
el.style.top = '0';
el.style.bottom = '0';
el.style.display = 'none';
}
}

28
ui-ngx/src/app/shared/components/dashboard-select-panel.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" class="mat-content mat-padding">
<mat-form-field>
<mat-label>{{'dashboard.select-dashboard' | translate}}</mat-label>
<mat-select matInput [(ngModel)]="dashboardId"
(ngModelChange)="dashboardSelected(dashboardId)">
<mat-option *ngFor="let dashboard of dashboards$ | async" [value]="dashboard.id.id">
{{dashboard.title}}
</mat-option>
</mat-select>
</mat-form-field>
</div>

55
ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../scss/constants";
:host {
min-width: 300px;
max-width: 320px;
max-height: 150px;
overflow-x: hidden;
overflow-y: auto;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
@media (min-height: 350px) {
max-height: 250px;
}
@media #{$mat-gt-xs} {
max-width: 100%;
}
.mat-content {
background-color: #fff;
}
}
:host ::ng-deep {
mat-form-field {
.mat-form-field-infix {
width: 100%;
mat-select {
.mat-select-value {
max-width: 100%;
}
}
}
}
}

48
ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken } from '@angular/core';
import { Observable } from 'rxjs';
import { DashboardInfo } from '../models/dashboard.models';
export const DASHBOARD_SELECT_PANEL_DATA = new InjectionToken<any>('DashboardSelectPanelData');
export interface DashboardSelectPanelData {
dashboards$: Observable<Array<DashboardInfo>>;
dashboardId: string;
onDashboardSelected: (dashboardId: string) => void;
}
@Component({
selector: 'tb-dashboard-select-panel',
templateUrl: './dashboard-select-panel.component.html',
styleUrls: ['./dashboard-select-panel.component.scss']
})
export class DashboardSelectPanelComponent {
dashboards$: Observable<Array<DashboardInfo>>;
dashboardId: string;
constructor(@Inject(DASHBOARD_SELECT_PANEL_DATA)
private data: DashboardSelectPanelData) {
this.dashboards$ = this.data.dashboards$;
this.dashboardId = this.data.dashboardId;
}
public dashboardSelected(dashboardId: string) {
this.data.onDashboardSelected(dashboardId);
}
}

35
ui-ngx/src/app/shared/components/dashboard-select.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-select fxHide.xs fxHide.sm fxHide.md
[required]="required"
[disabled]="disabled"
[(ngModel)]="dashboardId"
(ngModelChange)="dashboardIdChanged()">
<mat-option *ngFor="let dashboard of dashboards$ | async" [value]="dashboard.id.id">
{{dashboard.title}}
</mat-option>
</mat-select>
<section fxHide.gt-md class="tb-dashboard-select">
<button mat-button mat-icon-button
cdkOverlayOrigin #dashboardSelectPanelOrigin="cdkOverlayOrigin"
(click)="openDashboardSelectPanel()"
matTooltip="{{ 'dashboard.select-dashboard' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon>dashboards</mat-icon>
</button>
</section>

40
ui-ngx/src/app/shared/components/dashboard-select.component.scss

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
min-width: 52px;
mat-select {
max-width: 300px;
pointer-events: all;
}
.tb-dashboard-select {
min-height: 32px;
span {
pointer-events: all;
cursor: pointer;
}
}
}
:host ::ng-deep {
mat-select {
.mat-select-value {
max-width: 282px;
}
}
}

214
ui-ngx/src/app/shared/components/dashboard-select.component.ts

@ -0,0 +1,214 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { map, share } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { DashboardInfo } from '@app/shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TooltipPosition } from '@angular/material/tooltip';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { BreakpointObserver } from '@angular/cdk/layout';
import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import {
DASHBOARD_SELECT_PANEL_DATA,
DashboardSelectPanelComponent,
DashboardSelectPanelData
} from './dashboard-select-panel.component';
@Component({
selector: 'tb-dashboard-select',
templateUrl: './dashboard-select.component.html',
styleUrls: ['./dashboard-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DashboardSelectComponent),
multi: true
}]
})
export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
@Input()
dashboardsScope: 'customer' | 'tenant';
@Input()
customerId: string;
@Input()
tooltipPosition: TooltipPosition = 'above';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
dashboards$: Observable<Array<DashboardInfo>>;
dashboardId: string | null;
@ViewChild('dashboardSelectPanelOrigin', {static: false}) dashboardSelectPanelOrigin: CdkOverlayOrigin;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private dashboardService: DashboardService,
private overlay: Overlay,
private breakpointObserver: BreakpointObserver,
private viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
const pageLink = new PageLink(100);
this.dashboards$ = this.getDashboards(pageLink).pipe(
map((pageData) => pageData.data),
share()
);
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
this.dashboardId = value;
}
dashboardIdChanged() {
this.updateView();
}
openDashboardSelectPanel() {
if (this.disabled) {
return;
}
const panelHeight = this.breakpointObserver.isMatched('min-height: 350px') ? 250 : 150;
const panelWidth = 300;
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-dashboard-select-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
});
const el = this.dashboardSelectPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.dashboardSelectPanelOrigin.elementRef)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createDashboardSelectPanelInjector(
overlayRef,
{
dashboards$: this.dashboards$,
dashboardId: this.dashboardId,
onDashboardSelected: (dashboardId) => {
overlayRef.dispose();
this.dashboardId = dashboardId;
this.updateView();
}
}
);
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[DASHBOARD_SELECT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
}
private updateView() {
this.propagateChange(this.dashboardId);
}
private getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> {
let dashboardsObservable: Observable<PageData<DashboardInfo>>;
const authUser = getCurrentAuthUser(this.store);
if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
if (this.customerId) {
dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
} else {
dashboardsObservable = of(emptyPageData());
}
} else {
dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true);
}
return dashboardsObservable;
}
}

22
ui-ngx/src/app/shared/components/fab-toolbar.component.html

@ -0,0 +1,22 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="mat-fab-toolbar-wrapper">
<div class="mat-fab-toolbar-content">
<ng-content></ng-content>
</div>
</div>

187
ui-ngx/src/app/shared/components/fab-toolbar.component.scss

@ -0,0 +1,187 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$font-size: 10px !default;
@function rem($multiplier) {
@return $multiplier * $font-size;
}
$button-fab-width: rem(5.600) !default;
$button-fab-height: rem(5.600) !default;
$button-fab-padding: rem(1.60) !default;
$icon-button-margin: rem(0.600) !default;
$z-index-fab: 20 !default;
$swift-ease-in-duration: 0.3s !default;
$swift-ease-in-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2) !default;
$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default;
@mixin rtl-prop($ltr-prop, $rtl-prop, $value, $reset-value) {
#{$ltr-prop}: $value;
[dir=rtl] & {
#{$ltr-prop}: $reset-value;
#{$rtl-prop}: $value;
}
}
@mixin fab-position($spot, $top: auto, $right: auto, $bottom: auto, $left: auto) {
&.mat-fab-#{$spot} {
top: $top;
right: $right;
bottom: $bottom;
left: $left;
position: absolute;
}
}
@mixin fab-all-positions() {
@include fab-position(bottom-right, auto, ($button-fab-width - $button-fab-padding)/2, ($button-fab-height - $button-fab-padding)/2, auto);
@include fab-position(bottom-left, auto, auto, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2);
@include fab-position(top-right, ($button-fab-height - $button-fab-padding)/2, ($button-fab-width - $button-fab-padding)/2, auto, auto);
@include fab-position(top-left, ($button-fab-height - $button-fab-padding)/2, auto, auto, ($button-fab-width - $button-fab-padding)/2);
}
mat-fab-toolbar {
$icon-delay: 200ms;
@include fab-all-positions();
display: block;
.mat-fab-toolbar-wrapper {
display: block;
position: relative;
overflow: hidden;
height: $button-fab-width + ($icon-button-margin * 2);
}
mat-fab-trigger {
position: absolute;
z-index: $z-index-fab;
button {
overflow: visible !important;
opacity: .5;
}
.mat-fab-toolbar-background {
display: block;
position: absolute;
z-index: $z-index-fab + 1;
opacity: 1;
}
mat-icon {
position: relative;
z-index: $z-index-fab + 2;
opacity: 1;
}
}
&.mat-left {
mat-fab-trigger {
@include rtl-prop(right, left, 0, auto);
}
.mat-toolbar-tools {
flex-direction: row-reverse;
> .mat-button:first-child {
@include rtl-prop(margin-right, margin-left, 0.6rem, auto)
}
> .mat-button:first-child {
@include rtl-prop(margin-left, margin-right, -0.8rem, auto);
}
> .mat-button:last-child {
@include rtl-prop(margin-right, margin-left, 8px, auto);
}
}
}
&.mat-right {
mat-fab-trigger {
@include rtl-prop(left, right, 0, auto);
}
.mat-toolbar-tools {
flex-direction: row;
}
}
mat-toolbar {
padding: 0 !important;
background-color: transparent !important;
pointer-events: none;
position: relative;
z-index: $z-index-fab + 3;
.mat-toolbar-tools {
padding: 0 20px !important;
margin-top: 3px;
}
.mat-fab-action-item {
opacity: 0;
transform: scale(0);
}
}
&.mat-is-open {
mat-fab-trigger > button {
box-shadow: none;
opacity: 1;
mat-icon {
opacity: 0;
}
}
.mat-fab-action-item {
opacity: 1;
transform: scale(1);
}
}
&.mat-animation {
mat-fab-trigger {
button {
transition: opacity .3s cubic-bezier(.55, 0, .55, .2) .2s;
}
.mat-fab-toolbar-background {
transition: $swift-ease-in;
}
mat-icon {
transition: all $icon-delay ease-in;
}
}
mat-toolbar {
.mat-fab-action-item {
transition: $swift-ease-in;
transition-duration: $swift-ease-in-duration / 2;
}
}
&.mat-is-open {
mat-fab-trigger > button {
transition: opacity .3s cubic-bezier(.55, 0, .55, .2);
}
}
}
}

188
ui-ngx/src/app/shared/components/fab-toolbar.component.ts

@ -0,0 +1,188 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
Input,
OnChanges,
OnInit,
Renderer2,
ViewEncapsulation,
SimpleChanges,
Inject, AfterViewInit, AfterViewChecked, Directive, OnDestroy
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WINDOW } from '@core/services/window.service';
import { mixinColor, CanColorCtor } from '@angular/material';
export declare type FabToolbarDirection = 'left' | 'right';
class MatFabToolbarBase {
// tslint:disable-next-line:variable-name
constructor(public _elementRef: ElementRef) {}
}
const MatFabToolbarMixinBase: CanColorCtor & typeof MatFabToolbarBase = mixinColor(MatFabToolbarBase);
@Directive({
selector: 'mat-fab-trigger'
})
export class FabTriggerDirective {
constructor(private el: ElementRef<HTMLElement>) {
}
}
@Directive({
selector: 'mat-fab-actions'
})
export class FabActionsDirective implements OnInit {
constructor(private el: ElementRef<HTMLElement>) {
}
ngOnInit(): void {
const element = $(this.el.nativeElement);
const children = element.children();
children.wrap('<div class="mat-fab-action-item">');
}
}
@Component({
selector: 'mat-fab-toolbar',
templateUrl: './fab-toolbar.component.html',
styleUrls: ['./fab-toolbar.component.scss'],
inputs: ['color'],
encapsulation: ViewEncapsulation.None
})
export class FabToolbarComponent extends MatFabToolbarMixinBase implements OnInit, OnDestroy, AfterViewInit, OnChanges {
@Input()
isOpen: boolean;
@Input()
direction: FabToolbarDirection;
fabToolbarResizeListener = this.onFabToolbarResize.bind(this);
constructor(private el: ElementRef<HTMLElement>,
@Inject(WINDOW) private window: Window) {
super(el);
}
ngOnInit(): void {
const element = $(this.el.nativeElement);
element.addClass('mat-fab-toolbar');
element.find('mat-fab-trigger').find('button')
.prepend('<div class="mat-fab-toolbar-background"></div>');
element.addClass(`mat-${this.direction}`);
// @ts-ignore
addResizeListener(this.el.nativeElement, this.fabToolbarResizeListener);
}
ngOnDestroy(): void {
// @ts-ignore
removeResizeListener(this.el.nativeElement, this.fabToolbarResizeListener);
}
ngAfterViewInit(): void {
this.triggerOpenClose(true);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'isOpen') {
this.triggerOpenClose();
}
}
}
}
private onFabToolbarResize() {
if (this.isOpen) {
this.triggerOpenClose(true);
}
}
private triggerOpenClose(disableAnimation?: boolean): void {
const el = this.el.nativeElement;
const element = $(this.el.nativeElement);
if (disableAnimation) {
element.removeClass('mat-animation');
} else {
element.addClass('mat-animation');
}
const backgroundElement: HTMLElement = el.querySelector('.mat-fab-toolbar-background');
const triggerElement: HTMLElement = el.querySelector('mat-fab-trigger button');
const toolbarElement: HTMLElement = el.querySelector('mat-toolbar');
const iconElement: HTMLElement = el.querySelector('mat-fab-trigger button mat-icon');
const actions = element.find('mat-fab-actions').children();
if (triggerElement && backgroundElement) {
const width = el.offsetWidth;
const height = el.offsetHeight;
const scale = 2 * (width / triggerElement.offsetWidth);
backgroundElement.style.borderRadius = width + 'px';
if (this.isOpen) {
element.addClass('mat-is-open');
toolbarElement.style.pointerEvents = 'inherit';
backgroundElement.style.width = triggerElement.offsetWidth + 'px';
backgroundElement.style.height = triggerElement.offsetHeight + 'px';
backgroundElement.style.transform = 'scale(' + scale + ')';
backgroundElement.style.transitionDelay = '0ms';
if (iconElement) {
iconElement.style.transitionDelay = disableAnimation ? '0ms' : '.3s';
}
actions.each((index, action) => {
action.style.transitionDelay = disableAnimation ? '0ms' : ((actions.length - index) * 25 + 'ms');
});
} else {
element.removeClass('mat-is-open');
toolbarElement.style.pointerEvents = 'none';
backgroundElement.style.transform = 'scale(1)';
backgroundElement.style.top = '0';
if (element.hasClass('mat-right')) {
backgroundElement.style.left = '0';
backgroundElement.style.right = null;
}
if (element.hasClass('mat-left')) {
backgroundElement.style.right = '0';
backgroundElement.style.left = null;
}
backgroundElement.style.transitionDelay = disableAnimation ? '0ms' : '200ms';
actions.each((index, action) => {
action.style.transitionDelay = (disableAnimation ? 0 : 200) + (index * 25) + 'ms';
});
}
}
}
}

70
ui-ngx/src/app/shared/models/alias.models.ts

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityType } from '@shared/models/entity-type.models';
export enum AliasFilterType {
singleEntity = 'singleEntity',
entityList = 'entityList',
entityName = 'entityName',
stateEntity = 'stateEntity',
assetType = 'assetType',
deviceType = 'deviceType',
entityViewType = 'entityViewType',
relationsQuery = 'relationsQuery',
assetSearchQuery = 'assetSearchQuery',
deviceSearchQuery = 'deviceSearchQuery',
entityViewSearchQuery = 'entityViewSearchQuery'
}
export const aliasFilterTypeTranslationMap = new Map<AliasFilterType, string>(
[
[ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ],
[ AliasFilterType.entityList, 'alias.filter-type-entity-list' ],
[ AliasFilterType.entityName, 'alias.filter-type-entity-name' ],
[ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ],
[ AliasFilterType.assetType, 'alias.filter-type-asset-type' ],
[ AliasFilterType.deviceType, 'alias.filter-type-device-type' ],
[ AliasFilterType.entityViewType, 'alias.filter-type-entity-view-type' ],
[ AliasFilterType.relationsQuery, 'alias.filter-type-relations-query' ],
[ AliasFilterType.assetSearchQuery, 'alias.filter-type-asset-search-query' ],
[ AliasFilterType.deviceSearchQuery, 'alias.filter-type-device-search-query' ],
[ AliasFilterType.entityViewSearchQuery, 'alias.filter-type-entity-view-search-query' ]
]
);
export interface EntityAliasFilter {
type: AliasFilterType;
entityType: EntityType;
resolveMultiple: boolean;
entityList?: string[];
entityNameFilter?: string;
[key: string]: any;
// TODO:
}
export interface EntityAlias {
id: string;
alias: string;
filter: EntityAliasFilter;
[key: string]: any;
// TODO:
}
export interface EntityAliases {
[id: string]: EntityAlias;
}

70
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -14,22 +14,26 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {BaseData} from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import {DashboardId} from '@shared/models/id/dashboard-id'; import { DashboardId } from '@shared/models/id/dashboard-id';
import {TenantId} from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import {ShortCustomerInfo} from '@shared/models/customer.model'; import { ShortCustomerInfo } from '@shared/models/customer.model';
import { Widget } from './widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityAlias, EntityAliases } from './alias.models';
export interface DashboardInfo extends BaseData<DashboardId> { export interface DashboardInfo extends BaseData<DashboardId> {
tenantId: TenantId; tenantId?: TenantId;
title: string; title?: string;
assignedCustomers: Array<ShortCustomerInfo>; assignedCustomers?: Array<ShortCustomerInfo>;
} }
export interface WidgetLayout { export interface WidgetLayout {
sizeX: number; sizeX: number;
sizeY: number; sizeY: number;
mobileHeight: number; mobileHeight?: number;
mobileOrder: number; mobileOrder?: number;
col: number; col: number;
row: number; row: number;
} }
@ -38,13 +42,59 @@ export interface WidgetLayouts {
[id: string]: WidgetLayout; [id: string]: WidgetLayout;
} }
export interface GridSettings {
backgroundColor?: string;
color?: string;
columns?: number;
margins?: [number, number];
backgroundSizeMode?: string;
[key: string]: any;
// TODO:
}
export interface DashboardLayout {
widgets: WidgetLayouts;
gridSettings: GridSettings;
}
export declare type DashboardLayoutId = 'main' | 'right';
export interface DashboardStateLayouts {
main?: DashboardLayout;
right?: DashboardLayout;
}
export interface DashboardState {
name: string;
root: boolean;
layouts: DashboardStateLayouts;
}
export declare type StateControllerId = 'entity' | 'default' | string;
export interface DashboardSettings {
stateControllerId?: StateControllerId;
showTitle?: boolean;
showDashboardsSelect?: boolean;
showEntitiesSelect?: boolean;
showDashboardTimewindow?: boolean;
showDashboardExport?: boolean;
toolbarAlwaysOpen?: boolean;
titleColor?: string;
}
export interface DashboardConfiguration { export interface DashboardConfiguration {
timewindow?: Timewindow;
settings?: DashboardSettings;
widgets?: {[id: string]: Widget } | Widget[];
states?: {[id: string]: DashboardState };
entityAliases?: EntityAliases;
[key: string]: any; [key: string]: any;
// TODO: // TODO:
} }
export interface Dashboard extends DashboardInfo { export interface Dashboard extends DashboardInfo {
configuration: DashboardConfiguration; configuration?: DashboardConfiguration;
} }
export function isPublicDashboard(dashboard: DashboardInfo): boolean { export function isPublicDashboard(dashboard: DashboardInfo): boolean {

28
ui-ngx/src/app/shared/models/entity.models.ts

@ -0,0 +1,28 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
export interface EntityInfo {
origEntity?: BaseData<EntityId>;
name?: string;
label?: string;
entityType?: EntityType;
id?: string;
entityDescription?: string;
}

12
ui-ngx/src/app/shared/models/widget.models.ts

@ -38,6 +38,8 @@ export interface WidgetTypeTemplate {
export interface WidgetTypeData { export interface WidgetTypeData {
name: string; name: string;
icon: string;
isMdiIcon?: boolean;
template: WidgetTypeTemplate; template: WidgetTypeTemplate;
} }
@ -47,6 +49,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
widgetType.timeseries, widgetType.timeseries,
{ {
name: 'widget.timeseries', name: 'widget.timeseries',
icon: 'timeline',
template: { template: {
bundleAlias: 'charts', bundleAlias: 'charts',
alias: 'basic_timeseries' alias: 'basic_timeseries'
@ -57,6 +60,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
widgetType.latest, widgetType.latest,
{ {
name: 'widget.latest-values', name: 'widget.latest-values',
icon: 'track_changes',
template: { template: {
bundleAlias: 'cards', bundleAlias: 'cards',
alias: 'attributes_card' alias: 'attributes_card'
@ -67,6 +71,8 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
widgetType.rpc, widgetType.rpc,
{ {
name: 'widget.rpc', name: 'widget.rpc',
icon: 'mdi:developer-board',
isMdiIcon: true,
template: { template: {
bundleAlias: 'gpio_widgets', bundleAlias: 'gpio_widgets',
alias: 'basic_gpio_control' alias: 'basic_gpio_control'
@ -77,6 +83,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
widgetType.alarm, widgetType.alarm,
{ {
name: 'widget.alarm', name: 'widget.alarm',
icon: 'error',
template: { template: {
bundleAlias: 'alarm_widgets', bundleAlias: 'alarm_widgets',
alias: 'alarms_table' alias: 'alarms_table'
@ -87,6 +94,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
widgetType.static, widgetType.static,
{ {
name: 'widget.static', name: 'widget.static',
icon: 'font_download',
template: { template: {
bundleAlias: 'cards', bundleAlias: 'cards',
alias: 'html_card' alias: 'html_card'
@ -214,7 +222,7 @@ export enum DatasourceType {
} }
export interface Datasource { export interface Datasource {
type: DatasourceType; type?: DatasourceType | any;
name?: string; name?: string;
dataKeys?: Array<DataKey>; dataKeys?: Array<DataKey>;
entityType?: EntityType; entityType?: EntityType;
@ -333,7 +341,7 @@ export interface WidgetConfig {
export interface Widget { export interface Widget {
id?: string; id?: string;
typeId: WidgetTypeId; typeId?: WidgetTypeId;
isSystemType: boolean; isSystemType: boolean;
bundleAlias: string; bundleAlias: string;
typeAlias: string; typeAlias: string;

4
ui-ngx/src/app/shared/models/window-message.model.ts

@ -14,9 +14,9 @@
/// limitations under the License. /// limitations under the License.
/// ///
export type WindowMessageType = 'widgetException'; export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated';
export interface WindowMessage { export interface WindowMessage {
type: WindowMessageType; type: WindowMessageType;
data: any; data?: any;
} }

99
ui-ngx/src/app/shared/shared.module.ts

@ -14,12 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import {NgModule} from '@angular/core'; import { NgModule } from '@angular/core';
import {CommonModule, DatePipe} from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import {FooterComponent} from './components/footer.component'; import { FooterComponent } from './components/footer.component';
import {LogoComponent} from './components/logo.component'; import { LogoComponent } from './components/logo.component';
import {TbSnackBarComponent, ToastDirective} from './components/toast.directive'; import { TbSnackBarComponent, ToastDirective } from './components/toast.directive';
import {BreadcrumbComponent} from '@app/shared/components/breadcrumb.component'; import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component';
import { import {
MatAutocompleteModule, MatAutocompleteModule,
@ -51,43 +51,49 @@ import {
MatToolbarModule, MatToolbarModule,
MatTooltipModule MatTooltipModule
} from '@angular/material'; } from '@angular/material';
import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import {GridsterModule} from 'angular-gridster2'; import { GridsterModule } from 'angular-gridster2';
import {FlexLayoutModule} from '@angular/flex-layout'; import { FlexLayoutModule } from '@angular/flex-layout';
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {RouterModule} from '@angular/router'; import { RouterModule } from '@angular/router';
import {ShareModule as ShareButtonsModule} from '@ngx-share/core'; import { ShareModule as ShareButtonsModule } from '@ngx-share/core';
import {UserMenuComponent} from '@shared/components/user-menu.component'; import { HotkeyModule } from 'angular2-hotkeys';
import {NospacePipe} from './pipe/nospace.pipe'; import { UserMenuComponent } from '@shared/components/user-menu.component';
import {TranslateModule} from '@ngx-translate/core'; import { NospacePipe } from './pipe/nospace.pipe';
import {TbCheckboxComponent} from '@shared/components/tb-checkbox.component'; import { TranslateModule } from '@ngx-translate/core';
import {HelpComponent} from '@shared/components/help.component'; import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
import {TbAnchorComponent} from '@shared/components/tb-anchor.component'; import { HelpComponent } from '@shared/components/help.component';
import {MillisecondsToTimeStringPipe} from '@shared/pipe/milliseconds-to-time-string.pipe'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import {TimewindowComponent} from '@shared/components/time/timewindow.component'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import {OverlayModule} from '@angular/cdk/overlay'; import { TimewindowComponent } from '@shared/components/time/timewindow.component';
import {TimewindowPanelComponent} from '@shared/components/time/timewindow-panel.component'; import { OverlayModule } from '@angular/cdk/overlay';
import {TimeintervalComponent} from '@shared/components/time/timeinterval.component'; import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component'; import { TimeintervalComponent } from '@shared/components/time/timeinterval.component';
import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe'; import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component';
import {ClipboardModule} from 'ngx-clipboard'; import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
import { ClipboardModule } from 'ngx-clipboard';
import { ValueInputComponent } from '@shared/components/value-input.component'; import { ValueInputComponent } from '@shared/components/value-input.component';
import {FullscreenDirective} from '@shared/components/fullscreen.directive'; import { FullscreenDirective } from '@shared/components/fullscreen.directive';
import {HighlightPipe} from '@shared/pipe/highlight.pipe'; import { HighlightPipe } from '@shared/pipe/highlight.pipe';
import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component'; import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component';
import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component'; import { EntitySubTypeSelectComponent } from './components/entity/entity-subtype-select.component';
import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component'; import { EntityAutocompleteComponent } from './components/entity/entity-autocomplete.component';
import {EntityListComponent} from '@shared/components/entity/entity-list.component'; import { EntityListComponent } from '@shared/components/entity/entity-list.component';
import {EntityTypeSelectComponent} from './components/entity/entity-type-select.component'; import { EntityTypeSelectComponent } from './components/entity/entity-type-select.component';
import {EntitySelectComponent} from './components/entity/entity-select.component'; import { EntitySelectComponent } from './components/entity/entity-select.component';
import {DatetimeComponent} from '@shared/components/time/datetime.component'; import { DatetimeComponent } from '@shared/components/time/datetime.component';
import {EntityKeysListComponent} from './components/entity/entity-keys-list.component'; import { EntityKeysListComponent } from './components/entity/entity-keys-list.component';
import {SocialSharePanelComponent} from './components/socialshare-panel.component'; import { SocialSharePanelComponent } from './components/socialshare-panel.component';
import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component';
import { EntityListSelectComponent } from './components/entity/entity-list-select.component'; import { EntityListSelectComponent } from './components/entity/entity-list-select.component';
import { JsonObjectEditComponent } from './components/json-object-edit.component'; import { JsonObjectEditComponent } from './components/json-object-edit.component';
import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
import { CircularProgressDirective } from './components/circular-progress.directive';
import { MatSpinner } from '@angular/material/progress-spinner';
import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component';
import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -100,6 +106,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
TbSnackBarComponent, TbSnackBarComponent,
TbAnchorComponent, TbAnchorComponent,
TimewindowPanelComponent, TimewindowPanelComponent,
DashboardSelectPanelComponent,
MatSpinner
], ],
declarations: [ declarations: [
FooterComponent, FooterComponent,
@ -107,6 +115,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
FooterFabButtonsComponent, FooterFabButtonsComponent,
ToastDirective, ToastDirective,
FullscreenDirective, FullscreenDirective,
CircularProgressDirective,
TbAnchorComponent, TbAnchorComponent,
HelpComponent, HelpComponent,
TbCheckboxComponent, TbCheckboxComponent,
@ -116,6 +125,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
TimewindowComponent, TimewindowComponent,
TimewindowPanelComponent, TimewindowPanelComponent,
TimeintervalComponent, TimeintervalComponent,
DashboardSelectComponent,
DashboardSelectPanelComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
DatetimeComponent, DatetimeComponent,
ValueInputComponent, ValueInputComponent,
@ -131,6 +142,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
RelationTypeAutocompleteComponent, RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent, JsonObjectEditComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
@ -176,7 +190,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
OverlayModule, OverlayModule,
ShareButtonsModule ShareButtonsModule,
HotkeyModule
], ],
exports: [ exports: [
FooterComponent, FooterComponent,
@ -184,6 +199,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
FooterFabButtonsComponent, FooterFabButtonsComponent,
ToastDirective, ToastDirective,
FullscreenDirective, FullscreenDirective,
CircularProgressDirective,
TbAnchorComponent, TbAnchorComponent,
HelpComponent, HelpComponent,
TbCheckboxComponent, TbCheckboxComponent,
@ -192,6 +208,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
TimewindowComponent, TimewindowComponent,
TimewindowPanelComponent, TimewindowPanelComponent,
TimeintervalComponent, TimeintervalComponent,
DashboardSelectComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
DatetimeComponent, DatetimeComponent,
DashboardAutocompleteComponent, DashboardAutocompleteComponent,
@ -206,6 +223,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
RelationTypeAutocompleteComponent, RelationTypeAutocompleteComponent,
SocialSharePanelComponent, SocialSharePanelComponent,
JsonObjectEditComponent, JsonObjectEditComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,
ValueInputComponent, ValueInputComponent,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
@ -244,6 +264,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons
ReactiveFormsModule, ReactiveFormsModule,
OverlayModule, OverlayModule,
ShareButtonsModule, ShareButtonsModule,
HotkeyModule,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,

BIN
ui-ngx/src/assets/split.js/grips/horizontal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

BIN
ui-ngx/src/assets/split.js/grips/vertical.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

21
ui-ngx/src/styles.scss

@ -169,6 +169,25 @@ section.tb-header-buttons {
} }
} }
section.tb-footer-buttons {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 30;
pointer-events: none;
.tb-btn-footer {
margin: 6px 8px;
position: relative !important;
display: inline-block !important;
animation: tbMoveFromBottomFade .3s ease both;
&.tb-hide {
animation: tbMoveToBottomFade .3s ease both;
}
}
}
.tb-details-buttons { .tb-details-buttons {
button { button {
margin: 6px 8px; margin: 6px 8px;
@ -293,7 +312,7 @@ pre.tb-highlight {
} }
.tb-fullscreen-parent { .tb-fullscreen-parent {
background: #fff; background: #eee;
} }
mat-label { mat-label {

66
ui-ngx/src/theme.scss

@ -166,9 +166,52 @@ $tb-dark-theme: get-tb-dark-theme(
$tb-accent $tb-accent
); );
@mixin mat-fab-toolbar-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
mat-fab-toolbar {
.mat-fab-toolbar-background {
background: mat-color($background, app-bar);
color: mat-color($foreground, text);
}
&.mat-primary {
.mat-fab-toolbar-background {
@include _mat-toolbar-color($primary);
}
}
&.mat-accent {
.mat-fab-toolbar-background {
@include _mat-toolbar-color($accent);
}
}
&.mat-warn {
.mat-fab-toolbar-background {
@include _mat-toolbar-color($warn);
}
}
}
}
@mixin tb-components-theme($theme) {
$primary: map-get($theme, primary);
mat-toolbar{
&.mat-hue-3 {
background-color: mat-color($primary, 'A100');
}
}
@include mat-fab-toolbar-theme($tb-theme);
}
.tb-default { .tb-default {
@include angular-material-theme($tb-theme); @include angular-material-theme($tb-theme);
@include mat-datetimepicker-theme($tb-theme); @include mat-datetimepicker-theme($tb-theme);
@include tb-components-theme($tb-theme);
} }
.tb-dark { .tb-dark {
@ -388,11 +431,13 @@ $tb-dark-theme: get-tb-dark-theme(
width: 32px; width: 32px;
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
padding: 0 !important;
} }
&.tb-mat-96 { &.tb-mat-96 {
width: 96px; width: 96px;
height: 96px; height: 96px;
line-height: 96px; line-height: 96px;
padding: 0 !important;
} }
} }
@ -549,6 +594,27 @@ $tb-dark-theme: get-tb-dark-theme(
right: 0; right: 0;
} }
.tb-progress-cover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 6;
background-color: #eee;
opacity: 1;
}
.mat-button.tb-fullscreen-button-style,
.tb-fullscreen-button-style {
background: #ccc;
opacity: .85;
mat-icon {
color: #666;
}
}
span.no-data-found { span.no-data-found {
position: relative; position: relative;
display: flex; display: flex;

2
ui-ngx/src/tsconfig.app.json

@ -2,7 +2,7 @@
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../out-tsc/app", "outDir": "../out-tsc/app",
"types": ["node", "jquery", "flot", "tinycolor2"] "types": ["node", "jquery", "flot", "tinycolor2", "js-beautify"]
}, },
"exclude": [ "exclude": [
"test.ts", "test.ts",

39
ui-ngx/src/typings/split.js.typings.d.ts

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
interface SplitOptions {
sizes?: number[];
minSize?: number[] | number;
gutterSize?: number;
snapOffset?: number;
direction?: 'horizontal' | 'vertical';
cursor?: 'col-resize' | 'row-resize';
gutter?: (index: number, direction: string) => HTMLElement;
elementStyle?: (dimension: string, elementSize: number, gutterSize: number) => any;
gutterStyle?: (dimension: string, gutterSize: number) => any;
onDrag?: () => void;
onDragStart?: () => void;
onDragEnd?: () => void;
}
interface SplitObject {
setSizes: (sizes: number[]) => void;
getSizes: () => number[];
collapse: (index: number) => void;
destroy: () => void;
}
declare function Split(elements: HTMLElement | string[], options?: SplitOptions): SplitObject;

3
ui-ngx/tsconfig.json

@ -14,7 +14,8 @@
"typeRoots": [ "typeRoots": [
"node_modules/@types", "node_modules/@types",
"src/typings/jquery.typings.d.ts", "src/typings/jquery.typings.d.ts",
"src/typings/jquery.flot.typings.d.ts" "src/typings/jquery.flot.typings.d.ts",
"src/typings/split.js.typings.d.ts"
], ],
"paths": { "paths": {
"@app/*": ["src/app/*"], "@app/*": ["src/app/*"],

Loading…
Cancel
Save