diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index fd0555c661..f58d5bbbb5 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -47,6 +47,10 @@ "node_modules/flot/src/plugins/jquery.flot.stack.js", "node_modules/flot.curvedlines/curvedLines.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/ext-language_tools.js", "node_modules/ace-builds/src-min/ext-searchbox.js", diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index 5f93537aff..388d4cb64e 100644 --- a/ui-ngx/package-lock.json +++ b/ui-ngx/package-lock.json @@ -1170,12 +1170,23 @@ "@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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "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": { "version": "10.14.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.15.tgz", @@ -1434,6 +1445,11 @@ "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": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -1523,6 +1539,15 @@ "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": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -1955,8 +1980,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2143,7 +2167,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2854,8 +2877,7 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "commondir": { "version": "1.0.1", @@ -3107,8 +3129,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -3122,6 +3143,15 @@ "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": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -3762,6 +3792,17 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4574,8 +4615,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -5183,7 +5223,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5709,7 +5748,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5718,14 +5756,12 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "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": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -6918,7 +6966,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -7258,7 +7305,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7368,7 +7414,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -7376,8 +7421,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -7386,6 +7430,11 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -7563,6 +7612,15 @@ "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": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7801,7 +7859,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -7877,8 +7934,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "3.1.0", @@ -7894,14 +7950,12 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -8222,8 +8276,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -8465,6 +8518,11 @@ "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": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", @@ -8612,8 +8670,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.3.0", @@ -9208,8 +9265,7 @@ "semver": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "semver-dsl": { "version": "1.0.1", @@ -9408,6 +9464,11 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "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": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -9920,6 +9981,11 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11121,8 +11187,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "3.3.3", @@ -11180,8 +11245,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { "version": "12.0.5", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 12c92c1192..8527312a0f 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -33,6 +33,7 @@ "@ngx-translate/http-loader": "^4.0.0", "ace-builds": "^1.4.5", "angular-gridster2": "^8.1.0", + "angular2-hotkeys": "^2.1.5", "base64-js": "^1.3.1", "compass-sass-mixins": "^0.12.7", "core-js": "^3.1.4", @@ -44,6 +45,7 @@ "javascript-detect-element-resize": "^0.5.3", "jquery": "^3.4.1", "jquery.terminal": "^2.8.0", + "js-beautify": "^1.10.2", "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", "moment": "^2.24.0", @@ -51,6 +53,7 @@ "ngx-translate-messageformat-compiler": "^4.5.0", "rxjs": "~6.5.2", "screenfull": "^4.2.1", + "split.js": "^1.5.11", "tinycolor2": "^1.4.1", "tslib": "^1.10.0", "typeface-roboto": "^0.0.75", @@ -66,6 +69,7 @@ "@types/jasmine": "~3.4.0", "@types/jasminewd2": "~2.0.6", "@types/jquery": "^3.3.31", + "@types/js-beautify": "^1.8.1", "@types/node": "~10.14.15", "@types/tinycolor2": "^1.4.2", "codelyzer": "~5.1.0", diff --git a/ui-ngx/src/app/app.module.ts b/ui-ngx/src/app/app.module.ts index 1a1e6ab20b..98cc2d158b 100644 --- a/ui-ngx/src/app/app.module.ts +++ b/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 { AppComponent } from './app.component'; +import { DashboardRoutingModule } from './modules/dashboard/dashboard-routing.module'; @NgModule({ declarations: [ @@ -35,7 +36,8 @@ import { AppComponent } from './app.component'; AppRoutingModule, CoreModule, LoginModule, - HomeModule + HomeModule, + DashboardRoutingModule ], providers: [], bootstrap: [AppComponent] diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 12e7232b26..8f16ee5209 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -14,19 +14,26 @@ /// 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 { Datasource } from '@app/shared/models/widget.models'; 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 { entityAliasesChanged: Observable>; + entityAliasResolved: Observable; [key: string]: any | null; constructor() { this.entityAliasesChanged = new Subject>().asObservable(); + this.entityAliasResolved = new Subject().asObservable(); } getAliasInfo(aliasId): Observable { @@ -36,4 +43,72 @@ export class DummyAliasController implements IAliasController { resolveDatasources(datasources: Array): Observable> { 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>(); + entityAliasesChanged: Observable> = this.entityAliasesChangedSubject.asObservable(); + + private entityAliasResolvedSubject = new Subject(); + entityAliasResolved: Observable = 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 { + return of(null); + } + + resolveDatasources(datasources: Array): Observable> { + 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) { + } + } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 9afaa2053d..9301857659 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/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 { DatasourceService } from '@core/api/datasource.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 { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -66,20 +68,24 @@ export interface WidgetActionsApi { } export interface AliasInfo { + alias?: string; stateEntity?: boolean; - currentEntity?: { - id: string; - entityType: EntityType; - name?: string; - }; + currentEntity?: EntityInfo; + selectedId?: string; + resolvedEntities?: Array; [key: string]: any | null; // TODO: } export interface IAliasController { entityAliasesChanged: Observable>; - getAliasInfo(aliasId): Observable; + entityAliasResolved: Observable; + getAliasInfo(aliasId: string): Observable; + getInstantAliasInfo(aliasId: string): AliasInfo; resolveDatasources(datasources: Array): Observable>; + getEntityAliases(): EntityAliases; + updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); + updateEntityAliases(entityAliases: EntityAliases); [key: string]: any | null; // TODO: } @@ -97,17 +103,14 @@ export interface StateParams { } export interface IStateController { - getStateParams: () => StateParams; - openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void; - updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; + getStateParams?: () => StateParams; + openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void; + updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; + openRightLayout: () => void; + preserveState?: () => void; // TODO: } -export interface EntityInfo { - entityId: EntityId; - entityName: string; -} - export interface SubscriptionInfo { type: DatasourceType; name?: string; @@ -171,6 +174,11 @@ export interface WidgetSubscriptionOptions { // TODO: } +export interface SubscriptionEntityInfo { + entityId: EntityId; + entityName: string; +} + export interface IWidgetSubscription { id: string; @@ -201,7 +209,7 @@ export interface IWidgetSubscription { rpcErrorText?: string; rpcRejection?: HttpErrorResponse; - getFirstEntityInfo(): EntityInfo; + getFirstEntityInfo(): SubscriptionEntityInfo; onAliasesChanged(aliasIds: Array): boolean; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 5545b22ef7..da15591613 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -15,8 +15,7 @@ /// import { - EntityInfo, - IWidgetSubscription, + IWidgetSubscription, SubscriptionEntityInfo, WidgetSubscriptionCallbacks, WidgetSubscriptionContext, WidgetSubscriptionOptions @@ -339,7 +338,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.onDataUpdated(); } - getFirstEntityInfo(): EntityInfo { + getFirstEntityInfo(): SubscriptionEntityInfo { return undefined; } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 1ae48af68c..c6ede83445 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -42,6 +42,8 @@ import {TimeService} from '@core/services/time.service'; }) export class AuthService { + forceFullscreen = false; // TODO: + constructor( private store: Store, private http: HttpClient, diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts index c47572f4a3..343592ac64 100644 --- a/ui-ngx/src/app/core/core.module.ts +++ b/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 { WINDOW_PROVIDERS } from '@core/services/window.service'; import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component"; +import { HotkeyModule } from 'angular2-hotkeys'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); @@ -79,6 +80,7 @@ export function HttpLoaderFactory(http: HttpClient) { useClass: TranslateDefaultCompiler } }), + HotkeyModule.forRoot(), // ngrx StoreModule.forRoot(reducers, { metaReducers }), diff --git a/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts b/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts index ecf827d91c..af5f2852cc 100644 --- a/ui-ngx/src/app/core/guards/confirm-on-exit.guard.ts +++ b/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 { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; +import { isDefined } from '../utils'; export interface HasConfirmForm { confirmForm(): FormGroup; } +export interface HasDirtyFlag { + isDirty: boolean; +} + @Injectable({ providedIn: 'root' }) -export class ConfirmOnExitGuard implements CanDeactivate { +export class ConfirmOnExitGuard implements CanDeactivate { constructor(private store: Store, private dialogService: DialogService, private translate: TranslateService) { } - canDeactivate(component: HasConfirmForm, + canDeactivate(component: HasConfirmForm & HasDirtyFlag, route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { @@ -54,9 +59,17 @@ export class ConfirmOnExitGuard implements CanDeactivate { } ); - if (component.confirmForm && auth && auth.isAuthenticated) { - const confirmForm = component.confirmForm(); - if (confirmForm && confirmForm.dirty) { + if (auth && auth.isAuthenticated) { + let isDirty = false; + 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( this.translate.instant('confirm-on-exit.title'), this.translate.instant('confirm-on-exit.html-message') diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 4c0606b599..50739dc6df 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/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 { PageData } from '@shared/models/page/page-data'; 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 { TranslateService } from '@ngx-translate/core'; import { ResourcesService } from '../services/resources.service'; +import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models'; +import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -70,4 +72,23 @@ export class WidgetService { return this.http.get(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); } + + public getWidgetTypeById(widgetTypeId: string, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.get(`/api/widgetType/${widgetTypeId}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getWidgetTemplate(widgetTypeParam: widgetType, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + 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; + }) + ); + } } diff --git a/ui-ngx/src/app/core/interceptors/load.selectors.ts b/ui-ngx/src/app/core/interceptors/load.selectors.ts index a34063c465..553d199d7c 100644 --- a/ui-ngx/src/app/core/interceptors/load.selectors.ts +++ b/ui-ngx/src/app/core/interceptors/load.selectors.ts @@ -14,10 +14,13 @@ /// limitations under the License. /// -import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; 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( 'load' @@ -32,3 +35,11 @@ export const selectIsLoading = createSelector( selectLoadState, (state: LoadState) => state.isLoading ); + +export function getCurrentIsLoading(store: Store): boolean { + let isLoading: boolean; + store.pipe(select(selectIsLoading), take(1)).subscribe( + val => isLoading = val + ); + return isLoading; +} diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts new file mode 100644 index 0000000000..30fdb3aded --- /dev/null +++ b/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} = {}; + const targetDevicesByAliasId: {[aliasId: string]: Array>} = {}; + 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}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): 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}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): 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}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): 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}, + targetDevicesByAliasId: {[aliasId: string]: Array>}): 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; + } + } + +} diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 6622051e8d..b323baf2b5 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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 { TranslateService } from '@ngx-translate/core'; 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 { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { alarmFields } from '@shared/models/alarm.models'; import { materialColors } from '@app/shared/models/material.models'; +import { WidgetInfo } from '@home/models/widget-component.models'; @Injectable({ providedIn: 'root' @@ -34,7 +35,7 @@ export class UtilsService { iframeMode = false; widgetEditMode = false; - editWidgetInfo: any = null; + editWidgetInfo: WidgetInfo = null; constructor(@Inject(WINDOW) private window: Window, private translate: TranslateService) { @@ -87,7 +88,7 @@ export class UtilsService { type: 'widgetException', data }; - this.window.parent.postMessage(message, '*'); + this.window.parent.postMessage(JSON.stringify(message), '*'); } return data; } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 8cf2cdc59b..6b8846f8d3 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -95,6 +95,10 @@ export function isNumber(value: any): boolean { return typeof value === 'number'; } +export function isString(value: any): boolean { + return typeof value === 'string'; +} + export function objToBase64(obj: any): string { const json = JSON.stringify(obj); const encoded = utf8Encode(json); diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts new file mode 100644 index 0000000000..752857bf5d --- /dev/null +++ b/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 { } diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts new file mode 100644 index 0000000000..5379e16ba5 --- /dev/null +++ b/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 { + + 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 { } diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-routing.module.ts new file mode 100644 index 0000000000..24228db507 --- /dev/null +++ b/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 { } diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html new file mode 100644 index 0000000000..8e675e7a39 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html @@ -0,0 +1,30 @@ + +
+
+ + {{alias.value.alias}} + + + {{resolvedEntity.name}} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.scss new file mode 100644 index 0000000000..3fa6292b41 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts new file mode 100644 index 0000000000..f05f5bc69a --- /dev/null +++ b/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('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]); + } + } + + +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html new file mode 100644 index 0000000000..22d920917d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html @@ -0,0 +1,32 @@ + +
+ + + {{displayValue}} + +
diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.scss new file mode 100644 index 0000000000..393de63842 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts new file mode 100644 index 0000000000..cc9b6b91ea --- /dev/null +++ b/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(); + + 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([ + [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; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index d306bc6a26..2f15f9d253 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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 { WidgetComponentService } from './widget/widget-component.service'; 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({ entryComponents: [ @@ -48,7 +50,8 @@ import { LegendComponent } from '@home/components/widget/legend.component'; AlarmTableHeaderComponent, AlarmDetailsDialogComponent, AddAttributeDialogComponent, - EditAttributeValuePanelComponent + EditAttributeValuePanelComponent, + AliasesEntitySelectPanelComponent ], declarations: [ @@ -69,6 +72,8 @@ import { LegendComponent } from '@home/components/widget/legend.component'; AttributeTableComponent, AddAttributeDialogComponent, EditAttributeValuePanelComponent, + AliasesEntitySelectPanelComponent, + AliasesEntitySelectComponent, DashboardComponent, WidgetComponent, LegendComponent @@ -89,6 +94,7 @@ import { LegendComponent } from '@home/components/widget/legend.component'; AlarmTableComponent, AlarmDetailsDialogComponent, AttributeTableComponent, + AliasesEntitySelectComponent, DashboardComponent, WidgetComponent, LegendComponent diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index e3d8bd9c1c..0097c56238 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -61,10 +61,9 @@ import { WidgetTypeInstance } from '@home/models/widget-component.models'; import { - EntityInfo, IWidgetSubscription, StateObject, - StateParams, + StateParams, SubscriptionEntityInfo, SubscriptionInfo, WidgetSubscriptionContext, WidgetSubscriptionOptions @@ -1065,7 +1064,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); } - private getActiveEntityInfo(): EntityInfo { + private getActiveEntityInfo(): SubscriptionEntityInfo { let entityInfo = this.widgetContext.activeEntityInfo; if (!entityInfo) { for (const id of Object.keys(this.widgetContext.subscriptions)) { diff --git a/ui-ngx/src/app/modules/home/home.component.html b/ui-ngx/src/app/modules/home/home.component.html index c4d387464e..b29f051677 100644 --- a/ui-ngx/src/app/modules/home/home.component.html +++ b/ui-ngx/src/app/modules/home/home.component.html @@ -38,7 +38,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 4d83abb163..dde0ba0a27 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/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 { + activeComponent: any; + sidenavMode = 'side'; sidenavOpened = true; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index cb8c8605ea..35517d5209 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -31,12 +31,11 @@ import { } from '@shared/models/widget.models'; import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { - EntityInfo, IAliasController, IStateController, IWidgetSubscription, IWidgetUtils, - RpcApi, + RpcApi, SubscriptionEntityInfo, TimewindowFunctions, WidgetActionsApi, WidgetSubscriptionApi @@ -85,7 +84,7 @@ export interface WidgetContext { actionsApi?: WidgetActionsApi; stateController?: IStateController; aliasController?: IAliasController; - activeEntityInfo?: EntityInfo; + activeEntityInfo?: SubscriptionEntityInfo; widgetTitleTemplate?: string; widgetTitle?: string; customHeaderActions?: Array; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html new file mode 100644 index 0000000000..e2956320ba --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html @@ -0,0 +1,157 @@ + +
+
+ +
+
+ + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+
+
+

{{ dashboard.title }}

+ + dashboard.title + + +
+
+
+ TODO: MAIN LAYOUT tb-dashboard-layout +
+ + + TODO: RIGHT LAYOUT tb-dashboard-layout + + +
+ + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss new file mode 100644 index 0000000000..4b927b1eeb --- /dev/null +++ b/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; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts new file mode 100644 index 0000000000..73c81a2347 --- /dev/null +++ b/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(); + + 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, + @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); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts new file mode 100644 index 0000000000..c08000c4e2 --- /dev/null +++ b/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; + widgetLayouts: {[id: string]: WidgetLayout}; + gridSettings: GridSettings; + ignoreLoading: boolean; +} + +export interface DashboardPageLayout { + show: boolean; + layoutCtx: DashboardPageLayoutContext; +} + +export interface DashboardPageLayouts { + main: DashboardPageLayout; + right: DashboardPageLayout; +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts index ee52db4849..0d1e1c6db1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -14,12 +14,39 @@ /// limitations under the License. /// -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; +import { Injectable, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; import {Authority} from '@shared/models/authority.enum'; 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 { + + constructor(private dashboardService: DashboardService, + private dashboardUtils: DashboardUtilsService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + 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 = [ { @@ -42,6 +69,22 @@ const routes: Routes = [ resolve: { 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)], exports: [RouterModule], providers: [ - DashboardsTableConfigResolver + DashboardsTableConfigResolver, + DashboardResolver ] }) export class DashboardRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html new file mode 100644 index 0000000000..50291df2a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.scss new file mode 100644 index 0000000000..f6a2f6b105 --- /dev/null +++ b/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; + } + } + } + } + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-toolbar.component.ts new file mode 100644 index 0000000000..bee1c2838f --- /dev/null +++ b/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(); + + constructor() { + } + + ngOnInit(): void { + } + + onTriggerClick() { + this.triggerClick.emit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts index 4b66eb7a7e..5ab82cd49a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -14,16 +14,18 @@ /// limitations under the License. /// -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {SharedModule} from '@shared/shared.module'; -import {HomeDialogsModule} from '../../dialogs/home-dialogs.module'; -import {DashboardFormComponent} from '@modules/home/pages/dashboard/dashboard-form.component'; -import {ManageDashboardCustomersDialogComponent} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; -import {DashboardRoutingModule} from './dashboard-routing.module'; -import {MakeDashboardPublicDialogComponent} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; -import {HomeComponentsModule} from '@modules/home/components/home-components.module'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; +import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; +import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; +import { DashboardRoutingModule } from './dashboard-routing.module'; +import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; 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({ entryComponents: [ @@ -36,7 +38,9 @@ import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.com DashboardFormComponent, DashboardTabsComponent, ManageDashboardCustomersDialogComponent, - MakeDashboardPublicDialogComponent + MakeDashboardPublicDialogComponent, + DashboardToolbarComponent, + DashboardPageComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index c29628003e..93335e7aa2 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -303,9 +303,11 @@ export class DashboardsTableConfigResolver implements Resolve +
+ +

widget.select-widget-type

+ + +
+ + +
+
+
+
+ +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss new file mode 100644 index 0000000000..60ba753e8f --- /dev/null +++ b/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); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.ts new file mode 100644 index 0000000000..6e854cb96a --- /dev/null +++ b/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 { + + widgetTypes = widgetType; + + allWidgetTypes = Object.keys(widgetType); + + widgetTypesDataMap = widgetTypesData; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + cancel(): void { + this.dialogRef.close(null); + } + + typeSelected(type: widgetType) { + this.dialogRef.close(type); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html new file mode 100644 index 0000000000..224490a5a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -0,0 +1,255 @@ + + +
+
+ + + + + + + + + {{ widgetTypesDataMap.get(type).name | translate }} + + + + + + + + + + + + + + + + + +
+
+
+
+ + +
+
+
+ + + + +
+
+ +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss new file mode 100644 index 0000000000..220b388c39 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts new file mode 100644 index 0000000000..ef07407bd3 --- /dev/null +++ b/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; + + iframe: JQuery; + + 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, + @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 = { + 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 += '
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; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 98d8fcb797..5f452b346f 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -17,20 +17,24 @@ import { Injectable, NgModule } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; -import {EntitiesTableComponent} from '../../components/entity/entities-table.component'; -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 { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; 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 { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 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() export class WidgetsBundleResolver implements Resolve { @@ -39,13 +43,61 @@ export class WidgetsBundleResolver implements Resolve { } resolve(route: ActivatedRouteSnapshot): Observable { - const widgetsBundleId = route.params.widgetsBundleId; + let widgetsBundleId = route.params.widgetsBundleId; + if (!widgetsBundleId) { + widgetsBundleId = route.parent.params.widgetsBundleId; + } return this.widgetsService.getWidgetsBundle(widgetsBundleId); } } +@Injectable() +export class WidgetEditorDataResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + 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 { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + 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 widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction = + ((route, translate, component) => component ? (component as WidgetEditorComponent).widget.widgetName : ''); + export const routes: Routes = [ { path: 'widgets-bundles', @@ -69,10 +121,7 @@ export const routes: Routes = [ }, { path: ':widgetsBundleId/widgetTypes', - component: WidgetLibraryComponent, data: { - auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], - title: 'widget.widget-library', breadcrumb: { labelFunction: widgetTypesBreadcumbLabelFunction, icon: 'now_widgets' @@ -80,7 +129,49 @@ export const routes: Routes = [ }, resolve: { 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], providers: [ WidgetsBundlesTableConfigResolver, - WidgetsBundleResolver + WidgetsBundleResolver, + WidgetEditorDataResolver, + WidgetEditorAddDataResolver ] }) export class WidgetLibraryRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index 3400d56514..9f1f2e39b9 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/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 { getCurrentAuthUser } from '@core/auth/auth.selectors'; 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 { NULL_UUID } from '@shared/models/id/has-uuid'; 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 { toWidgetInfo } from '@home/models/widget-component.models'; 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({ selector: 'tb-widget-library', @@ -83,8 +90,10 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { constructor(protected store: Store, private route: ActivatedRoute, + private router: Router, private widgetService: WidgetService, - private dialogService: DialogService) { + private dialogService: DialogService, + private dialog: MatDialog) { super(store); this.authUser = getCurrentAuthUser(store); @@ -163,33 +172,43 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { } importWidgetType($event: Event): void { - if (event) { - event.stopPropagation(); + if ($event) { + $event.stopPropagation(); } this.dialogService.todo(); } openWidgetType($event: Event, widget?: Widget): void { - if (event) { - event.stopPropagation(); + if ($event) { + $event.stopPropagation(); } if (widget) { - this.dialogService.todo(); + this.router.navigate([widget.typeId.id], {relativeTo: this.route}); } else { - this.dialogService.todo(); + this.dialog.open(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 { - if (event) { - event.stopPropagation(); + if ($event) { + $event.stopPropagation(); } this.dialogService.todo(); } removeWidgetType($event: Event, widget: Widget): void { - if (event) { - event.stopPropagation(); + if ($event) { + $event.stopPropagation(); } this.dialogService.todo(); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts index e0a1234fae..25ff3a9b78 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts +++ b/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 {HomeComponentsModule} from '@modules/home/components/home-components.module'; 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({ entryComponents: [ - WidgetsBundleComponent + WidgetsBundleComponent, + SelectWidgetTypeDialogComponent ], declarations: [ WidgetsBundleComponent, - WidgetLibraryComponent + WidgetLibraryComponent, + WidgetEditorComponent, + SelectWidgetTypeDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.html b/ui-ngx/src/app/shared/components/breadcrumb.component.html index 7f895270b4..35643ba4fd 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.html +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.html @@ -17,7 +17,7 @@ -->

- {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }}

@@ -26,7 +26,7 @@ {{ breadcrumb.icon }} - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} @@ -34,7 +34,7 @@ {{ breadcrumb.icon }} - {{ breadcrumb.ignoreTranslate ? breadcrumb.label : (breadcrumb.label | translate) }} + {{ breadcrumb.ignoreTranslate ? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : breadcrumb.label) : (breadcrumb.label | translate) }} > diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts index 1ea092d637..57c99d4340 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts @@ -14,7 +14,7 @@ /// 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 { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; @@ -28,6 +28,13 @@ import { TranslateService } from '@ngx-translate/core'; }) export class BreadcrumbComponent implements OnInit, OnDestroy { + activeComponentValue: any; + + @Input() + set activeComponent(activeComponent: any) { + this.activeComponentValue = activeComponent; + } + breadcrumbs$: Subject> = new BehaviorSubject>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); routerEventsSubscription = this.router.events.pipe( @@ -61,9 +68,12 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { const breadcrumbConfig = route.routeConfig.data.breadcrumb as BreadCrumbConfig; if (breadcrumbConfig && !breadcrumbConfig.skip) { let label; + let labelFunction; let ignoreTranslate; if (breadcrumbConfig.labelFunction) { - label = breadcrumbConfig.labelFunction(route, this.translate); + labelFunction = () => { + return breadcrumbConfig.labelFunction(route, this.translate, this.activeComponentValue); + }; ignoreTranslate = true; } else { label = breadcrumbConfig.label || 'home.home'; @@ -71,10 +81,11 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { } const icon = breadcrumbConfig.icon || 'home'; 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 breadcrumb = { label, + labelFunction, ignoreTranslate, icon, isMdiIcon, @@ -89,5 +100,4 @@ export class BreadcrumbComponent implements OnInit, OnDestroy { } return newBreadcrumbs; } - } diff --git a/ui-ngx/src/app/shared/components/breadcrumb.ts b/ui-ngx/src/app/shared/components/breadcrumb.ts index 5db7b7b4c4..e7071ed2ca 100644 --- a/ui-ngx/src/app/shared/components/breadcrumb.ts +++ b/ui-ngx/src/app/shared/components/breadcrumb.ts @@ -19,6 +19,7 @@ import { TranslateService } from '@ngx-translate/core'; export interface BreadCrumb { label: string; + labelFunction?: () => string; ignoreTranslate: boolean; icon: string; isMdiIcon: boolean; @@ -26,7 +27,7 @@ export interface BreadCrumb { queryParams: Params; } -export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService) => string; +export type BreadCrumbLabelFunction = (route: ActivatedRouteSnapshot, translate: TranslateService, component: any) => string; export interface BreadCrumbConfig { labelFunction: BreadCrumbLabelFunction; diff --git a/ui-ngx/src/app/shared/components/circular-progress.directive.ts b/ui-ngx/src/app/shared/components/circular-progress.directive.ts new file mode 100644 index 0000000000..38166e629f --- /dev/null +++ b/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; + + 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; + + 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'; + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html new file mode 100644 index 0000000000..6787d4b85c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.html @@ -0,0 +1,28 @@ + +
+ + {{'dashboard.select-dashboard' | translate}} + + + {{dashboard.title}} + + + +
diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.scss new file mode 100644 index 0000000000..628af4eeed --- /dev/null +++ b/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%; + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts b/ui-ngx/src/app/shared/components/dashboard-select-panel.component.ts new file mode 100644 index 0000000000..ff9de24bc7 --- /dev/null +++ b/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('DashboardSelectPanelData'); + +export interface DashboardSelectPanelData { + dashboards$: Observable>; + 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>; + 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); + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.html b/ui-ngx/src/app/shared/components/dashboard-select.component.html new file mode 100644 index 0000000000..40e80cbdd7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.html @@ -0,0 +1,35 @@ + + + + {{dashboard.title}} + + +
+ +
diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.scss b/ui-ngx/src/app/shared/components/dashboard-select.component.scss new file mode 100644 index 0000000000..bb0233663b --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.ts b/ui-ngx/src/app/shared/components/dashboard-select.component.ts new file mode 100644 index 0000000000..b9d56ac080 --- /dev/null +++ b/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>; + + dashboardId: string | null; + + @ViewChild('dashboardSelectPanelOrigin', {static: false}) dashboardSelectPanelOrigin: CdkOverlayOrigin; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + 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([ + [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> { + let dashboardsObservable: Observable>; + 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; + } + +} diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.html b/ui-ngx/src/app/shared/components/fab-toolbar.component.html new file mode 100644 index 0000000000..384bec4dc1 --- /dev/null +++ b/ui-ngx/src/app/shared/components/fab-toolbar.component.html @@ -0,0 +1,22 @@ + +
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.scss b/ui-ngx/src/app/shared/components/fab-toolbar.component.scss new file mode 100644 index 0000000000..443ff107e0 --- /dev/null +++ b/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); + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/fab-toolbar.component.ts b/ui-ngx/src/app/shared/components/fab-toolbar.component.ts new file mode 100644 index 0000000000..92447f36b4 --- /dev/null +++ b/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) { + } + +} + +@Directive({ + selector: 'mat-fab-actions' +}) +export class FabActionsDirective implements OnInit { + + constructor(private el: ElementRef) { + } + + ngOnInit(): void { + const element = $(this.el.nativeElement); + const children = element.children(); + children.wrap('
'); + } + +} + +@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, + @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('
'); + 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'; + }); + } + } + } + +} diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts new file mode 100644 index 0000000000..d6d9a58c20 --- /dev/null +++ b/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.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; +} diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 5de95e2a1f..e2cf08a6e2 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -14,22 +14,26 @@ /// limitations under the License. /// -import {BaseData} from '@shared/models/base-data'; -import {DashboardId} from '@shared/models/id/dashboard-id'; -import {TenantId} from '@shared/models/id/tenant-id'; -import {ShortCustomerInfo} from '@shared/models/customer.model'; +import { BaseData } from '@shared/models/base-data'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { TenantId } from '@shared/models/id/tenant-id'; +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 { - tenantId: TenantId; - title: string; - assignedCustomers: Array; + tenantId?: TenantId; + title?: string; + assignedCustomers?: Array; } export interface WidgetLayout { sizeX: number; sizeY: number; - mobileHeight: number; - mobileOrder: number; + mobileHeight?: number; + mobileOrder?: number; col: number; row: number; } @@ -38,13 +42,59 @@ export interface WidgetLayouts { [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 { + timewindow?: Timewindow; + settings?: DashboardSettings; + widgets?: {[id: string]: Widget } | Widget[]; + states?: {[id: string]: DashboardState }; + entityAliases?: EntityAliases; [key: string]: any; // TODO: } export interface Dashboard extends DashboardInfo { - configuration: DashboardConfiguration; + configuration?: DashboardConfiguration; } export function isPublicDashboard(dashboard: DashboardInfo): boolean { diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts new file mode 100644 index 0000000000..b1349986fa --- /dev/null +++ b/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; + name?: string; + label?: string; + entityType?: EntityType; + id?: string; + entityDescription?: string; +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index f57e0e52e4..d94fead1e5 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -38,6 +38,8 @@ export interface WidgetTypeTemplate { export interface WidgetTypeData { name: string; + icon: string; + isMdiIcon?: boolean; template: WidgetTypeTemplate; } @@ -47,6 +49,7 @@ export const widgetTypesData = new Map( widgetType.timeseries, { name: 'widget.timeseries', + icon: 'timeline', template: { bundleAlias: 'charts', alias: 'basic_timeseries' @@ -57,6 +60,7 @@ export const widgetTypesData = new Map( widgetType.latest, { name: 'widget.latest-values', + icon: 'track_changes', template: { bundleAlias: 'cards', alias: 'attributes_card' @@ -67,6 +71,8 @@ export const widgetTypesData = new Map( widgetType.rpc, { name: 'widget.rpc', + icon: 'mdi:developer-board', + isMdiIcon: true, template: { bundleAlias: 'gpio_widgets', alias: 'basic_gpio_control' @@ -77,6 +83,7 @@ export const widgetTypesData = new Map( widgetType.alarm, { name: 'widget.alarm', + icon: 'error', template: { bundleAlias: 'alarm_widgets', alias: 'alarms_table' @@ -87,6 +94,7 @@ export const widgetTypesData = new Map( widgetType.static, { name: 'widget.static', + icon: 'font_download', template: { bundleAlias: 'cards', alias: 'html_card' @@ -214,7 +222,7 @@ export enum DatasourceType { } export interface Datasource { - type: DatasourceType; + type?: DatasourceType | any; name?: string; dataKeys?: Array; entityType?: EntityType; @@ -333,7 +341,7 @@ export interface WidgetConfig { export interface Widget { id?: string; - typeId: WidgetTypeId; + typeId?: WidgetTypeId; isSystemType: boolean; bundleAlias: string; typeAlias: string; diff --git a/ui-ngx/src/app/shared/models/window-message.model.ts b/ui-ngx/src/app/shared/models/window-message.model.ts index adb53b2c42..9889214ab6 100644 --- a/ui-ngx/src/app/shared/models/window-message.model.ts +++ b/ui-ngx/src/app/shared/models/window-message.model.ts @@ -14,9 +14,9 @@ /// limitations under the License. /// -export type WindowMessageType = 'widgetException'; +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated'; export interface WindowMessage { type: WindowMessageType; - data: any; + data?: any; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 051d17c936..4a074919b8 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -14,12 +14,12 @@ /// limitations under the License. /// -import {NgModule} from '@angular/core'; -import {CommonModule, DatePipe} from '@angular/common'; -import {FooterComponent} from './components/footer.component'; -import {LogoComponent} from './components/logo.component'; -import {TbSnackBarComponent, ToastDirective} from './components/toast.directive'; -import {BreadcrumbComponent} from '@app/shared/components/breadcrumb.component'; +import { NgModule } from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { FooterComponent } from './components/footer.component'; +import { LogoComponent } from './components/logo.component'; +import { TbSnackBarComponent, ToastDirective } from './components/toast.directive'; +import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component'; import { MatAutocompleteModule, @@ -51,43 +51,49 @@ import { MatToolbarModule, MatTooltipModule } from '@angular/material'; -import {MatDatetimepickerModule, MatNativeDatetimeModule} from '@mat-datetimepicker/core'; -import {GridsterModule} from 'angular-gridster2'; -import {FlexLayoutModule} from '@angular/flex-layout'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {RouterModule} from '@angular/router'; -import {ShareModule as ShareButtonsModule} from '@ngx-share/core'; -import {UserMenuComponent} from '@shared/components/user-menu.component'; -import {NospacePipe} from './pipe/nospace.pipe'; -import {TranslateModule} from '@ngx-translate/core'; -import {TbCheckboxComponent} from '@shared/components/tb-checkbox.component'; -import {HelpComponent} from '@shared/components/help.component'; -import {TbAnchorComponent} from '@shared/components/tb-anchor.component'; -import {MillisecondsToTimeStringPipe} from '@shared/pipe/milliseconds-to-time-string.pipe'; -import {TimewindowComponent} from '@shared/components/time/timewindow.component'; -import {OverlayModule} from '@angular/cdk/overlay'; -import {TimewindowPanelComponent} from '@shared/components/time/timewindow-panel.component'; -import {TimeintervalComponent} from '@shared/components/time/timeinterval.component'; -import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component'; -import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe'; -import {ClipboardModule} from 'ngx-clipboard'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; +import { GridsterModule } from 'angular-gridster2'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { ShareModule as ShareButtonsModule } from '@ngx-share/core'; +import { HotkeyModule } from 'angular2-hotkeys'; +import { UserMenuComponent } from '@shared/components/user-menu.component'; +import { NospacePipe } from './pipe/nospace.pipe'; +import { TranslateModule } from '@ngx-translate/core'; +import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; +import { HelpComponent } from '@shared/components/help.component'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { TimewindowComponent } from '@shared/components/time/timewindow.component'; +import { OverlayModule } from '@angular/cdk/overlay'; +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; +import { ClipboardModule } from 'ngx-clipboard'; import { ValueInputComponent } from '@shared/components/value-input.component'; -import {FullscreenDirective} from '@shared/components/fullscreen.directive'; -import {HighlightPipe} from '@shared/pipe/highlight.pipe'; -import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; -import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component'; -import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component'; -import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component'; -import {EntityListComponent} from '@shared/components/entity/entity-list.component'; -import {EntityTypeSelectComponent} from './components/entity/entity-type-select.component'; -import {EntitySelectComponent} from './components/entity/entity-select.component'; -import {DatetimeComponent} from '@shared/components/time/datetime.component'; -import {EntityKeysListComponent} from './components/entity/entity-keys-list.component'; -import {SocialSharePanelComponent} from './components/socialshare-panel.component'; +import { FullscreenDirective } from '@shared/components/fullscreen.directive'; +import { HighlightPipe } from '@shared/pipe/highlight.pipe'; +import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { EntitySubTypeAutocompleteComponent } from '@shared/components/entity/entity-subtype-autocomplete.component'; +import { EntitySubTypeSelectComponent } from './components/entity/entity-subtype-select.component'; +import { EntityAutocompleteComponent } from './components/entity/entity-autocomplete.component'; +import { EntityListComponent } from '@shared/components/entity/entity-list.component'; +import { EntityTypeSelectComponent } from './components/entity/entity-type-select.component'; +import { EntitySelectComponent } from './components/entity/entity-select.component'; +import { DatetimeComponent } from '@shared/components/time/datetime.component'; +import { EntityKeysListComponent } from './components/entity/entity-keys-list.component'; +import { SocialSharePanelComponent } from './components/socialshare-panel.component'; import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; import { EntityListSelectComponent } from './components/entity/entity-list-select.component'; import { JsonObjectEditComponent } from './components/json-object-edit.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({ providers: [ @@ -100,6 +106,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons TbSnackBarComponent, TbAnchorComponent, TimewindowPanelComponent, + DashboardSelectPanelComponent, + MatSpinner ], declarations: [ FooterComponent, @@ -107,6 +115,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons FooterFabButtonsComponent, ToastDirective, FullscreenDirective, + CircularProgressDirective, TbAnchorComponent, HelpComponent, TbCheckboxComponent, @@ -116,6 +125,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons TimewindowComponent, TimewindowPanelComponent, TimeintervalComponent, + DashboardSelectComponent, + DashboardSelectPanelComponent, DatetimePeriodComponent, DatetimeComponent, ValueInputComponent, @@ -131,6 +142,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + FabTriggerDirective, + FabActionsDirective, + FabToolbarComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -176,7 +190,8 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons FormsModule, ReactiveFormsModule, OverlayModule, - ShareButtonsModule + ShareButtonsModule, + HotkeyModule ], exports: [ FooterComponent, @@ -184,6 +199,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons FooterFabButtonsComponent, ToastDirective, FullscreenDirective, + CircularProgressDirective, TbAnchorComponent, HelpComponent, TbCheckboxComponent, @@ -192,6 +208,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons TimewindowComponent, TimewindowPanelComponent, TimeintervalComponent, + DashboardSelectComponent, DatetimePeriodComponent, DatetimeComponent, DashboardAutocompleteComponent, @@ -206,6 +223,9 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + FabTriggerDirective, + FabActionsDirective, + FabToolbarComponent, ValueInputComponent, MatButtonModule, MatCheckboxModule, @@ -244,6 +264,7 @@ import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons ReactiveFormsModule, OverlayModule, ShareButtonsModule, + HotkeyModule, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, diff --git a/ui-ngx/src/assets/split.js/grips/horizontal.png b/ui-ngx/src/assets/split.js/grips/horizontal.png new file mode 100644 index 0000000000..cbe7da77dd Binary files /dev/null and b/ui-ngx/src/assets/split.js/grips/horizontal.png differ diff --git a/ui-ngx/src/assets/split.js/grips/vertical.png b/ui-ngx/src/assets/split.js/grips/vertical.png new file mode 100644 index 0000000000..0ac8fa1e03 Binary files /dev/null and b/ui-ngx/src/assets/split.js/grips/vertical.png differ diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index e99c4270ee..d3583f98e7 100644 --- a/ui-ngx/src/styles.scss +++ b/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 { button { margin: 6px 8px; @@ -293,7 +312,7 @@ pre.tb-highlight { } .tb-fullscreen-parent { - background: #fff; + background: #eee; } mat-label { diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index ed82d0f56b..eb7ce76033 100644 --- a/ui-ngx/src/theme.scss +++ b/ui-ngx/src/theme.scss @@ -166,9 +166,52 @@ $tb-dark-theme: get-tb-dark-theme( $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 { @include angular-material-theme($tb-theme); @include mat-datetimepicker-theme($tb-theme); + @include tb-components-theme($tb-theme); } .tb-dark { @@ -388,11 +431,13 @@ $tb-dark-theme: get-tb-dark-theme( width: 32px; height: 32px; line-height: 32px; + padding: 0 !important; } &.tb-mat-96 { width: 96px; height: 96px; line-height: 96px; + padding: 0 !important; } } @@ -549,6 +594,27 @@ $tb-dark-theme: get-tb-dark-theme( 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 { position: relative; display: flex; diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index 44125beebb..33398f7584 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/ui-ngx/src/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", - "types": ["node", "jquery", "flot", "tinycolor2"] + "types": ["node", "jquery", "flot", "tinycolor2", "js-beautify"] }, "exclude": [ "test.ts", diff --git a/ui-ngx/src/typings/split.js.typings.d.ts b/ui-ngx/src/typings/split.js.typings.d.ts new file mode 100644 index 0000000000..5c999f3126 --- /dev/null +++ b/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; diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index 02a68043b0..d1e3fa083a 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -14,7 +14,8 @@ "typeRoots": [ "node_modules/@types", "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": { "@app/*": ["src/app/*"],