Commit 9ec843cbfacc8fcfef106eaff278caa78ba1e2b4

Authored by Igor Kulikov
1 parent 2e7070a9

Widget component initial implementation

Showing 30 changed files with 3188 additions and 169 deletions
... ... @@ -30,6 +30,8 @@
30 30 "src/styles.scss"
31 31 ],
32 32 "scripts": [
  33 + "node_modules/javascript-detect-element-resize/detect-element-resize.js",
  34 + "node_modules/jquery/dist/jquery.min.js",
33 35 "node_modules/ace-builds/src-min/ace.js",
34 36 "node_modules/ace-builds/src-min/ext-language_tools.js",
35 37 "node_modules/ace-builds/src-min/ext-searchbox.js",
... ... @@ -71,10 +73,10 @@
71 73 "sourceMap": false,
72 74 "extractCss": true,
73 75 "namedChunks": false,
74   - "aot": true,
  76 + "aot": false,
75 77 "extractLicenses": true,
76 78 "vendorChunk": false,
77   - "buildOptimizer": true,
  79 + "buildOptimizer": false,
78 80 "budgets": [
79 81 {
80 82 "type": "initial",
... ...
... ... @@ -1993,8 +1993,7 @@
1993 1993 "base64-js": {
1994 1994 "version": "1.3.1",
1995 1995 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
1996   - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
1997   - "dev": true
  1996 + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
1998 1997 },
1999 1998 "base64id": {
2000 1999 "version": "1.0.0",
... ... @@ -6336,6 +6335,16 @@
6336 6335 "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
6337 6336 "dev": true
6338 6337 },
  6338 + "javascript-detect-element-resize": {
  6339 + "version": "0.5.3",
  6340 + "resolved": "https://registry.npmjs.org/javascript-detect-element-resize/-/javascript-detect-element-resize-0.5.3.tgz",
  6341 + "integrity": "sha1-GnHNUd/lZZB/KZAS/nOilBBAJd4="
  6342 + },
  6343 + "jquery": {
  6344 + "version": "3.4.1",
  6345 + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
  6346 + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
  6347 + },
6339 6348 "js-tokens": {
6340 6349 "version": "3.0.2",
6341 6350 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
... ...
... ... @@ -33,11 +33,14 @@
33 33 "@ngx-translate/http-loader": "^4.0.0",
34 34 "ace-builds": "^1.4.5",
35 35 "angular-gridster2": "^8.1.0",
  36 + "base64-js": "^1.3.1",
36 37 "compass-sass-mixins": "^0.12.7",
37 38 "core-js": "^3.1.4",
38 39 "deep-equal": "^1.0.1",
39 40 "font-awesome": "^4.7.0",
40 41 "hammerjs": "^2.0.8",
  42 + "javascript-detect-element-resize": "^0.5.3",
  43 + "jquery": "^3.4.1",
41 44 "material-design-icons": "^3.0.1",
42 45 "messageformat": "^2.3.0",
43 46 "ngx-clipboard": "^12.2.0",
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Observable } from 'rxjs';
  18 +import { EntityId } from '@app/shared/models/id/entity-id';
  19 +import { WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
  20 +import { TimeService } from '../services/time.service';
  21 +import { DeviceService } from '../http/device.service';
  22 +import { AlarmService } from '../http/alarm.service';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +
  25 +export interface TimewindowFunctions {
  26 + onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void;
  27 + onResetTimewindow: () => void;
  28 +}
  29 +
  30 +export interface WidgetSubscriptionApi {
  31 + createSubscription: (options: WidgetSubscriptionOptions, subscribe: boolean) => Observable<IWidgetSubscription>;
  32 + createSubscriptionFromInfo: (type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>,
  33 + options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean)
  34 + => Observable<IWidgetSubscription>;
  35 + removeSubscription: (id: string) => void;
  36 +}
  37 +
  38 +export interface RpcApi {
  39 + sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
  40 + sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
  41 +}
  42 +
  43 +export interface IWidgetUtils {
  44 + formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
  45 +}
  46 +
  47 +export interface WidgetActionsApi {
  48 + actionDescriptorsBySourceId: {[sourceId: string]: Array<WidgetActionDescriptor>};
  49 + getActionDescriptors: (actionSourceId: string) => Array<WidgetActionDescriptor>;
  50 + handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor,
  51 + entityId?: EntityId, entityName?: string, additionalParams?: any) => void;
  52 + elementClick: ($event: Event) => void;
  53 +}
  54 +
  55 +export interface IAliasController {
  56 + [key: string]: any | null;
  57 + // TODO:
  58 +}
  59 +
  60 +export interface StateObject {
  61 + id?: string;
  62 + params?: StateParams;
  63 +}
  64 +
  65 +export interface StateParams {
  66 + entityName?: string;
  67 + targetEntityParamName?: string;
  68 + entityId?: EntityId;
  69 + [key: string]: any | null;
  70 +}
  71 +
  72 +export interface IStateController {
  73 + getStateParams: () => StateParams;
  74 + openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void;
  75 + updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void;
  76 + // TODO:
  77 +}
  78 +
  79 +export interface EntityInfo {
  80 + entityId: EntityId;
  81 + entityName: string;
  82 +}
  83 +
  84 +export interface SubscriptionInfo {
  85 + [key: string]: any;
  86 + // TODO:
  87 +}
  88 +
  89 +export interface WidgetSubscriptionContext {
  90 + timeService: TimeService;
  91 + deviceService: DeviceService;
  92 + alarmService: AlarmService;
  93 + utils: UtilsService;
  94 + widgetUtils: IWidgetUtils;
  95 + dashboardTimewindowApi: TimewindowFunctions;
  96 + getServerTimeDiff: Observable<number>;
  97 + aliasController: IAliasController;
  98 + [key: string]: any;
  99 + // TODO:
  100 +}
  101 +
  102 +export interface WidgetSubscriptionOptions {
  103 + [key: string]: any;
  104 + // TODO:
  105 +}
  106 +
  107 +export interface IWidgetSubscription {
  108 +
  109 + onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void;
  110 + onResetTimewindow: () => void;
  111 +
  112 + sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
  113 + sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>;
  114 +
  115 + clearRpcError: () => void;
  116 +
  117 + getFirstEntityInfo: () => EntityInfo;
  118 +
  119 + destroy(): void;
  120 +
  121 + [key: string]: any;
  122 + // TODO:
  123 +}
... ...
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +/* eslint-disable */
  17 +
  18 +/* jshint unused:false */
  19 +/* global base64_decode, CSSWizardView, window, console, jQuery */
  20 +var fi = function () {
  21 +
  22 + this.cssImportStatements = [];
  23 + this.cssKeyframeStatements = [];
  24 +
  25 + this.cssRegex = new RegExp('([\\s\\S]*?){([\\s\\S]*?)}', 'gi');
  26 + this.cssMediaQueryRegex = '((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})';
  27 + this.cssKeyframeRegex = '((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})';
  28 + this.combinedCSSRegex = '((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; //to match css & media queries together
  29 + this.cssCommentsRegex = '(\\/\\*[\\s\\S]*?\\*\\/)';
  30 + this.cssImportStatementRegex = new RegExp('@import .*?;', 'gi');
  31 +};
  32 +
  33 +/*
  34 + Strip outs css comments and returns cleaned css string
  35 +
  36 + @param css, the original css string to be stipped out of comments
  37 +
  38 + @return cleanedCSS contains no css comments
  39 + */
  40 +fi.prototype.stripComments = function (cssString) {
  41 + var regex = new RegExp(this.cssCommentsRegex, 'gi');
  42 +
  43 + return cssString.replace(regex, '');
  44 +};
  45 +
  46 +/*
  47 + Parses given css string, and returns css object
  48 + keys as selectors and values are css rules
  49 + eliminates all css comments before parsing
  50 +
  51 + @param source css string to be parsed
  52 +
  53 + @return object css
  54 + */
  55 +fi.prototype.parseCSS = function (source) {
  56 +
  57 + if (source === undefined) {
  58 + return [];
  59 + }
  60 +
  61 + var css = [];
  62 + //strip out comments
  63 + //source = this.stripComments(source);
  64 +
  65 + //get import statements
  66 +
  67 + while (true) {
  68 + var imports = this.cssImportStatementRegex.exec(source);
  69 + if (imports !== null) {
  70 + this.cssImportStatements.push(imports[0]);
  71 + css.push({
  72 + selector: '@imports',
  73 + type: 'imports',
  74 + styles: imports[0]
  75 + });
  76 + } else {
  77 + break;
  78 + }
  79 + }
  80 + source = source.replace(this.cssImportStatementRegex, '');
  81 + //get keyframe statements
  82 + var keyframesRegex = new RegExp(this.cssKeyframeRegex, 'gi');
  83 + var arr;
  84 + while (true) {
  85 + arr = keyframesRegex.exec(source);
  86 + if (arr === null) {
  87 + break;
  88 + }
  89 + css.push({
  90 + selector: '@keyframes',
  91 + type: 'keyframes',
  92 + styles: arr[0]
  93 + });
  94 + }
  95 + source = source.replace(keyframesRegex, '');
  96 +
  97 + //unified regex
  98 + var unified = new RegExp(this.combinedCSSRegex, 'gi');
  99 +
  100 + while (true) {
  101 + arr = unified.exec(source);
  102 + if (arr === null) {
  103 + break;
  104 + }
  105 + var selector = '';
  106 + if (arr[2] === undefined) {
  107 + selector = arr[5].split('\r\n').join('\n').trim();
  108 + } else {
  109 + selector = arr[2].split('\r\n').join('\n').trim();
  110 + }
  111 +
  112 + /*
  113 + fetch comments and associate it with current selector
  114 + */
  115 + var commentsRegex = new RegExp(this.cssCommentsRegex, 'gi');
  116 + var comments = commentsRegex.exec(selector);
  117 + if (comments !== null) {
  118 + selector = selector.replace(commentsRegex, '').trim();
  119 + }
  120 +
  121 + //determine the type
  122 + if (selector.indexOf('@media') !== -1) {
  123 + //we have a media query
  124 + var cssObject = {
  125 + selector: selector,
  126 + type: 'media',
  127 + subStyles: this.parseCSS(arr[3] + '\n}') //recursively parse media query inner css
  128 + };
  129 + if (comments !== null) {
  130 + cssObject.comments = comments[0];
  131 + }
  132 + css.push(cssObject);
  133 + } else {
  134 + //we have standart css
  135 + var rules = this.parseRules(arr[6]);
  136 + var style = {
  137 + selector: selector,
  138 + rules: rules
  139 + };
  140 + if (selector === '@font-face') {
  141 + style.type = 'font-face';
  142 + }
  143 + if (comments !== null) {
  144 + style.comments = comments[0];
  145 + }
  146 + css.push(style);
  147 + }
  148 + }
  149 +
  150 + return css;
  151 +};
  152 +
  153 +/*
  154 + parses given string containing css directives
  155 + and returns an array of objects containing ruleName:ruleValue pairs
  156 +
  157 + @param rules, css directive string example
  158 + \n\ncolor:white;\n font-size:18px;\n
  159 + */
  160 +fi.prototype.parseRules = function (rules) {
  161 + //convert all windows style line endings to unix style line endings
  162 + rules = rules.split('\r\n').join('\n');
  163 + var ret = [];
  164 +
  165 + // Split all rules but keep semicolon for base64 url data
  166 + rules = rules.split(/;(?![^\(]*\))/);
  167 +
  168 + //proccess rules line by line
  169 + for (var i = 0; i < rules.length; i++) {
  170 + var line = rules[i];
  171 +
  172 + //determine if line is a valid css directive, ie color:white;
  173 + line = line.trim();
  174 + if (line.indexOf(':') !== -1) {
  175 + //line contains :
  176 + line = line.split(':');
  177 + var cssDirective = line[0].trim();
  178 + var cssValue = line.slice(1).join(':').trim();
  179 +
  180 + //more checks
  181 + if (cssDirective.length < 1 || cssValue.length < 1) {
  182 + continue; //there is no css directive or value that is of length 1 or 0
  183 + // PLAIN WRONG WHAT ABOUT margin:0; ?
  184 + }
  185 +
  186 + //push rule
  187 + ret.push({
  188 + directive: cssDirective,
  189 + value: cssValue
  190 + });
  191 + } else {
  192 + //if there is no ':', but what if it was mis splitted value which starts with base64
  193 + if (line.trim().substr(0, 7) == 'base64,') { //hack :)
  194 + ret[ret.length - 1].value += line.trim();
  195 + } else {
  196 + //add rule, even if it is defective
  197 + if (line.length > 0) {
  198 + ret.push({
  199 + directive: '',
  200 + value: line,
  201 + defective: true
  202 + });
  203 + }
  204 + }
  205 + }
  206 + }
  207 +
  208 + return ret; //we are done!
  209 +};
  210 +/*
  211 + just returns the rule having given directive
  212 + if not found returns false;
  213 + */
  214 +fi.prototype.findCorrespondingRule = function (rules, directive, value) {
  215 + if (value === undefined) {
  216 + value = false;
  217 + }
  218 + var ret = false;
  219 + for (var i = 0; i < rules.length; i++) {
  220 + if (rules[i].directive == directive) {
  221 + ret = rules[i];
  222 + if (value === rules[i].value) {
  223 + break;
  224 + }
  225 + }
  226 + }
  227 + return ret;
  228 +};
  229 +
  230 +/*
  231 + Finds styles that have given selector, compress them,
  232 + and returns them
  233 + */
  234 +fi.prototype.findBySelector = function (cssObjectArray, selector, contains) {
  235 + if (contains === undefined) {
  236 + contains = false;
  237 + }
  238 +
  239 + var found = [];
  240 + for (var i = 0; i < cssObjectArray.length; i++) {
  241 + if (contains === false) {
  242 + if (cssObjectArray[i].selector === selector) {
  243 + found.push(cssObjectArray[i]);
  244 + }
  245 + } else {
  246 + if (cssObjectArray[i].selector.indexOf(selector) !== -1) {
  247 + found.push(cssObjectArray[i]);
  248 + }
  249 + }
  250 +
  251 + }
  252 + if (found.length < 2) {
  253 + return found;
  254 + } else {
  255 + var base = found[0];
  256 + for (i = 1; i < found.length; i++) {
  257 + this.intelligentCSSPush([base], found[i]);
  258 + }
  259 + return [base]; //we are done!! all properties merged into base!
  260 + }
  261 +};
  262 +
  263 +/*
  264 + deletes cssObjects having given selector, and returns new array
  265 + */
  266 +fi.prototype.deleteBySelector = function (cssObjectArray, selector) {
  267 + var ret = [];
  268 + for (var i = 0; i < cssObjectArray.length; i++) {
  269 + if (cssObjectArray[i].selector !== selector) {
  270 + ret.push(cssObjectArray[i]);
  271 + }
  272 + }
  273 + return ret;
  274 +};
  275 +
  276 +/*
  277 + Compresses given cssObjectArray and tries to minimize
  278 + selector redundence.
  279 + */
  280 +fi.prototype.compressCSS = function (cssObjectArray) {
  281 + var compressed = [];
  282 + var done = {};
  283 + for (var i = 0; i < cssObjectArray.length; i++) {
  284 + var obj = cssObjectArray[i];
  285 + if (done[obj.selector] === true) {
  286 + continue;
  287 + }
  288 +
  289 + var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed
  290 + if (found.length !== 0) {
  291 + compressed.push(found[0]);
  292 + done[obj.selector] = true;
  293 + }
  294 + }
  295 + return compressed;
  296 +};
  297 +
  298 +/*
  299 + Received 2 css objects with following structure
  300 + {
  301 + rules : [{directive:"", value:""}, {directive:"", value:""}, ...]
  302 + selector : "SOMESELECTOR"
  303 + }
  304 +
  305 + returns the changed(new,removed,updated) values on css1 parameter, on same structure
  306 +
  307 + if two css objects are the same, then returns false
  308 +
  309 + if a css directive exists in css1 and css2, and its value is different, it is included in diff
  310 + if a css directive exists in css1 and not css2, it is then included in diff
  311 + if a css directive exists in css2 but not css1, then it is deleted in css1, it would be included in diff but will be marked as type='DELETED'
  312 +
  313 + @object css1 css object
  314 + @object css2 css object
  315 +
  316 + @return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js
  317 + */
  318 +fi.prototype.cssDiff = function (css1, css2) {
  319 + if (css1.selector !== css2.selector) {
  320 + return false;
  321 + }
  322 +
  323 + //if one of them is media query return false, because diff function can not operate on media queries
  324 + if ((css1.type === 'media' || css2.type === 'media')) {
  325 + return false;
  326 + }
  327 +
  328 + var diff = {
  329 + selector: css1.selector,
  330 + rules: []
  331 + };
  332 + var rule1, rule2;
  333 + for (var i = 0; i < css1.rules.length; i++) {
  334 + rule1 = css1.rules[i];
  335 + //find rule2 which has the same directive as rule1
  336 + rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value);
  337 + if (rule2 === false) {
  338 + //rule1 is a new rule in css1
  339 + diff.rules.push(rule1);
  340 + } else {
  341 + //rule2 was found only push if its value is different too
  342 + if (rule1.value !== rule2.value) {
  343 + diff.rules.push(rule1);
  344 + }
  345 + }
  346 + }
  347 +
  348 + //now for rules exists in css2 but not in css1, which means deleted rules
  349 + for (var ii = 0; ii < css2.rules.length; ii++) {
  350 + rule2 = css2.rules[ii];
  351 + //find rule2 which has the same directive as rule1
  352 + rule1 = this.findCorrespondingRule(css1.rules, rule2.directive);
  353 + if (rule1 === false) {
  354 + //rule1 is a new rule
  355 + rule2.type = 'DELETED'; //mark it as a deleted rule, so that other merge operations could be true
  356 + diff.rules.push(rule2);
  357 + }
  358 + }
  359 +
  360 +
  361 + if (diff.rules.length === 0) {
  362 + return false;
  363 + }
  364 + return diff;
  365 +};
  366 +
  367 +/*
  368 + Merges 2 different css objects together
  369 + using intelligentCSSPush,
  370 +
  371 + @param cssObjectArray, target css object array
  372 + @param newArray, source array that will be pushed into cssObjectArray parameter
  373 + @param reverse, [optional], if given true, first parameter will be traversed on reversed order
  374 + effectively giving priority to the styles in newArray
  375 + */
  376 +fi.prototype.intelligentMerge = function (cssObjectArray, newArray, reverse) {
  377 + if (reverse === undefined) {
  378 + reverse = false;
  379 + }
  380 +
  381 +
  382 + for (var i = 0; i < newArray.length; i++) {
  383 + this.intelligentCSSPush(cssObjectArray, newArray[i], reverse);
  384 + }
  385 + for (i = 0; i < cssObjectArray.length; i++) {
  386 + var cobj = cssObjectArray[i];
  387 + if (cobj.type === 'media' || (cobj.type === 'keyframes')) {
  388 + continue;
  389 + }
  390 + cobj.rules = this.compactRules(cobj.rules);
  391 + }
  392 +};
  393 +
  394 +/*
  395 + inserts new css objects into a bigger css object
  396 + with same selectors groupped together
  397 +
  398 + @param cssObjectArray, array of bigger css object to be pushed into
  399 + @param minimalObject, single css object
  400 + @param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed
  401 + resulting more priority in minimalObject's styles
  402 + */
  403 +fi.prototype.intelligentCSSPush = function (cssObjectArray, minimalObject, reverse) {
  404 + var pushSelector = minimalObject.selector;
  405 + //find correct selector if not found just push minimalObject into cssObject
  406 + var cssObject = false;
  407 +
  408 + if (reverse === undefined) {
  409 + reverse = false;
  410 + }
  411 +
  412 + if (reverse === false) {
  413 + for (var i = 0; i < cssObjectArray.length; i++) {
  414 + if (cssObjectArray[i].selector === minimalObject.selector) {
  415 + cssObject = cssObjectArray[i];
  416 + break;
  417 + }
  418 + }
  419 + } else {
  420 + for (var j = cssObjectArray.length - 1; j > -1; j--) {
  421 + if (cssObjectArray[j].selector === minimalObject.selector) {
  422 + cssObject = cssObjectArray[j];
  423 + break;
  424 + }
  425 + }
  426 + }
  427 +
  428 + if (cssObject === false) {
  429 + cssObjectArray.push(minimalObject); //just push, because cssSelector is new
  430 + } else {
  431 + if (minimalObject.type !== 'media') {
  432 + for (var ii = 0; ii < minimalObject.rules.length; ii++) {
  433 + var rule = minimalObject.rules[ii];
  434 + //find rule inside cssObject
  435 + var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive);
  436 + if (oldRule === false) {
  437 + cssObject.rules.push(rule);
  438 + } else if (rule.type == 'DELETED') {
  439 + oldRule.type = 'DELETED';
  440 + } else {
  441 + //rule found just update value
  442 +
  443 + oldRule.value = rule.value;
  444 + }
  445 + }
  446 + } else {
  447 + cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too
  448 + }
  449 +
  450 + }
  451 +};
  452 +
  453 +/*
  454 + filter outs rule objects whose type param equal to DELETED
  455 +
  456 + @param rules, array of rules
  457 +
  458 + @returns rules array, compacted by deleting all unneccessary rules
  459 + */
  460 +fi.prototype.compactRules = function (rules) {
  461 + var newRules = [];
  462 + for (var i = 0; i < rules.length; i++) {
  463 + if (rules[i].type !== 'DELETED') {
  464 + newRules.push(rules[i]);
  465 + }
  466 + }
  467 + return newRules;
  468 +};
  469 +/*
  470 + computes string for ace editor using this.css or given cssBase optional parameter
  471 +
  472 + @param [optional] cssBase, if given computes cssString from cssObject array
  473 + */
  474 +fi.prototype.getCSSForEditor = function (cssBase, depth) {
  475 + if (depth === undefined) {
  476 + depth = 0;
  477 + }
  478 + var ret = '';
  479 + if (cssBase === undefined) {
  480 + cssBase = this.css;
  481 + }
  482 + //append imports
  483 + for (var i = 0; i < cssBase.length; i++) {
  484 + if (cssBase[i].type == 'imports') {
  485 + ret += cssBase[i].styles + '\n\n';
  486 + }
  487 + }
  488 + for (i = 0; i < cssBase.length; i++) {
  489 + var tmp = cssBase[i];
  490 + if (tmp.selector === undefined) { //temporarily omit media queries
  491 + continue;
  492 + }
  493 + var comments = "";
  494 + if (tmp.comments !== undefined) {
  495 + comments = tmp.comments + '\n';
  496 + }
  497 +
  498 + if (tmp.type == 'media') { //also put media queries to output
  499 + ret += comments + tmp.selector + '{\n';
  500 + ret += this.getCSSForEditor(tmp.subStyles, depth + 1);
  501 + ret += '}\n\n';
  502 + } else if (tmp.type !== 'keyframes' && tmp.type !== 'imports') {
  503 + ret += this.getSpaces(depth) + comments + tmp.selector + ' {\n';
  504 + ret += this.getCSSOfRules(tmp.rules, depth + 1);
  505 + ret += this.getSpaces(depth) + '}\n\n';
  506 + }
  507 + }
  508 +
  509 + //append keyFrames
  510 + for (i = 0; i < cssBase.length; i++) {
  511 + if (cssBase[i].type == 'keyframes') {
  512 + ret += cssBase[i].styles + '\n\n';
  513 + }
  514 + }
  515 +
  516 + return ret;
  517 +};
  518 +
  519 +fi.prototype.getImports = function (cssObjectArray) {
  520 + var imps = [];
  521 + for (var i = 0; i < cssObjectArray.length; i++) {
  522 + if (cssObjectArray[i].type == 'imports') {
  523 + imps.push(cssObjectArray[i].styles);
  524 + }
  525 + }
  526 + return imps;
  527 +};
  528 +/*
  529 + given rules array, returns visually formatted css string
  530 + to be used inside editor
  531 + */
  532 +fi.prototype.getCSSOfRules = function (rules, depth) {
  533 + var ret = '';
  534 + for (var i = 0; i < rules.length; i++) {
  535 + if (rules[i] === undefined) {
  536 + continue;
  537 + }
  538 + if (rules[i].defective === undefined) {
  539 + ret += this.getSpaces(depth) + rules[i].directive + ' : ' + rules[i].value + ';\n';
  540 + } else {
  541 + ret += this.getSpaces(depth) + rules[i].value + ';\n';
  542 + }
  543 +
  544 + }
  545 + return ret || '\n';
  546 +};
  547 +
  548 +/*
  549 + A very simple helper function returns number of spaces appended in a single string,
  550 + the number depends input parameter, namely input*2
  551 + */
  552 +fi.prototype.getSpaces = function (num) {
  553 + var ret = '';
  554 + for (var i = 0; i < num * 4; i++) {
  555 + ret += ' ';
  556 + }
  557 + return ret;
  558 +};
  559 +
  560 +/*
  561 + Given css string or objectArray, parses it and then for every selector,
  562 + prepends this.cssPreviewNamespace to prevent css collision issues
  563 +
  564 + @returns css string in which this.cssPreviewNamespace prepended
  565 + */
  566 +fi.prototype.applyNamespacing = function (css, forcedNamespace) {
  567 + var cssObjectArray = css;
  568 + var namespaceClass = '.' + this.cssPreviewNamespace;
  569 + if (forcedNamespace !== undefined) {
  570 + namespaceClass = forcedNamespace;
  571 + }
  572 +
  573 + if (typeof css === 'string') {
  574 + cssObjectArray = this.parseCSS(css);
  575 + }
  576 +
  577 + for (var i = 0; i < cssObjectArray.length; i++) {
  578 + var obj = cssObjectArray[i];
  579 +
  580 + //bypass namespacing for @font-face @keyframes @import
  581 + if (obj.selector.indexOf('@font-face') > -1 || obj.selector.indexOf('keyframes') > -1 || obj.selector.indexOf('@import') > -1 || obj.selector.indexOf('.form-all') > -1 || obj.selector.indexOf('#stage') > -1) {
  582 + continue;
  583 + }
  584 +
  585 + if (obj.type !== 'media') {
  586 + var selector = obj.selector.split(',');
  587 + var newSelector = [];
  588 + for (var j = 0; j < selector.length; j++) {
  589 + if (selector[j].indexOf('.supernova') === -1) { //do not apply namespacing to selectors including supernova
  590 + newSelector.push(namespaceClass + ' ' + selector[j]);
  591 + } else {
  592 + newSelector.push(selector[j]);
  593 + }
  594 + }
  595 + obj.selector = newSelector.join(',');
  596 + } else {
  597 + obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well
  598 + }
  599 + }
  600 +
  601 + return cssObjectArray;
  602 +};
  603 +
  604 +/*
  605 + given css string or object array, clears possible namespacing from
  606 + all of the selectors inside the css
  607 + */
  608 +fi.prototype.clearNamespacing = function (css, returnObj) {
  609 + if (returnObj === undefined) {
  610 + returnObj = false;
  611 + }
  612 + var cssObjectArray = css;
  613 + var namespaceClass = '.' + this.cssPreviewNamespace;
  614 + if (typeof css === 'string') {
  615 + cssObjectArray = this.parseCSS(css);
  616 + }
  617 +
  618 + for (var i = 0; i < cssObjectArray.length; i++) {
  619 + var obj = cssObjectArray[i];
  620 +
  621 + if (obj.type !== 'media') {
  622 + var selector = obj.selector.split(',');
  623 + var newSelector = [];
  624 + for (var j = 0; j < selector.length; j++) {
  625 + newSelector.push(selector[j].split(namespaceClass + ' ').join(''));
  626 + }
  627 + obj.selector = newSelector.join(',');
  628 + } else {
  629 + obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well
  630 + }
  631 + }
  632 + if (returnObj === false) {
  633 + return this.getCSSForEditor(cssObjectArray);
  634 + } else {
  635 + return cssObjectArray;
  636 + }
  637 +
  638 +};
  639 +
  640 +/*
  641 + creates a new style tag (also destroys the previous one)
  642 + and injects given css string into that css tag
  643 + */
  644 +fi.prototype.createStyleElement = function (id, css, format) {
  645 + if (format === undefined) {
  646 + format = false;
  647 + }
  648 +
  649 + if (this.testMode === false && format !== 'nonamespace') {
  650 + //apply namespacing classes
  651 + css = this.applyNamespacing(css);
  652 + }
  653 +
  654 + if (typeof css != 'string') {
  655 + css = this.getCSSForEditor(css);
  656 + }
  657 + //apply formatting for css
  658 + if (format === true) {
  659 + css = this.getCSSForEditor(this.parseCSS(css));
  660 + }
  661 +
  662 + if (this.testMode !== false) {
  663 + return this.testMode('create style #' + id, css); //if test mode, just pass result to callback
  664 + }
  665 +
  666 + var __el = document.getElementById(id);
  667 + if (__el) {
  668 + __el.parentNode.removeChild(__el);
  669 + }
  670 +
  671 + var head = document.head || document.getElementsByTagName('head')[0],
  672 + style = document.createElement('style');
  673 +
  674 + style.id = id;
  675 + style.type = 'text/css';
  676 +
  677 + head.appendChild(style);
  678 +
  679 + if (style.styleSheet && !style.sheet) {
  680 + style.styleSheet.cssText = css;
  681 + } else {
  682 + style.appendChild(document.createTextNode(css));
  683 + }
  684 +};
  685 +
  686 +export default fi;
  687 +
  688 +/* eslint-enable */
... ...
... ... @@ -16,21 +16,46 @@
16 16
17 17 import {Injectable} from '@angular/core';
18 18 import {defaultHttpOptions} from './http-utils';
19   -import {Observable} from 'rxjs/index';
  19 +import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } from 'rxjs/index';
20 20 import {HttpClient} from '@angular/common/http';
21 21 import {PageLink} from '@shared/models/page/page-link';
22 22 import {PageData} from '@shared/models/page/page-data';
23 23 import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
24   -import { WidgetType } from '@shared/models/widget.models';
  24 +import {
  25 + WidgetControllerDescriptor,
  26 + WidgetInfo,
  27 + WidgetType,
  28 + WidgetTypeInstance,
  29 + widgetActionSources,
  30 + MissingWidgetType, toWidgetInfo, ErrorWidgetType
  31 +} from '@shared/models/widget.models';
  32 +import { UtilsService } from '@core/services/utils.service';
  33 +import { isFunction, isUndefined } from '@core/utils';
  34 +import { TranslateService } from '@ngx-translate/core';
  35 +import { AuthPayload } from '@core/auth/auth.models';
  36 +import cssjs from '@core/css/css';
  37 +import { ResourcesService } from '../services/resources.service';
  38 +import { catchError, map, switchMap } from 'rxjs/operators';
25 39
26 40 @Injectable({
27 41 providedIn: 'root'
28 42 })
29 43 export class WidgetService {
30 44
  45 + private cssParser = new cssjs();
  46 +
  47 + private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>();
  48 +
  49 + private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>();
  50 +
31 51 constructor(
32   - private http: HttpClient
33   - ) { }
  52 + private http: HttpClient,
  53 + private utils: UtilsService,
  54 + private resources: ResourcesService,
  55 + private translate: TranslateService
  56 + ) {
  57 + this.cssParser.testMode = false;
  58 + }
34 59
35 60 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
36 61 ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> {
... ... @@ -58,4 +83,266 @@ export class WidgetService {
58 83 defaultHttpOptions(ignoreLoading, ignoreErrors));
59 84 }
60 85
  86 + public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean,
  87 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
  88 + return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
  89 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  90 + }
  91 +
  92 + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
  93 + const widgetInfoSubject = new ReplaySubject<WidgetInfo>();
  94 + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
  95 + if (widgetInfo) {
  96 + widgetInfoSubject.next(widgetInfo);
  97 + widgetInfoSubject.complete();
  98 + } else {
  99 + if (this.utils.widgetEditMode) {
  100 + // TODO:
  101 + } else {
  102 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  103 + let fetchQueue = this.widgetsInfoFetchQueue.get(key);
  104 + if (fetchQueue) {
  105 + fetchQueue.push(widgetInfoSubject);
  106 + } else {
  107 + fetchQueue = new Array<Subject<WidgetInfo>>();
  108 + this.widgetsInfoFetchQueue.set(key, fetchQueue);
  109 + this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe(
  110 + (widgetType) => {
  111 + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject);
  112 + },
  113 + () => {
  114 + widgetInfoSubject.next(MissingWidgetType);
  115 + widgetInfoSubject.complete();
  116 + this.resolveWidgetsInfoFetchQueue(key, MissingWidgetType);
  117 + }
  118 + );
  119 + }
  120 + }
  121 + }
  122 + return widgetInfoSubject.asObservable();
  123 + }
  124 +
  125 + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) {
  126 + const widgetInfo = toWidgetInfo(widgetType);
  127 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem);
  128 + this.loadWidgetResources(widgetInfo, bundleAlias, isSystem).subscribe(
  129 + () => {
  130 + let widgetControllerDescriptor: WidgetControllerDescriptor = null;
  131 + try {
  132 + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key);
  133 + } catch (e) {
  134 + const details = this.utils.parseException(e);
  135 + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`;
  136 + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject);
  137 + }
  138 + if (widgetControllerDescriptor) {
  139 + if (widgetControllerDescriptor.settingsSchema) {
  140 + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
  141 + }
  142 + if (widgetControllerDescriptor.dataKeySettingsSchema) {
  143 + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema;
  144 + }
  145 + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters;
  146 + widgetInfo.actionSources = widgetControllerDescriptor.actionSources;
  147 + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction;
  148 + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
  149 + if (widgetInfoSubject) {
  150 + widgetInfoSubject.next(widgetInfo);
  151 + widgetInfoSubject.complete();
  152 + }
  153 + this.resolveWidgetsInfoFetchQueue(key, widgetInfo);
  154 + }
  155 + },
  156 + (errorMessages: string[]) => {
  157 + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject);
  158 + }
  159 + );
  160 + }
  161 +
  162 + private loadWidgetResources(widgetInfo: WidgetInfo, bundleAlias: string, isSystem: boolean): Observable<any> {
  163 + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
  164 + this.cssParser.cssPreviewNamespace = widgetNamespace;
  165 + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
  166 + const resourceTasks: Observable<string>[] = [];
  167 + if (widgetInfo.resources.length > 0) {
  168 + widgetInfo.resources.forEach((resource) => {
  169 + resourceTasks.push(
  170 + this.resources.loadResource(resource.url).pipe(
  171 + catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
  172 + )
  173 + );
  174 + });
  175 + return forkJoin(resourceTasks).pipe(
  176 + switchMap(msgs => {
  177 + let errors: string[];
  178 + if (msgs && msgs.length) {
  179 + errors = msgs.filter(msg => msg && msg.length > 0);
  180 + }
  181 + if (errors && errors.length) {
  182 + return throwError(errors);
  183 + } else {
  184 + return of(null);
  185 + }
  186 + }
  187 + ));
  188 + } else {
  189 + return of(null);
  190 + }
  191 + }
  192 +
  193 + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor {
  194 + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` +
  195 + ' var self = this;\n' +
  196 + ' self.ctx = ctx;\n\n'; /*+
  197 +
  198 + ' self.onInit = function() {\n\n' +
  199 +
  200 + ' }\n\n' +
  201 +
  202 + ' self.onDataUpdated = function() {\n\n' +
  203 +
  204 + ' }\n\n' +
  205 +
  206 + ' self.useCustomDatasources = function() {\n\n' +
  207 +
  208 + ' }\n\n' +
  209 +
  210 + ' self.typeParameters = function() {\n\n' +
  211 + return {
  212 + useCustomDatasources: false,
  213 + maxDatasources: -1, //unlimited
  214 + maxDataKeys: -1, //unlimited
  215 + dataKeysOptional: false,
  216 + stateData: false
  217 + };
  218 + ' }\n\n' +
  219 +
  220 + ' self.actionSources = function() {\n\n' +
  221 + return {
  222 + 'headerButton': {
  223 + name: 'Header button',
  224 + multiple: true
  225 + }
  226 + };
  227 + }\n\n' +
  228 + ' self.onResize = function() {\n\n' +
  229 +
  230 + ' }\n\n' +
  231 +
  232 + ' self.onEditModeChanged = function() {\n\n' +
  233 +
  234 + ' }\n\n' +
  235 +
  236 + ' self.onMobileModeChanged = function() {\n\n' +
  237 +
  238 + ' }\n\n' +
  239 +
  240 + ' self.getSettingsSchema = function() {\n\n' +
  241 +
  242 + ' }\n\n' +
  243 +
  244 + ' self.getDataKeySettingsSchema = function() {\n\n' +
  245 +
  246 + ' }\n\n' +
  247 +
  248 + ' self.onDestroy = function() {\n\n' +
  249 +
  250 + ' }\n\n' +
  251 + '}';*/
  252 +
  253 + widgetTypeFunctionBody += widgetInfo.controllerScript;
  254 + widgetTypeFunctionBody += '\n};\n';
  255 +
  256 + try {
  257 +
  258 + const widgetTypeFunction = new Function(widgetTypeFunctionBody);
  259 + const widgetType = widgetTypeFunction.apply(this);
  260 + const widgetTypeInstance: WidgetTypeInstance = new widgetType();
  261 + const result: WidgetControllerDescriptor = {
  262 + widgetTypeFunction: widgetType
  263 + };
  264 + if (isFunction(widgetTypeInstance.getSettingsSchema)) {
  265 + result.settingsSchema = widgetTypeInstance.getSettingsSchema();
  266 + }
  267 + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
  268 + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
  269 + }
  270 + if (isFunction(widgetTypeInstance.typeParameters)) {
  271 + result.typeParameters = widgetTypeInstance.typeParameters();
  272 + } else {
  273 + result.typeParameters = {};
  274 + }
  275 + if (isFunction(widgetTypeInstance.useCustomDatasources)) {
  276 + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
  277 + } else {
  278 + result.typeParameters.useCustomDatasources = false;
  279 + }
  280 + if (isUndefined(result.typeParameters.maxDatasources)) {
  281 + result.typeParameters.maxDatasources = -1;
  282 + }
  283 + if (isUndefined(result.typeParameters.maxDataKeys)) {
  284 + result.typeParameters.maxDataKeys = -1;
  285 + }
  286 + if (isUndefined(result.typeParameters.dataKeysOptional)) {
  287 + result.typeParameters.dataKeysOptional = false;
  288 + }
  289 + if (isUndefined(result.typeParameters.stateData)) {
  290 + result.typeParameters.stateData = false;
  291 + }
  292 + if (isFunction(widgetTypeInstance.actionSources)) {
  293 + result.actionSources = widgetTypeInstance.actionSources();
  294 + } else {
  295 + result.actionSources = {};
  296 + }
  297 + for (const actionSourceId of Object.keys(widgetActionSources)) {
  298 + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]};
  299 + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name);
  300 + }
  301 + return result;
  302 + } catch (e) {
  303 + this.utils.processWidgetException(e);
  304 + throw e;
  305 + }
  306 + }
  307 +
  308 + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) {
  309 + const widgetInfo = {...ErrorWidgetType};
  310 + errorMessages.forEach(error => {
  311 + widgetInfo.templateHtml += `<div class="tb-widget-error-msg">${error}</div>`;
  312 + });
  313 + widgetInfo.templateHtml += '</div>';
  314 + if (widgetInfoSubject) {
  315 + widgetInfoSubject.next(widgetInfo);
  316 + widgetInfoSubject.complete();
  317 + }
  318 + this.resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo);
  319 + }
  320 +
  321 + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo) {
  322 + const fetchQueue = this.widgetsInfoFetchQueue.get(key);
  323 + if (fetchQueue) {
  324 + fetchQueue.forEach(subject => {
  325 + subject.next(widgetInfo);
  326 + subject.complete();
  327 + });
  328 + this.widgetsInfoFetchQueue.delete(key);
  329 + }
  330 + }
  331 +
  332 + // Cache functions
  333 +
  334 + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string {
  335 + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`;
  336 + }
  337 +
  338 + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined {
  339 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  340 + return this.widgetsInfoInMemoryCache.get(key);
  341 + }
  342 +
  343 + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
  344 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  345 + this.widgetsInfoInMemoryCache.set(key, widgetInfo);
  346 + }
  347 +
61 348 }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable, Inject } from '@angular/core';
  18 +import { DOCUMENT } from '@angular/common';
  19 +import { ReplaySubject, Observable, throwError } from 'rxjs';
  20 +
  21 +@Injectable({
  22 + providedIn: 'root'
  23 +})
  24 +export class ResourcesService {
  25 +
  26 + private loadedResources: { [url: string]: ReplaySubject<any> } = {};
  27 +
  28 + private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
  29 +
  30 + constructor(@Inject(DOCUMENT) private readonly document: any) {}
  31 +
  32 + public loadResource(url: string): Observable<any> {
  33 + if (this.loadedResources[url]) {
  34 + return this.loadedResources[url].asObservable();
  35 + }
  36 +
  37 + let fileType;
  38 + const match = /[.](css|less|html|htm|js)?((\?|#).*)?$/.exec(url);
  39 + if (match !== null) {
  40 + fileType = match[1];
  41 + }
  42 + if (!fileType) {
  43 + return throwError(new Error(`Unable to detect file type from url: ${url}`));
  44 + } else if (fileType !== 'css' && fileType !== 'js') {
  45 + return throwError(new Error(`Unsupported file type: ${fileType}`));
  46 + }
  47 + return this.loadResourceByType(fileType, url);
  48 + }
  49 +
  50 + private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> {
  51 + const subject = new ReplaySubject();
  52 + this.loadedResources[url] = subject;
  53 + let el;
  54 + let loaded = false;
  55 + switch (type) {
  56 + case 'js':
  57 + el = this.document.createElement('script');
  58 + el.type = 'text/javascript';
  59 + el.async = true;
  60 + el.src = url;
  61 + break;
  62 + case 'css':
  63 + el = this.document.createElement('link');
  64 + el.type = 'text/css';
  65 + el.rel = 'stylesheet';
  66 + el.href = url;
  67 + break;
  68 + }
  69 + el.onload = el.onreadystatechange = (e) => {
  70 + if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; }
  71 + el.onload = el.onreadystatechange = null;
  72 + loaded = true;
  73 + this.loadedResources[url].next();
  74 + this.loadedResources[url].complete();
  75 + };
  76 + el.onerror = () => {
  77 + this.loadedResources[url].error(new Error(`Unable to load ${url}`));
  78 + delete this.loadedResources[url];
  79 + };
  80 + this.anchor.appendChild(el);
  81 + return subject.asObservable();
  82 + }
  83 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Inject, Injectable } from '@angular/core';
  18 +import { WINDOW } from '@core/services/window.service';
  19 +import { WidgetInfo } from '@shared/models/widget.models';
  20 +import { ExceptionData } from '@app/shared/models/error.models';
  21 +import { isUndefined } from '@core/utils';
  22 +import { WindowMessage } from '@shared/models/window-message.model';
  23 +import { TranslateService } from '@ngx-translate/core';
  24 +import { customTranslationsPrefix } from '@app/shared/models/constants';
  25 +
  26 +@Injectable({
  27 + providedIn: 'root'
  28 +})
  29 +export class UtilsService {
  30 +
  31 + iframeMode = false;
  32 + widgetEditMode = false;
  33 + editWidgetInfo: WidgetInfo = null;
  34 +
  35 + constructor(@Inject(WINDOW) private window: Window,
  36 + private translate: TranslateService) {
  37 + let frame: Element = null;
  38 + try {
  39 + frame = window.frameElement;
  40 + } catch (e) {
  41 + // ie11 fix
  42 + }
  43 + if (frame) {
  44 + this.iframeMode = true;
  45 + const dataWidgetAttr = frame.getAttribute('data-widget');
  46 + if (dataWidgetAttr && dataWidgetAttr.length) {
  47 + this.editWidgetInfo = JSON.parse(dataWidgetAttr);
  48 + this.widgetEditMode = true;
  49 + }
  50 + }
  51 + }
  52 +
  53 + public processWidgetException(exception: any): ExceptionData {
  54 + const data = this.parseException(exception, -5);
  55 + if (this.widgetEditMode) {
  56 + const message: WindowMessage = {
  57 + type: 'widgetException',
  58 + data
  59 + };
  60 + this.window.parent.postMessage(message, '*');
  61 + }
  62 + return data;
  63 + }
  64 +
  65 + public parseException(exception: any, lineOffset?: number): ExceptionData {
  66 + const data: ExceptionData = {};
  67 + if (exception) {
  68 + if (typeof exception === 'string') {
  69 + data.message = exception;
  70 + } else if (exception instanceof String) {
  71 + data.message = exception.toString();
  72 + } else {
  73 + if (exception.name) {
  74 + data.name = exception.name;
  75 + } else {
  76 + data.name = 'UnknownError';
  77 + }
  78 + if (exception.message) {
  79 + data.message = exception.message;
  80 + }
  81 + if (exception.lineNumber) {
  82 + data.lineNumber = exception.lineNumber;
  83 + if (exception.columnNumber) {
  84 + data.columnNumber = exception.columnNumber;
  85 + }
  86 + } else if (exception.stack) {
  87 + const lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g;
  88 + const lineInfoGroups = lineInfoRegexp.exec(exception.stack);
  89 + if (lineInfoGroups != null && lineInfoGroups.length >= 3) {
  90 + if (isUndefined(lineOffset)) {
  91 + lineOffset = -2;
  92 + }
  93 + data.lineNumber = Number(lineInfoGroups[2]) + lineOffset;
  94 + if (lineInfoGroups.length >= 5) {
  95 + data.columnNumber = Number(lineInfoGroups[4]);
  96 + }
  97 + }
  98 + }
  99 + }
  100 + }
  101 + return data;
  102 + }
  103 +
  104 + public customTranslation(translationValue: string, defaultValue: string): string {
  105 + let result = '';
  106 + const translationId = customTranslationsPrefix + translationValue;
  107 + const translation = this.translate.instant(translationId);
  108 + if (translation !== translationId) {
  109 + result = translation + '';
  110 + } else {
  111 + result = defaultValue;
  112 + }
  113 + return result;
  114 + }
  115 +
  116 +}
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 import { BehaviorSubject, Observable, Subject } from 'rxjs';
18 18 import { finalize, share } from 'rxjs/operators';
  19 +import base64js from 'base64-js';
19 20
20 21 export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
21 22 const scrollSubject = new Subject<Event>();
... ... @@ -78,6 +79,38 @@ export function isDefined(value: any): boolean {
78 79 return typeof value !== 'undefined';
79 80 }
80 81
  82 +export function isFunction(value: any): boolean {
  83 + return typeof value === 'function';
  84 +}
  85 +
  86 +export function objToBase64(obj: any): string {
  87 + const json = JSON.stringify(obj);
  88 + const encoded = utf8Encode(json);
  89 + const b64Encoded: string = base64js.fromByteArray(encoded);
  90 + return b64Encoded;
  91 +}
  92 +
  93 +export function base64toObj(b64Encoded: string): any {
  94 + const encoded: Uint8Array | number[] = base64js.toByteArray(b64Encoded);
  95 + const json = utf8Decode(encoded);
  96 + const obj = JSON.parse(json);
  97 + return obj;
  98 +}
  99 +
  100 +function utf8Encode(str: string): Uint8Array | number[] {
  101 + let result: Uint8Array | number[];
  102 + if (isUndefined(Uint8Array)) {
  103 + result = utf8ToBytes(str);
  104 + } else {
  105 + result = new Uint8Array(utf8ToBytes(str));
  106 + }
  107 + return result;
  108 +}
  109 +
  110 +function utf8Decode(bytes: Uint8Array | number[]): string {
  111 + return utf8Slice(bytes, 0, bytes.length);
  112 +}
  113 +
81 114 const scrollRegex = /(auto|scroll)/;
82 115
83 116 function parentNodes(node: Node, nodes: Node[]): Node[] {
... ... @@ -138,3 +171,126 @@ function easeInOut(
138 171 (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime
139 172 );
140 173 }
  174 +
  175 +function utf8Slice(buf: Uint8Array | number[], start: number, end: number): string {
  176 + let res = '';
  177 + let tmp = '';
  178 + end = Math.min(buf.length, end || Infinity);
  179 + start = start || 0;
  180 +
  181 + for (let i = start; i < end; i++) {
  182 + if (buf[i] <= 0x7F) {
  183 + res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]);
  184 + tmp = '';
  185 + } else {
  186 + tmp += '%' + buf[i].toString(16);
  187 + }
  188 + }
  189 + return res + decodeUtf8Char(tmp);
  190 +}
  191 +
  192 +function decodeUtf8Char(str: string): string {
  193 + try {
  194 + return decodeURIComponent(str);
  195 + } catch (err) {
  196 + return String.fromCharCode(0xFFFD); // UTF 8 invalid char
  197 + }
  198 +}
  199 +
  200 +function utf8ToBytes(input: string, units?: number): number[] {
  201 + units = units || Infinity;
  202 + let codePoint: number;
  203 + const length = input.length;
  204 + let leadSurrogate: number = null;
  205 + const bytes: number[] = [];
  206 + let i = 0;
  207 +
  208 + for (; i < length; i++) {
  209 + codePoint = input.charCodeAt(i);
  210 +
  211 + // is surrogate component
  212 + if (codePoint > 0xD7FF && codePoint < 0xE000) {
  213 + // last char was a lead
  214 + if (leadSurrogate) {
  215 + // 2 leads in a row
  216 + if (codePoint < 0xDC00) {
  217 + units -= 3;
  218 + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); }
  219 + leadSurrogate = codePoint;
  220 + continue;
  221 + } else {
  222 + // valid surrogate pair
  223 + // tslint:disable-next-line:no-bitwise
  224 + codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000;
  225 + leadSurrogate = null;
  226 + }
  227 + } else {
  228 + // no lead yet
  229 +
  230 + if (codePoint > 0xDBFF) {
  231 + // unexpected trail
  232 + units -= 3;
  233 + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); }
  234 + continue;
  235 + } else if (i + 1 === length) {
  236 + // unpaired lead
  237 + units -= 3;
  238 + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); }
  239 + continue;
  240 + } else {
  241 + // valid lead
  242 + leadSurrogate = codePoint;
  243 + continue;
  244 + }
  245 + }
  246 + } else if (leadSurrogate) {
  247 + // valid bmp char, but last char was a lead
  248 + units -= 3;
  249 + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); }
  250 + leadSurrogate = null;
  251 + }
  252 +
  253 + // encode utf8
  254 + if (codePoint < 0x80) {
  255 + units -= 1;
  256 + if (units < 0) { break; }
  257 + bytes.push(codePoint);
  258 + } else if (codePoint < 0x800) {
  259 + units -= 2;
  260 + if (units < 0) { break; }
  261 + bytes.push(
  262 + // tslint:disable-next-line:no-bitwise
  263 + codePoint >> 0x6 | 0xC0,
  264 + // tslint:disable-next-line:no-bitwise
  265 + codePoint & 0x3F | 0x80
  266 + );
  267 + } else if (codePoint < 0x10000) {
  268 + units -= 3;
  269 + if (units < 0) { break; }
  270 + bytes.push(
  271 + // tslint:disable-next-line:no-bitwise
  272 + codePoint >> 0xC | 0xE0,
  273 + // tslint:disable-next-line:no-bitwise
  274 + codePoint >> 0x6 & 0x3F | 0x80,
  275 + // tslint:disable-next-line:no-bitwise
  276 + codePoint & 0x3F | 0x80
  277 + );
  278 + } else if (codePoint < 0x200000) {
  279 + units -= 4;
  280 + if (units < 0) { break; }
  281 + bytes.push(
  282 + // tslint:disable-next-line:no-bitwise
  283 + codePoint >> 0x12 | 0xF0,
  284 + // tslint:disable-next-line:no-bitwise
  285 + codePoint >> 0xC & 0x3F | 0x80,
  286 + // tslint:disable-next-line:no-bitwise
  287 + codePoint >> 0x6 & 0x3F | 0x80,
  288 + // tslint:disable-next-line:no-bitwise
  289 + codePoint & 0x3F | 0x80
  290 + );
  291 + } else {
  292 + throw new Error('Invalid code point');
  293 + }
  294 + }
  295 + return bytes;
  296 +}
... ...
... ... @@ -16,15 +16,15 @@
16 16
17 17 -->
18 18 <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
19   - [ngStyle]="options.dashboardStyle"
20   - [fxShow]="(loading() | async) && !options.isEdit">
  19 + [ngStyle]="dashboardStyle"
  20 + [fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit">
21 21 <mat-spinner color="warn" mode="indeterminate" diameter="100">
22 22 </mat-spinner>
23 23 </div>
24 24 <div id="gridster-parent"
25 25 fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
26 26 (contextmenu)="openDashboardContextMenu($event)">
27   - <div [ngClass]="options.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
  27 + <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
28 28 <gridster #gridster id="gridster-child" [options]="gridsterOpts">
29 29 <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async">
30 30 <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
... ... @@ -58,42 +58,42 @@
58 58 fxLayoutAlign="start center"
59 59 (mousedown)="$event.stopPropagation()">
60 60 <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
61   - [fxShow]="!options.isEdit"
  61 + [fxShow]="!isEdit"
62 62 (click)="action.onAction($event)"
63 63 matTooltip="{{ action.displayName }}"
64 64 matTooltipPosition="above">
65 65 <mat-icon>{{ action.icon }}</mat-icon>
66 66 </button>
67 67 <button mat-button mat-icon-button *ngFor="let action of widget.widgetActions"
68   - [fxShow]="!options.isEdit && action.show"
  68 + [fxShow]="!isEdit && action.show"
69 69 (click)="action.onAction($event)"
70 70 matTooltip="{{ action.name | translate }}"
71 71 matTooltipPosition="above">
72 72 <mat-icon>{{ action.icon }}</mat-icon>
73 73 </button>
74 74 <button mat-button mat-icon-button
75   - [fxShow]="!options.isEdit && widget.enableFullscreen"
  75 + [fxShow]="!isEdit && widget.enableFullscreen"
76 76 (click)="widget.isFullscreen = !widget.isFullscreen"
77 77 matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
78 78 matTooltipPosition="above">
79 79 <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
80 80 </button>
81 81 <button mat-button mat-icon-button
82   - [fxShow]="options.isEditActionEnabled && !widget.isFullscreen"
  82 + [fxShow]="isEditActionEnabled && !widget.isFullscreen"
83 83 (click)="editWidget($event, widget)"
84 84 matTooltip="{{ 'widget.edit' | translate }}"
85 85 matTooltipPosition="above">
86 86 <mat-icon>edit</mat-icon>
87 87 </button>
88 88 <button mat-button mat-icon-button
89   - [fxShow]="options.isExportActionEnabled && !widget.isFullscreen"
  89 + [fxShow]="isExportActionEnabled && !widget.isFullscreen"
90 90 (click)="exportWidget($event, widget)"
91 91 matTooltip="{{ 'widget.export' | translate }}"
92 92 matTooltipPosition="above">
93 93 <mat-icon>file_download</mat-icon>
94 94 </button>
95 95 <button mat-button mat-icon-button
96   - [fxShow]="options.isRemoveActionEnabled && !widget.isFullscreen"
  96 + [fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
97 97 (click)="removeWidget($event, widget)"
98 98 matTooltip="{{ 'widget.remove' | translate }}"
99 99 matTooltipPosition="above">
... ... @@ -102,7 +102,12 @@
102 102 </div>
103 103 </div>
104 104 <div fxFlex fxLayout="column" class="tb-widget-content">
105   -
  105 + <tb-widget fxFlex
  106 + [dashboardWidget]="widget"
  107 + [isEdit]="isEdit"
  108 + [isMobile]="isMobileSize"
  109 + [dashboard]="this">
  110 + </tb-widget>
106 111 </div>
107 112 </div>
108 113 </gridster-item>
... ...
... ... @@ -14,40 +14,108 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, OnInit, Input, ViewChild, AfterViewInit, ViewChildren, QueryList, ElementRef } from '@angular/core';
  17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + Input,
  21 + OnChanges,
  22 + OnInit,
  23 + QueryList,
  24 + SimpleChanges,
  25 + ViewChild,
  26 + ViewChildren
  27 +} from '@angular/core';
18 28 import { Store } from '@ngrx/store';
19 29 import { AppState } from '@core/core.state';
20 30 import { PageComponent } from '@shared/components/page.component';
21 31 import { AuthUser } from '@shared/models/user.model';
22 32 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
23   -import { coerceBooleanProperty } from '@angular/cdk/coercion';
24 33 import { Timewindow } from '@shared/models/time/time.models';
25 34 import { TimeService } from '@core/services/time.service';
26 35 import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
27   -import { GridsterResizable } from 'angular-gridster2/lib/gridsterResizable.service';
28   -import { IDashboardComponent, DashboardConfig, DashboardWidget } from '../../models/dashboard-component.models';
29   -import { MatSort } from '@angular/material/sort';
30   -import { Observable, ReplaySubject, merge } from 'rxjs';
  36 +import {
  37 + DashboardCallbacks,
  38 + DashboardWidget,
  39 + IDashboardComponent,
  40 + WidgetsData
  41 +} from '../../models/dashboard-component.models';
  42 +import { merge, Observable } from 'rxjs';
31 43 import { map, share, tap } from 'rxjs/operators';
32 44 import { WidgetLayout } from '@shared/models/dashboard.models';
33 45 import { DialogService } from '@core/services/dialog.service';
34   -import { Widget } from '@app/shared/models/widget.models';
35   -import { MatTab } from '@angular/material/tabs';
36 46 import { animatedScroll, isDefined } from '@app/core/utils';
37   -import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
  47 +import { BreakpointObserver } from '@angular/cdk/layout';
38 48 import { MediaBreakpoints } from '@shared/models/constants';
  49 +import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
39 50
40 51 @Component({
41 52 selector: 'tb-dashboard',
42 53 templateUrl: './dashboard.component.html',
43 54 styleUrls: ['./dashboard.component.scss']
44 55 })
45   -export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit {
  56 +export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit, OnChanges {
46 57
47 58 authUser: AuthUser;
48 59
49 60 @Input()
50   - options: DashboardConfig;
  61 + widgetsData: Observable<WidgetsData>;
  62 +
  63 + @Input()
  64 + callbacks: DashboardCallbacks;
  65 +
  66 + @Input()
  67 + aliasController: IAliasController;
  68 +
  69 + @Input()
  70 + stateController: IStateController;
  71 +
  72 + @Input()
  73 + columns: number;
  74 +
  75 + @Input()
  76 + horizontalMargin: number;
  77 +
  78 + @Input()
  79 + verticalMargin: number;
  80 +
  81 + @Input()
  82 + isEdit: boolean;
  83 +
  84 + @Input()
  85 + autofillHeight: boolean;
  86 +
  87 + @Input()
  88 + mobileAutofillHeight: boolean;
  89 +
  90 + @Input()
  91 + mobileRowHeight: number;
  92 +
  93 + @Input()
  94 + isMobile: boolean;
  95 +
  96 + @Input()
  97 + isMobileDisabled: boolean;
  98 +
  99 + @Input()
  100 + isEditActionEnabled: boolean;
  101 +
  102 + @Input()
  103 + isExportActionEnabled: boolean;
  104 +
  105 + @Input()
  106 + isRemoveActionEnabled: boolean;
  107 +
  108 + @Input()
  109 + dashboardStyle: {[klass: string]: any};
  110 +
  111 + @Input()
  112 + dashboardClass: string;
  113 +
  114 + @Input()
  115 + ignoreLoading: boolean;
  116 +
  117 + @Input()
  118 + dashboardTimewindow: Timewindow;
51 119
52 120 gridsterOpts: GridsterConfig;
53 121
... ... @@ -77,8 +145,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
77 145 }
78 146
79 147 ngOnInit(): void {
80   - if (!this.options.dashboardTimewindow) {
81   - this.options.dashboardTimewindow = this.timeService.defaultTimewindow();
  148 + if (!this.dashboardTimewindow) {
  149 + this.dashboardTimewindow = this.timeService.defaultTimewindow();
82 150 }
83 151 this.gridsterOpts = {
84 152 gridType: 'scrollVertical',
... ... @@ -86,35 +154,65 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
86 154 pushItems: false,
87 155 swap: false,
88 156 maxRows: 100,
89   - minCols: this.options.columns ? this.options.columns : 24,
  157 + minCols: this.columns ? this.columns : 24,
90 158 outerMargin: true,
91   - outerMarginLeft: this.options.margins ? this.options.margins[0] : 10,
92   - outerMarginRight: this.options.margins ? this.options.margins[0] : 10,
93   - outerMarginTop: this.options.margins ? this.options.margins[1] : 10,
94   - outerMarginBottom: this.options.margins ? this.options.margins[1] : 10,
  159 + outerMarginLeft: this.horizontalMargin ? this.horizontalMargin : 10,
  160 + outerMarginRight: this.horizontalMargin ? this.horizontalMargin : 10,
  161 + outerMarginTop: this.verticalMargin ? this.verticalMargin : 10,
  162 + outerMarginBottom: this.horizontalMargin ? this.horizontalMargin : 10,
95 163 minItemCols: 1,
96 164 minItemRows: 1,
97 165 defaultItemCols: 8,
98 166 defaultItemRows: 6,
99   - resizable: {enabled: this.options.isEdit},
100   - draggable: {enabled: this.options.isEdit}
  167 + resizable: {enabled: this.isEdit},
  168 + draggable: {enabled: this.isEdit},
  169 + itemChangeCallback: item => this.sortWidgets(this.widgets)
101 170 };
102 171
103   - this.updateGridsterOpts();
  172 + this.updateMobileOpts();
104 173
105 174 this.loadDashboard();
106 175
107   - merge(this.breakpointObserver
108   - .observe(MediaBreakpoints['gt-sm']), this.options.layoutChange$).subscribe(
  176 + this.breakpointObserver
  177 + .observe(MediaBreakpoints['gt-sm']).subscribe(
109 178 () => {
110   - this.updateGridsterOpts();
111   - this.sortWidgets(this.widgets);
  179 + this.updateMobileOpts();
112 180 }
113 181 );
114 182 }
115 183
  184 + ngOnChanges(changes: SimpleChanges): void {
  185 + let updateMobileOpts = false;
  186 + let updateLayoutOpts = false;
  187 + let updateEditingOpts = false;
  188 + for (const propName of Object.keys(changes)) {
  189 + const change = changes[propName];
  190 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  191 + if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) {
  192 + updateMobileOpts = true;
  193 + } else if (['horizontalMargin', 'verticalMargin'].includes(propName)) {
  194 + updateLayoutOpts = true;
  195 + } else if (propName === 'isEdit') {
  196 + updateEditingOpts = true;
  197 + }
  198 + }
  199 + }
  200 + if (updateMobileOpts) {
  201 + this.updateMobileOpts();
  202 + }
  203 + if (updateLayoutOpts) {
  204 + this.updateLayoutOpts();
  205 + }
  206 + if (updateEditingOpts) {
  207 + this.updateEditingOpts();
  208 + }
  209 + if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) {
  210 + this.notifyGridsterOptionsChanged();
  211 + }
  212 + }
  213 +
116 214 loadDashboard() {
117   - this.widgets$ = this.options.widgetsData.pipe(
  215 + this.widgets$ = this.widgetsData.pipe(
118 216 map(widgetsData => {
119 217 const dashboardWidgets = new Array<DashboardWidget>();
120 218 let maxRows = this.gridsterOpts.maxRows;
... ... @@ -164,19 +262,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
164 262
165 263 isAutofillHeight(): boolean {
166 264 if (this.isMobileSize) {
167   - return isDefined(this.options.mobileAutofillHeight) ? this.options.mobileAutofillHeight : false;
  265 + return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false;
168 266 } else {
169   - return isDefined(this.options.autofillHeight) ? this.options.autofillHeight : false;
  267 + return isDefined(this.autofillHeight) ? this.autofillHeight : false;
170 268 }
171 269 }
172 270
173   - loading(): Observable<boolean> {
174   - return this.isLoading$.pipe(
175   - map(loading => (!this.options.ignoreLoading && loading) || this.dashboardLoading),
176   - share()
177   - );
178   - }
179   -
180 271 openDashboardContextMenu($event: Event) {
181 272 // TODO:
182 273 // this.dialogService.todo();
... ... @@ -192,14 +283,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
192 283 }
193 284
194 285 widgetMouseDown($event: Event, widget: DashboardWidget) {
195   - if (this.options.onWidgetMouseDown) {
196   - this.options.onWidgetMouseDown($event, widget.widget);
  286 + if (this.callbacks && this.callbacks.onWidgetMouseDown) {
  287 + this.callbacks.onWidgetMouseDown($event, widget.widget);
197 288 }
198 289 }
199 290
200 291 widgetClicked($event: Event, widget: DashboardWidget) {
201   - if (this.options.onWidgetClicked) {
202   - this.options.onWidgetClicked($event, widget.widget);
  292 + if (this.callbacks && this.callbacks.onWidgetClicked) {
  293 + this.callbacks.onWidgetClicked($event, widget.widget);
203 294 }
204 295 }
205 296
... ... @@ -207,8 +298,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
207 298 if ($event) {
208 299 $event.stopPropagation();
209 300 }
210   - if (this.options.isEditActionEnabled && this.options.onEditWidget) {
211   - this.options.onEditWidget($event, widget.widget);
  301 + if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) {
  302 + this.callbacks.onEditWidget($event, widget.widget);
212 303 }
213 304 }
214 305
... ... @@ -216,8 +307,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
216 307 if ($event) {
217 308 $event.stopPropagation();
218 309 }
219   - if (this.options.isExportActionEnabled && this.options.onExportWidget) {
220   - this.options.onExportWidget($event, widget.widget);
  310 + if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) {
  311 + this.callbacks.onExportWidget($event, widget.widget);
221 312 }
222 313 }
223 314
... ... @@ -225,8 +316,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
225 316 if ($event) {
226 317 $event.stopPropagation();
227 318 }
228   - if (this.options.isRemoveActionEnabled && this.options.onRemoveWidget) {
229   - this.options.onRemoveWidget($event, widget.widget);
  319 + if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
  320 + this.callbacks.onRemoveWidget($event, widget.widget);
230 321 }
231 322 }
232 323
... ... @@ -272,7 +363,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
272 363 }
273 364 }
274 365
275   - private updateGridsterOpts() {
  366 + private updateMobileOpts() {
276 367 this.isMobileSize = this.checkIsMobileSize();
277 368 const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
278 369 this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
... ... @@ -285,6 +376,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
285 376 } else {
286 377 this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical';
287 378 }
  379 + }
  380 +
  381 + private updateLayoutOpts() {
  382 + this.gridsterOpts.outerMarginLeft = this.horizontalMargin ? this.horizontalMargin : 10;
  383 + this.gridsterOpts.outerMarginRight = this.horizontalMargin ? this.horizontalMargin : 10;
  384 + this.gridsterOpts.outerMarginTop = this.verticalMargin ? this.verticalMargin : 10;
  385 + this.gridsterOpts.outerMarginBottom = this.horizontalMargin ? this.horizontalMargin : 10;
  386 + }
  387 +
  388 + private updateEditingOpts() {
  389 + this.gridsterOpts.resizable.enabled = this.isEdit;
  390 + this.gridsterOpts.draggable.enabled = this.isEdit;
  391 + }
  392 +
  393 + private notifyGridsterOptionsChanged() {
288 394 if (this.gridster && this.gridster.options) {
289 395 this.gridster.optionsChanged();
290 396 }
... ... @@ -294,15 +400,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
294 400 let rowHeight = null;
295 401 if (!this.isAutofillHeight()) {
296 402 if (isMobile) {
297   - rowHeight = isDefined(this.options.mobileRowHeight) ? this.options.mobileRowHeight : 70;
  403 + rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70;
298 404 }
299 405 }
300 406 return rowHeight;
301 407 }
302 408
303 409 private checkIsMobileSize(): boolean {
304   - const isMobileDisabled = this.options.isMobileDisabled === true;
305   - let isMobileSize = this.options.isMobile === true && !isMobileDisabled;
  410 + const isMobileDisabled = this.isMobileDisabled === true;
  411 + let isMobileSize = this.isMobile === true && !isMobileDisabled;
306 412 if (!isMobileSize && !isMobileDisabled) {
307 413 isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
308 414 }
... ...
... ... @@ -14,14 +14,14 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {NgModule} from '@angular/core';
18   -import {CommonModule} from '@angular/common';
19   -import {SharedModule} from '@app/shared/shared.module';
20   -import {AddEntityDialogComponent} from './entity/add-entity-dialog.component';
21   -import {EntitiesTableComponent} from './entity/entities-table.component';
22   -import {DetailsPanelComponent} from './details-panel.component';
23   -import {EntityDetailsPanelComponent} from './entity/entity-details-panel.component';
24   -import {ContactComponent} from './contact.component';
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@app/shared/shared.module';
  20 +import { AddEntityDialogComponent } from './entity/add-entity-dialog.component';
  21 +import { EntitiesTableComponent } from './entity/entities-table.component';
  22 +import { DetailsPanelComponent } from './details-panel.component';
  23 +import { EntityDetailsPanelComponent } from './entity/entity-details-panel.component';
  24 +import { ContactComponent } from './contact.component';
25 25 import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-dialog.component';
26 26 import { AuditLogTableComponent } from './audit-log/audit-log-table.component';
27 27 import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
... ... @@ -35,6 +35,8 @@ import { AttributeTableComponent } from '@home/components/attribute/attribute-ta
35 35 import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
36 36 import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
37 37 import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
  38 +import { WidgetComponent } from '@home/components/widget/widget.component';
  39 +import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-component-factory.service';
38 40
39 41 @NgModule({
40 42 entryComponents: [
... ... @@ -66,7 +68,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
66 68 AttributeTableComponent,
67 69 AddAttributeDialogComponent,
68 70 EditAttributeValuePanelComponent,
69   - DashboardComponent
  71 + DashboardComponent,
  72 + WidgetComponent
70 73 ],
71 74 imports: [
72 75 CommonModule,
... ... @@ -84,7 +87,11 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
84 87 AlarmTableComponent,
85 88 AlarmDetailsDialogComponent,
86 89 AttributeTableComponent,
87   - DashboardComponent
  90 + DashboardComponent,
  91 + WidgetComponent
  92 + ],
  93 + providers: [
  94 + DynamicWidgetComponentFactoryService
88 95 ]
89 96 })
90 97 export class HomeComponentsModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Compiler,
  19 + Component,
  20 + ComponentFactory,
  21 + Injectable,
  22 + Injector,
  23 + NgModule,
  24 + NgModuleRef,
  25 + Type,
  26 + ViewEncapsulation
  27 +} from '@angular/core';
  28 +import {
  29 + DynamicWidgetComponent,
  30 + DynamicWidgetComponentModule
  31 +} from '@home/components/widget/dynamic-widget.component';
  32 +import { CommonModule } from '@angular/common';
  33 +import { SharedModule } from '@shared/shared.module';
  34 +import { Observable, ReplaySubject } from 'rxjs';
  35 +import { HomeComponentsModule } from '../home-components.module';
  36 +import { WidgetComponentsModule } from './widget-components.module';
  37 +
  38 +interface DynamicWidgetComponentModuleData {
  39 + moduleRef: NgModuleRef<DynamicWidgetComponentModule>;
  40 + moduleType: Type<DynamicWidgetComponentModule>;
  41 +}
  42 +
  43 +@Injectable()
  44 +export class DynamicWidgetComponentFactoryService {
  45 +
  46 + private dynamicComponentModulesMap = new Map<ComponentFactory<DynamicWidgetComponent>, DynamicWidgetComponentModuleData>();
  47 +
  48 + constructor(private compiler: Compiler,
  49 + private injector: Injector) {
  50 + }
  51 +
  52 + public createDynamicWidgetComponentFactory(template: string): Observable<ComponentFactory<DynamicWidgetComponent>> {
  53 + const dymamicWidgetComponentFactorySubject = new ReplaySubject<ComponentFactory<DynamicWidgetComponent>>();
  54 + const comp = this.createDynamicWidgetComponent(template);
  55 + // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent
  56 + @NgModule({
  57 + declarations: [comp],
  58 + entryComponents: [comp],
  59 + imports: [CommonModule, SharedModule, WidgetComponentsModule],
  60 + })
  61 + class DynamicWidgetComponentInstanceModule extends DynamicWidgetComponentModule {}
  62 + this.compiler.compileModuleAsync(DynamicWidgetComponentInstanceModule).then(
  63 + (module) => {
  64 + const moduleRef = module.create(this.injector);
  65 + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
  66 + this.dynamicComponentModulesMap.set(factory, {
  67 + moduleRef,
  68 + moduleType: module.moduleType
  69 + });
  70 + dymamicWidgetComponentFactorySubject.next(factory);
  71 + dymamicWidgetComponentFactorySubject.complete();
  72 + }
  73 + ).catch(
  74 + (e) => {
  75 + dymamicWidgetComponentFactorySubject.error(`Failed to create dynamic widget component factory: ${e}`);
  76 + }
  77 + );
  78 + return dymamicWidgetComponentFactorySubject.asObservable();
  79 + }
  80 +
  81 + public destroyDynamicWidgetComponentFactory(factory: ComponentFactory<DynamicWidgetComponent>) {
  82 + const moduleData = this.dynamicComponentModulesMap.get(factory);
  83 + if (moduleData) {
  84 + moduleData.moduleRef.destroy();
  85 + this.compiler.clearCacheFor(moduleData.moduleType);
  86 + this.dynamicComponentModulesMap.delete(factory);
  87 + }
  88 + }
  89 +
  90 + private createDynamicWidgetComponent(template: string): Type<DynamicWidgetComponent> {
  91 + // noinspection AngularMissingOrInvalidDeclarationInModule
  92 + @Component({
  93 + template
  94 + })
  95 + class DynamicWidgetInstanceComponent extends DynamicWidgetComponent { }
  96 +
  97 + return DynamicWidgetInstanceComponent;
  98 + }
  99 +
  100 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { PageComponent } from '@shared/components/page.component';
  18 +import { Input, OnDestroy, OnInit } from '@angular/core';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models';
  22 +import { ExceptionData } from '@shared/models/error.models';
  23 +
  24 +export abstract class DynamicWidgetComponentModule implements OnDestroy {
  25 +
  26 + ngOnDestroy(): void {
  27 + console.log('Module destroyed!');
  28 + }
  29 +
  30 +}
  31 +
  32 +export abstract class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
  33 +
  34 + @Input()
  35 + widgetContext: WidgetContext;
  36 +
  37 + @Input()
  38 + widgetErrorData: ExceptionData;
  39 +
  40 + @Input()
  41 + loadingData: boolean;
  42 +
  43 + [key: string]: any;
  44 +
  45 + constructor(protected store: Store<AppState>) {
  46 + super(store);
  47 + }
  48 +
  49 + ngOnInit() {
  50 +
  51 + }
  52 +
  53 + ngOnDestroy(): void {
  54 + console.log('Component destroyed!');
  55 + }
  56 +
  57 + clearRpcError() {
  58 + if (this.widgetContext.defaultSubscription) {
  59 + this.widgetContext.defaultSubscription.clearRpcError();
  60 + }
  61 + }
  62 +
  63 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<table class="tb-legend">
  19 + <thead>
  20 + <tr class="tb-legend-header" *ngIf="!isRowDirection">
  21 + <th colspan="2"></th>
  22 + <th *ngIf="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th>
  23 + <th *ngIf="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th>
  24 + <th *ngIf="legendConfig.showAvg === true">{{ 'legend.avg' | translate }}</th>
  25 + <th *ngIf="legendConfig.showTotal === true">{{ 'legend.total' | translate }}</th>
  26 + </tr>
  27 + </thead>
  28 + <tbody>
  29 + <tr class="tb-legend-keys" *ngFor="let legendKey of legendData.keys" [ngClass]="{ 'tb-row-direction': isRowDirection }">
  30 + <td><span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span></td>
  31 + <td class="tb-legend-label"
  32 + (click)="toggleHideData(legendKey.dataIndex)"
  33 + [ngClass]="{ 'tb-hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden, 'tb-horizontal': isHorizontal }">
  34 + {{ legendKey.dataKey.label }}
  35 + </td>
  36 + <td class="tb-legend-value" *ngIf="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
  37 + <td class="tb-legend-value" *ngIf="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>
  38 + <td class="tb-legend-value" *ngIf="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td>
  39 + <td class="tb-legend-value" *ngIf="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td>
  40 + </tr>
  41 + </tbody>
  42 +</table>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + table.tb-legend {
  19 + width: 100%;
  20 + font-size: 12px;
  21 +
  22 + .tb-legend-header,
  23 + .tb-legend-value {
  24 + text-align: right;
  25 + }
  26 +
  27 + .tb-legend-header {
  28 + th {
  29 + padding: 0 10px 1px 0;
  30 + color: rgb(255, 110, 64);
  31 + white-space: nowrap;
  32 + }
  33 + }
  34 +
  35 + .tb-legend-keys {
  36 + td.tb-legend-label,
  37 + td.tb-legend-value {
  38 + padding: 2px 10px;
  39 + white-space: nowrap;
  40 + }
  41 +
  42 + .tb-legend-line {
  43 + display: inline-block;
  44 + width: 15px;
  45 + height: 3px;
  46 + vertical-align: middle;
  47 + }
  48 +
  49 + .tb-legend-label {
  50 + text-align: left;
  51 + outline: none;
  52 +
  53 + &.tb-horizontal {
  54 + width: 95%;
  55 + }
  56 +
  57 + &.tb-hidden-label {
  58 + text-decoration: line-through;
  59 + opacity: .6;
  60 + }
  61 + }
  62 +
  63 + &.tb-row-direction {
  64 + display: inline-block;
  65 + }
  66 + }
  67 + }
  68 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input, OnInit } from '@angular/core';
  18 +import { LegendConfig, LegendData, LegendDirection, LegendPosition } from '@shared/models/widget.models';
  19 +
  20 +@Component({
  21 + selector: 'tb-legend',
  22 + templateUrl: './legend.component.html',
  23 + styleUrls: ['./legend.component.scss']
  24 +})
  25 +export class LegendComponent implements OnInit {
  26 +
  27 + @Input()
  28 + legendConfig: LegendConfig;
  29 +
  30 + @Input()
  31 + legendData: LegendData;
  32 +
  33 + displayHeader: boolean;
  34 +
  35 + isHorizontal: boolean;
  36 +
  37 + isRowDirection: boolean;
  38 +
  39 + ngOnInit(): void {
  40 + this.displayHeader = this.legendConfig.showMin === true ||
  41 + this.legendConfig.showMax === true ||
  42 + this.legendConfig.showAvg === true ||
  43 + this.legendConfig.showTotal === true;
  44 +
  45 + this.isHorizontal = this.legendConfig.position === LegendPosition.bottom ||
  46 + this.legendConfig.position === LegendPosition.top;
  47 +
  48 + this.isRowDirection = this.legendConfig.direction === LegendDirection.row;
  49 + }
  50 +
  51 + toggleHideData(index: number) {
  52 + this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden;
  53 + }
  54 +
  55 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@app/shared/shared.module';
  20 +import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
  21 +import { LegendComponent } from '@home/components/widget/legend.component';
  22 +
  23 +@NgModule({
  24 + entryComponents: [
  25 + ],
  26 + declarations:
  27 + [
  28 + LegendComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule
  33 + ],
  34 + exports: [
  35 + LegendComponent
  36 + ]
  37 +})
  38 +export class WidgetComponentsModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<ng-container #widgetContent></ng-container>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-widget {
  18 + .tb-widget-error {
  19 + display: flex;
  20 + align-items: center;
  21 + justify-content: center;
  22 + background: rgba(255, 255, 255, .5);
  23 +
  24 + span {
  25 + color: #f00;
  26 + }
  27 + }
  28 +
  29 + .tb-widget-loading {
  30 + z-index: 3;
  31 + background: rgba(255, 255, 255, .15);
  32 + }
  33 +
  34 + .tb-widget-error-container {
  35 + position: absolute;
  36 + width: 100%;
  37 + height: 100%;
  38 + background-color: #fff;
  39 + }
  40 +
  41 + .tb-widget-error-msg {
  42 + padding: 5px;
  43 + font-size: 16px;
  44 + color: #f00;
  45 + word-wrap: break-word;
  46 + }
  47 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + ComponentFactory,
  21 + ComponentFactoryResolver,
  22 + ComponentRef,
  23 + ElementRef,
  24 + Injector,
  25 + Input,
  26 + OnChanges,
  27 + OnDestroy,
  28 + OnInit,
  29 + SimpleChanges,
  30 + ViewChild,
  31 + ViewContainerRef,
  32 + ViewEncapsulation
  33 +} from '@angular/core';
  34 +import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
  35 +import {
  36 + LegendConfig,
  37 + LegendData,
  38 + LegendPosition,
  39 + Widget,
  40 + WidgetActionDescriptor,
  41 + WidgetActionType,
  42 + WidgetInfo, WidgetResource,
  43 + widgetType,
  44 + WidgetTypeInstance,
  45 + widgetActionSources
  46 +} from '@shared/models/widget.models';
  47 +import { PageComponent } from '@shared/components/page.component';
  48 +import { Store } from '@ngrx/store';
  49 +import { AppState } from '@core/core.state';
  50 +import { WidgetService } from '@core/http/widget.service';
  51 +import { UtilsService } from '@core/services/utils.service';
  52 +import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
  53 +import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs';
  54 +import { DynamicWidgetComponentFactoryService } from '@home/components/widget/dynamic-widget-component-factory.service';
  55 +import { isDefined, objToBase64 } from '@core/utils';
  56 +import * as $ from 'jquery';
  57 +import { WidgetContext, WidgetHeaderAction } from '@home/models/widget-component.models';
  58 +import {
  59 + EntityInfo,
  60 + IWidgetSubscription,
  61 + SubscriptionInfo,
  62 + WidgetSubscriptionOptions,
  63 + StateObject,
  64 + StateParams,
  65 + WidgetSubscriptionContext
  66 +} from '@core/api/widget-api.models';
  67 +import { EntityId } from '@shared/models/id/entity-id';
  68 +import { ActivatedRoute, Router, UrlSegment } from '@angular/router';
  69 +import cssjs from '@core/css/css';
  70 +import { ResourcesService } from '@core/services/resources.service';
  71 +import { catchError, switchMap } from 'rxjs/operators';
  72 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  73 +import { TimeService } from '@core/services/time.service';
  74 +import { DeviceService } from '@app/core/http/device.service';
  75 +import { AlarmService } from '@app/core/http/alarm.service';
  76 +import { ExceptionData } from '@shared/models/error.models';
  77 +
  78 +@Component({
  79 + selector: 'tb-widget',
  80 + templateUrl: './widget.component.html',
  81 + styleUrls: ['./widget.component.scss'],
  82 + encapsulation: ViewEncapsulation.None
  83 +})
  84 +export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
  85 +
  86 + @Input()
  87 + isEdit: boolean;
  88 +
  89 + @Input()
  90 + isMobile: boolean;
  91 +
  92 + @Input()
  93 + dashboard: IDashboardComponent;
  94 +
  95 + @Input()
  96 + dashboardWidget: DashboardWidget;
  97 +
  98 + @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef;
  99 +
  100 + widget: Widget;
  101 + widgetInfo: WidgetInfo;
  102 + widgetContext: WidgetContext;
  103 + widgetType: any;
  104 + widgetTypeInstance: WidgetTypeInstance;
  105 + widgetErrorData: ExceptionData;
  106 +
  107 + dynamicWidgetComponentFactory: ComponentFactory<DynamicWidgetComponent>;
  108 + dynamicWidgetComponentRef: ComponentRef<DynamicWidgetComponent>;
  109 + dynamicWidgetComponent: DynamicWidgetComponent;
  110 +
  111 + subscriptionContext: WidgetSubscriptionContext;
  112 +
  113 + subscriptionInited = false;
  114 + widgetSizeDetected = false;
  115 +
  116 + onResizeListener = this.onResize.bind(this);
  117 +
  118 + private cssParser = new cssjs();
  119 +
  120 + constructor(protected store: Store<AppState>,
  121 + private route: ActivatedRoute,
  122 + private router: Router,
  123 + private dynamicWidgetComponentFactoryService: DynamicWidgetComponentFactoryService,
  124 + private componentFactoryResolver: ComponentFactoryResolver,
  125 + private elementRef: ElementRef,
  126 + private injector: Injector,
  127 + private widgetService: WidgetService,
  128 + private resources: ResourcesService,
  129 + private timeService: TimeService,
  130 + private deviceService: DeviceService,
  131 + private alarmService: AlarmService,
  132 + private utils: UtilsService) {
  133 + super(store);
  134 + }
  135 +
  136 + ngOnInit(): void {
  137 + this.widget = this.dashboardWidget.widget;
  138 +
  139 + const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
  140 + if (this.widget.config.actions) {
  141 + for (const actionSourceId of Object.keys(this.widget.config.actions)) {
  142 + const descriptors = this.widget.config.actions[actionSourceId];
  143 + const actionDescriptors: Array<WidgetActionDescriptor> = [];
  144 + descriptors.forEach((descriptor) => {
  145 + const actionDescriptor: WidgetActionDescriptor = {...descriptor};
  146 + actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name);
  147 + actionDescriptors.push(actionDescriptor);
  148 + });
  149 + actionDescriptorsBySourceId[actionSourceId] = actionDescriptors;
  150 + }
  151 + }
  152 +
  153 + this.widgetContext = this.dashboardWidget.widgetContext;
  154 + this.widgetContext.inited = false;
  155 + this.widgetContext.hideTitlePanel = false;
  156 + this.widgetContext.isEdit = this.isEdit;
  157 + this.widgetContext.isMobile = this.isMobile;
  158 + this.widgetContext.dashboard = this.dashboard;
  159 + this.widgetContext.widgetConfig = this.widget.config;
  160 + this.widgetContext.settings = this.widget.config.settings;
  161 + this.widgetContext.units = this.widget.config.units || '';
  162 + this.widgetContext.decimals = isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2;
  163 + this.widgetContext.subscriptions = {};
  164 + this.widgetContext.defaultSubscription = null;
  165 + this.widgetContext.dashboardTimewindow = this.dashboard.dashboardTimewindow;
  166 + this.widgetContext.timewindowFunctions = {
  167 + onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
  168 + if (this.widgetContext.defaultSubscription) {
  169 + this.widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval);
  170 + }
  171 + },
  172 + onResetTimewindow: () => {
  173 + if (this.widgetContext.defaultSubscription) {
  174 + this.widgetContext.defaultSubscription.onResetTimewindow();
  175 + }
  176 + }
  177 + };
  178 + this.widgetContext.subscriptionApi = {
  179 + createSubscription: this.createSubscription.bind(this),
  180 + createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this),
  181 + removeSubscription: (id) => {
  182 + const subscription = this.widgetContext.subscriptions[id];
  183 + if (subscription) {
  184 + subscription.destroy();
  185 + delete this.widgetContext.subscriptions[id];
  186 + }
  187 + }
  188 + };
  189 + this.widgetContext.controlApi = {
  190 + sendOneWayCommand: (method, params, timeout) => {
  191 + if (this.widgetContext.defaultSubscription) {
  192 + return this.widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
  193 + } else {
  194 + return of(null);
  195 + }
  196 + },
  197 + sendTwoWayCommand: (method, params, timeout) => {
  198 + if (this.widgetContext.defaultSubscription) {
  199 + return this.widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
  200 + } else {
  201 + return of(null);
  202 + }
  203 + }
  204 + };
  205 + this.widgetContext.utils = {
  206 + formatValue: this.formatValue
  207 + };
  208 + this.widgetContext.actionsApi = {
  209 + actionDescriptorsBySourceId,
  210 + getActionDescriptors: this.getActionDescriptors.bind(this),
  211 + handleWidgetAction: this.handleWidgetAction.bind(this),
  212 + elementClick: this.elementClick.bind(this)
  213 + };
  214 + this.widgetContext.stateController = this.dashboard.stateController;
  215 + this.widgetContext.aliasController = this.dashboard.aliasController;
  216 +
  217 + this.widgetContext.customHeaderActions = [];
  218 + const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value);
  219 + headerActionsDescriptors.forEach((descriptor) => {
  220 + const headerAction: WidgetHeaderAction = {
  221 + name: descriptor.name,
  222 + displayName: descriptor.displayName,
  223 + icon: descriptor.icon,
  224 + descriptor,
  225 + onAction: $event => {
  226 + const entityInfo = this.getActiveEntityInfo();
  227 + const entityId = entityInfo ? entityInfo.entityId : null;
  228 + const entityName = entityInfo ? entityInfo.entityName : null;
  229 + this.handleWidgetAction($event, descriptor, entityId, entityName);
  230 + }
  231 + };
  232 + this.widgetContext.customHeaderActions.push(headerAction);
  233 + });
  234 +
  235 +
  236 + this.subscriptionContext = {
  237 + timeService: this.timeService,
  238 + deviceService: this.deviceService,
  239 + alarmService: this.alarmService,
  240 + utils: this.utils,
  241 + widgetUtils: this.widgetContext.utils,
  242 + dashboardTimewindowApi: null, // TODO:
  243 + getServerTimeDiff: null, // TODO:
  244 + aliasController: this.dashboard.aliasController
  245 + };
  246 +
  247 + this.widgetService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe(
  248 + (widgetInfo) => {
  249 + this.widgetInfo = widgetInfo;
  250 + this.loadFromWidgetInfo();
  251 + }
  252 + );
  253 +
  254 + }
  255 +
  256 + ngAfterViewInit(): void {
  257 + }
  258 +
  259 + ngOnDestroy(): void {
  260 +
  261 + for (const id of Object.keys(this.widgetContext.subscriptions)) {
  262 + const subscription = this.widgetContext.subscriptions[id];
  263 + subscription.destroy();
  264 + }
  265 + this.subscriptionInited = false;
  266 + this.widgetContext.subscriptions = {};
  267 + if (this.widgetContext.inited) {
  268 + this.widgetContext.inited = false;
  269 + // TODO:
  270 + try {
  271 + this.widgetTypeInstance.onDestroy();
  272 + } catch (e) {
  273 + this.handleWidgetException(e);
  274 + }
  275 + }
  276 + this.destroyDynamicWidgetComponent();
  277 + }
  278 +
  279 + ngOnChanges(changes: SimpleChanges): void {
  280 + for (const propName of Object.keys(changes)) {
  281 + const change = changes[propName];
  282 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  283 + if (propName === 'isEdit') {
  284 + console.log(`isEdit changed: ${this.isEdit}`);
  285 + this.onEditModeChanged();
  286 + } else if (propName === 'isMobile') {
  287 + console.log(`isMobile changed: ${this.isMobile}`);
  288 + this.onMobileModeChanged();
  289 + }
  290 + }
  291 + }
  292 + }
  293 +
  294 + private onEditModeChanged() {
  295 + if (this.widgetContext.isEdit !== this.isEdit) {
  296 + this.widgetContext.isEdit = this.isEdit;
  297 + if (this.widgetContext.inited) {
  298 + // TODO:
  299 + }
  300 + }
  301 + }
  302 +
  303 + private onMobileModeChanged() {
  304 + if (this.widgetContext.isMobile !== this.isMobile) {
  305 + this.widgetContext.isMobile = this.isMobile;
  306 + if (this.widgetContext.inited) {
  307 + // TODO:
  308 + }
  309 + }
  310 + }
  311 +
  312 + private onResize() {
  313 + if (this.checkSize()) {
  314 + if (this.widgetContext.inited) {
  315 + // TODO:
  316 + }
  317 + }
  318 + }
  319 +
  320 + private loadFromWidgetInfo() {
  321 + const widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
  322 + const elem = this.elementRef.nativeElement;
  323 + elem.classList.add('tb-widget');
  324 + elem.classList.add(widgetNamespace);
  325 + this.widgetType = this.widgetInfo.widgetTypeFunction;
  326 +
  327 + if (!this.widgetType) {
  328 + this.widgetTypeInstance = {};
  329 + } else {
  330 + try {
  331 + this.widgetTypeInstance = new this.widgetType(this.widgetContext);
  332 + } catch (e) {
  333 + this.handleWidgetException(e);
  334 + this.widgetTypeInstance = {};
  335 + }
  336 + }
  337 + if (!this.widgetTypeInstance.onInit) {
  338 + this.widgetTypeInstance.onInit = () => {};
  339 + }
  340 + if (!this.widgetTypeInstance.onDataUpdated) {
  341 + this.widgetTypeInstance.onDataUpdated = () => {};
  342 + }
  343 + if (!this.widgetTypeInstance.onResize) {
  344 + this.widgetTypeInstance.onResize = () => {};
  345 + }
  346 + if (!this.widgetTypeInstance.onEditModeChanged) {
  347 + this.widgetTypeInstance.onEditModeChanged = () => {};
  348 + }
  349 + if (!this.widgetTypeInstance.onMobileModeChanged) {
  350 + this.widgetTypeInstance.onMobileModeChanged = () => {};
  351 + }
  352 + if (!this.widgetTypeInstance.onDestroy) {
  353 + this.widgetTypeInstance.onDestroy = () => {};
  354 + }
  355 +
  356 + this.initialize();
  357 + }
  358 +
  359 + private reInit() {
  360 + this.ngOnDestroy();
  361 + this.initialize();
  362 + // TODO:
  363 + }
  364 +
  365 + private initialize() {
  366 + this.configureDynamicWidgetComponent().subscribe(
  367 + () => {
  368 + this.dynamicWidgetComponent.loadingData = false;
  369 + },
  370 + (error) => {
  371 + // TODO:
  372 + }
  373 + );
  374 + }
  375 +
  376 + private destroyDynamicWidgetComponent() {
  377 + if (this.widgetContext.$containerParent) {
  378 + // @ts-ignore
  379 + removeResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
  380 + }
  381 + if (this.dynamicWidgetComponentRef) {
  382 + this.dynamicWidgetComponentRef.destroy();
  383 + }
  384 + if (this.dynamicWidgetComponentFactory) {
  385 + this.dynamicWidgetComponentFactoryService.destroyDynamicWidgetComponentFactory(this.dynamicWidgetComponentFactory);
  386 + }
  387 + }
  388 +
  389 + private handleWidgetException(e) {
  390 + console.error(e);
  391 + this.widgetErrorData = this.utils.processWidgetException(e);
  392 + if (this.dynamicWidgetComponent) {
  393 + this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData;
  394 + }
  395 + }
  396 +
  397 + private configureDynamicWidgetComponent(): Observable<any> {
  398 +
  399 + const dynamicWidgetComponentSubject = new ReplaySubject();
  400 +
  401 + let html = '<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">' +
  402 + '<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' +
  403 + '</div>' +
  404 + '<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">' +
  405 + '<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>' +
  406 + '</div>';
  407 +
  408 + let containerHtml = `<div id="container">${this.widgetInfo.templateHtml}</div>`;
  409 +
  410 + const displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
  411 + : this.widget.type === widgetType.timeseries;
  412 +
  413 + let legendConfig: LegendConfig;
  414 + let legendData: LegendData;
  415 + if (displayLegend) {
  416 + legendConfig = this.widget.config.legendConfig ||
  417 + {
  418 + position: LegendPosition.bottom,
  419 + showMin: false,
  420 + showMax: false,
  421 + showAvg: this.widget.type === widgetType.timeseries,
  422 + showTotal: false
  423 + };
  424 + legendData = {
  425 + keys: [],
  426 + data: []
  427 + };
  428 + let layoutType;
  429 + if (legendConfig.position === LegendPosition.top ||
  430 + legendConfig.position === LegendPosition.bottom) {
  431 + layoutType = 'column';
  432 + } else {
  433 + layoutType = 'row';
  434 + }
  435 + let legendStyle;
  436 + switch (legendConfig.position) {
  437 + case LegendPosition.top:
  438 + legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;';
  439 + break;
  440 + case LegendPosition.bottom:
  441 + legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;';
  442 + break;
  443 + case LegendPosition.left:
  444 + legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;';
  445 + break;
  446 + case LegendPosition.right:
  447 + legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;';
  448 + break;
  449 + }
  450 +
  451 + const legendHtml = `<tb-legend style="${legendStyle}" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>`;
  452 + containerHtml = `<div fxFlex id="widget-container">${containerHtml}</div>`;
  453 + html += `<div class="tb-absolute-fill" fxLayout="${layoutType}">`;
  454 + if (legendConfig.position === LegendPosition.top ||
  455 + legendConfig.position === LegendPosition.left) {
  456 + html += legendHtml;
  457 + html += containerHtml;
  458 + } else {
  459 + html += containerHtml;
  460 + html += legendHtml;
  461 + }
  462 + html += '</div>';
  463 + } else {
  464 + html += containerHtml;
  465 + }
  466 +
  467 + this.dynamicWidgetComponentFactoryService.createDynamicWidgetComponentFactory(html).subscribe(
  468 + (componentFactory) => {
  469 + this.dynamicWidgetComponentFactory = componentFactory;
  470 + this.widgetContentContainer.clear();
  471 + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.dynamicWidgetComponentFactory);
  472 + this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
  473 +
  474 + this.dynamicWidgetComponent.loadingData = true;
  475 + this.dynamicWidgetComponent.widgetContext = this.widgetContext;
  476 + this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData;
  477 + this.dynamicWidgetComponent.displayLegend = displayLegend;
  478 + this.dynamicWidgetComponent.legendConfig = legendConfig;
  479 + this.dynamicWidgetComponent.legendData = legendData;
  480 +
  481 + this.widgetContext.$scope = this.dynamicWidgetComponent;
  482 +
  483 + const containerElement = displayLegend ? $(this.elementRef.nativeElement.querySelector('#widget-container'))
  484 + : $(this.elementRef.nativeElement);
  485 +
  486 + this.widgetContext.$container = $('#container', containerElement);
  487 + this.widgetContext.$containerParent = $(containerElement);
  488 +
  489 + if (this.widgetSizeDetected) {
  490 + this.widgetContext.$container.css('height', this.widgetContext.height + 'px');
  491 + this.widgetContext.$container.css('width', this.widgetContext.width + 'px');
  492 + }
  493 +
  494 + // @ts-ignore
  495 + addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
  496 +
  497 + dynamicWidgetComponentSubject.next();
  498 + dynamicWidgetComponentSubject.complete();
  499 + },
  500 + (e) => {
  501 + dynamicWidgetComponentSubject.error(e);
  502 + }
  503 + );
  504 + return dynamicWidgetComponentSubject.asObservable();
  505 + }
  506 +
  507 + private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> {
  508 + // TODO:
  509 + return of(null);
  510 + }
  511 +
  512 + private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>,
  513 + options: WidgetSubscriptionOptions, useDefaultComponents: boolean,
  514 + subscribe: boolean): Observable<IWidgetSubscription> {
  515 + // TODO:
  516 + return of(null);
  517 + }
  518 +
  519 + private isNumeric(value: any): boolean {
  520 + return (value - parseFloat( value ) + 1) >= 0;
  521 + }
  522 +
  523 + private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
  524 + if (isDefined(value) &&
  525 + value != null && this.isNumeric(value)) {
  526 + let formatted: string | number = Number(value);
  527 + if (isDefined(dec)) {
  528 + formatted = formatted.toFixed(dec);
  529 + }
  530 + if (!showZeroDecimals) {
  531 + formatted = (Number(formatted) * 1);
  532 + }
  533 + formatted = formatted.toString();
  534 + if (isDefined(units) && units.length > 0) {
  535 + formatted += ' ' + units;
  536 + }
  537 + return formatted;
  538 + } else {
  539 + return value;
  540 + }
  541 + }
  542 +
  543 + private getActionDescriptors(actionSourceId: string): Array<WidgetActionDescriptor> {
  544 + let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
  545 + if (!result) {
  546 + result = [];
  547 + }
  548 + return result;
  549 + }
  550 +
  551 + private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor,
  552 + entityId?: EntityId, entityName?: string, additionalParams?: any): void {
  553 + const type = descriptor.type;
  554 + const targetEntityParamName = descriptor.stateEntityParamName;
  555 + let targetEntityId: EntityId;
  556 + if (descriptor.setEntityId) {
  557 + targetEntityId = entityId;
  558 + }
  559 + switch (type) {
  560 + case WidgetActionType.openDashboardState:
  561 + case WidgetActionType.updateDashboardState:
  562 + let targetDashboardStateId = descriptor.targetDashboardStateId;
  563 + const params = {...this.widgetContext.stateController.getStateParams()};
  564 + this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName);
  565 + if (type === WidgetActionType.openDashboardState) {
  566 + this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
  567 + } else {
  568 + this.widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout);
  569 + }
  570 + break;
  571 + case WidgetActionType.openDashboard:
  572 + const targetDashboardId = descriptor.targetDashboardId;
  573 + targetDashboardStateId = descriptor.targetDashboardStateId;
  574 + const stateObject: StateObject = {};
  575 + stateObject.params = {};
  576 + this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName);
  577 + if (targetDashboardStateId) {
  578 + stateObject.id = targetDashboardStateId;
  579 + }
  580 + const stateParams = {
  581 + dashboardId: targetDashboardId,
  582 + state: objToBase64([ stateObject ])
  583 + };
  584 + const state = objToBase64([ stateObject ]);
  585 + const currentUrl = this.route.snapshot.url;
  586 + let url;
  587 + if (currentUrl.length > 1) {
  588 + if (currentUrl[currentUrl.length - 2].path === 'dashboard') {
  589 + url = `/dashboard/${targetDashboardId}?state=${state}`;
  590 + } else {
  591 + url = `/dashboards/${targetDashboardId}?state=${state}`;
  592 + }
  593 + }
  594 + if (url) {
  595 + const urlTree = this.router.parseUrl(url);
  596 + this.router.navigateByUrl(url);
  597 + }
  598 + break;
  599 + case WidgetActionType.custom:
  600 + const customFunction = descriptor.customFunction;
  601 + if (isDefined(customFunction) && customFunction.length > 0) {
  602 + try {
  603 + if (!additionalParams) {
  604 + additionalParams = {};
  605 + }
  606 + const customActionFunction = new Function('$event', 'widgetContext', 'entityId',
  607 + 'entityName', 'additionalParams', customFunction);
  608 + customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams);
  609 + } catch (e) {
  610 + //
  611 + }
  612 + }
  613 + break;
  614 + case WidgetActionType.customPretty:
  615 + const customPrettyFunction = descriptor.customFunction;
  616 + const customHtml = descriptor.customHtml;
  617 + const customCss = descriptor.customCss;
  618 + const customResources = descriptor.customResources;
  619 + const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`;
  620 + let htmlTemplate = '';
  621 + if (isDefined(customHtml) && customHtml.length > 0) {
  622 + htmlTemplate = customHtml;
  623 + }
  624 + this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe(
  625 + () => {
  626 + if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) {
  627 + try {
  628 + if (!additionalParams) {
  629 + additionalParams = {};
  630 + }
  631 + const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId',
  632 + 'entityName', 'htmlTemplate', 'additionalParams', customPrettyFunction);
  633 + customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams);
  634 + } catch (e) {
  635 + //
  636 + }
  637 + }
  638 + },
  639 + (errorMessages: string[]) => {
  640 + this.processResourcesLoadErrors(errorMessages);
  641 + }
  642 + );
  643 + break;
  644 + }
  645 + }
  646 +
  647 + private elementClick($event: Event) {
  648 + $event.stopPropagation();
  649 + const e = ($event.target || $event.srcElement) as Element;
  650 + if (e.id) {
  651 + const descriptors = this.getActionDescriptors('elementClick');
  652 + if (descriptors.length) {
  653 + descriptors.forEach((descriptor) => {
  654 + if (descriptor.name === e.id) {
  655 + const entityInfo = this.getActiveEntityInfo();
  656 + const entityId = entityInfo ? entityInfo.entityId : null;
  657 + const entityName = entityInfo ? entityInfo.entityName : null;
  658 + this.handleWidgetAction(event, descriptor, entityId, entityName);
  659 + }
  660 + });
  661 + }
  662 + }
  663 + }
  664 +
  665 + private updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, entityName?: string) {
  666 + if (targetEntityId) {
  667 + let targetEntityParams: StateParams;
  668 + if (targetEntityParamName && targetEntityParamName.length) {
  669 + targetEntityParams = params[targetEntityParamName];
  670 + if (!targetEntityParams) {
  671 + targetEntityParams = {};
  672 + params[targetEntityParamName] = targetEntityParams;
  673 + params.targetEntityParamName = targetEntityParamName;
  674 + }
  675 + } else {
  676 + targetEntityParams = params;
  677 + }
  678 + targetEntityParams.entityId = targetEntityId;
  679 + if (entityName) {
  680 + targetEntityParams.entityName = entityName;
  681 + }
  682 + }
  683 + }
  684 +
  685 + private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> {
  686 + if (isDefined(customCss) && customCss.length > 0) {
  687 + this.cssParser.cssPreviewNamespace = actionNamespace;
  688 + this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace');
  689 + }
  690 + const resourceTasks: Observable<string>[] = [];
  691 + if (customResources.length > 0) {
  692 + customResources.forEach((resource) => {
  693 + resourceTasks.push(
  694 + this.resources.loadResource(resource.url).pipe(
  695 + catchError(e => of(`Failed to load custom action resource: '${resource.url}'`))
  696 + )
  697 + );
  698 + });
  699 + return forkJoin(resourceTasks).pipe(
  700 + switchMap(msgs => {
  701 + let errors: string[];
  702 + if (msgs && msgs.length) {
  703 + errors = msgs.filter(msg => msg && msg.length > 0);
  704 + }
  705 + if (errors && errors.length) {
  706 + return throwError(errors);
  707 + } else {
  708 + return of(null);
  709 + }
  710 + }
  711 + ));
  712 + } else {
  713 + return of(null);
  714 + }
  715 + }
  716 +
  717 + private processResourcesLoadErrors(errorMessages: string[]) {
  718 + let messageToShow = '';
  719 + errorMessages.forEach(error => {
  720 + messageToShow += `<div>${error}</div>`;
  721 + });
  722 + this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
  723 + }
  724 +
  725 + private getActiveEntityInfo(): EntityInfo {
  726 + let entityInfo = this.widgetContext.activeEntityInfo;
  727 + if (!entityInfo) {
  728 + for (const id of Object.keys(this.widgetContext.subscriptions)) {
  729 + const subscription = this.widgetContext.subscriptions[id];
  730 + entityInfo = subscription.getFirstEntityInfo();
  731 + if (entityInfo) {
  732 + break;
  733 + }
  734 + }
  735 + }
  736 + return entityInfo;
  737 + }
  738 +
  739 + private checkSize(): boolean {
  740 + const width = this.widgetContext.$containerParent.width();
  741 + const height = this.widgetContext.$containerParent.height();
  742 + let sizeChanged = false;
  743 +
  744 + if (!this.widgetContext.width || this.widgetContext.width !== width ||
  745 + !this.widgetContext.height || this.widgetContext.height !== height) {
  746 + if (width > 0 && height > 0) {
  747 + this.widgetContext.$container.css('height', height + 'px');
  748 + this.widgetContext.$container.css('width', width + 'px');
  749 + this.widgetContext.width = width;
  750 + this.widgetContext.height = height;
  751 + sizeChanged = true;
  752 + this.widgetSizeDetected = true;
  753 + }
  754 + }
  755 + return sizeChanged;
  756 + }
  757 +
  758 +}
... ...
... ... @@ -22,92 +22,33 @@ import { Timewindow } from '@shared/models/time/time.models';
22 22 import { Observable } from 'rxjs';
23 23 import { isDefined, isUndefined } from '@app/core/utils';
24 24 import { EventEmitter } from '@angular/core';
25   -
26   -export interface IAliasController {
27   - [key: string]: any | null;
28   - // TODO:
29   -}
  25 +import { EntityId } from '@app/shared/models/id/entity-id';
  26 +import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
30 27
31 28 export interface WidgetsData {
32 29 widgets: Array<Widget>;
33 30 widgetLayouts?: WidgetLayouts;
34 31 }
35 32
36   -export class DashboardConfig {
37   - widgetsData?: Observable<WidgetsData>;
38   - isEdit: boolean;
39   - isEditActionEnabled: boolean;
40   - isExportActionEnabled: boolean;
41   - isRemoveActionEnabled: boolean;
  33 +export interface DashboardCallbacks {
42 34 onEditWidget?: ($event: Event, widget: Widget) => void;
43 35 onExportWidget?: ($event: Event, widget: Widget) => void;
44 36 onRemoveWidget?: ($event: Event, widget: Widget) => void;
45 37 onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
46 38 onWidgetClicked?: ($event: Event, widget: Widget) => void;
47   - aliasController?: IAliasController;
48   - autofillHeight?: boolean;
49   - mobileAutofillHeight?: boolean;
50   - dashboardStyle?: {[klass: string]: any} | null;
51   - columns?: number;
52   - margins?: [number, number];
53   - dashboardTimewindow?: Timewindow;
54   - ignoreLoading?: boolean;
55   - dashboardClass?: string;
56   - mobileRowHeight?: number;
57   -
58   - private isMobileValue: boolean;
59   - private isMobileDisabledValue: boolean;
60   -
61   - private layoutChange = new EventEmitter();
62   - layoutChange$ = this.layoutChange.asObservable();
63   - layoutChangeTimeout = null;
64   -
65   - set isMobile(isMobile: boolean) {
66   - if (this.isMobileValue !== isMobile) {
67   - const changed = isDefined(this.isMobileValue);
68   - this.isMobileValue = isMobile;
69   - if (changed) {
70   - this.notifyLayoutChanged();
71   - }
72   - }
73   - }
74   - get isMobile(): boolean {
75   - return this.isMobileValue;
76   - }
77   -
78   - set isMobileDisabled(isMobileDisabled: boolean) {
79   - if (this.isMobileDisabledValue !== isMobileDisabled) {
80   - const changed = isDefined(this.isMobileDisabledValue);
81   - this.isMobileDisabledValue = isMobileDisabled;
82   - if (changed) {
83   - this.notifyLayoutChanged();
84   - }
85   - }
86   - }
87   - get isMobileDisabled(): boolean {
88   - return this.isMobileDisabledValue;
89   - }
90   -
91   - private notifyLayoutChanged() {
92   - if (this.layoutChangeTimeout) {
93   - clearTimeout(this.layoutChangeTimeout);
94   - }
95   - this.layoutChangeTimeout = setTimeout(() => {
96   - this.doNotifyLayoutChanged();
97   - }, 0);
98   - }
99   -
100   - private doNotifyLayoutChanged() {
101   - this.layoutChange.emit();
102   - this.layoutChangeTimeout = null;
103   - }
  39 + prepareDashboardContextMenu?: ($event: Event) => void;
  40 + prepareWidgetContextMenu?: ($event: Event, widget: Widget) => void;
104 41 }
105 42
106 43 export interface IDashboardComponent {
107   - options: DashboardConfig;
108 44 gridsterOpts: GridsterConfig;
109 45 gridster: GridsterComponent;
  46 + mobileAutofillHeight: boolean;
110 47 isMobileSize: boolean;
  48 + autofillHeight: boolean;
  49 + dashboardTimewindow: Timewindow;
  50 + aliasController: IAliasController;
  51 + stateController: IStateController;
111 52 }
112 53
113 54 export class DashboardWidget implements GridsterItem {
... ... @@ -262,7 +203,7 @@ export class DashboardWidget implements GridsterItem {
262 203 }
263 204
264 205 get rows(): number {
265   - if (this.dashboard.isMobileSize && !this.dashboard.options.mobileAutofillHeight) {
  206 + if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) {
266 207 let mobileHeight;
267 208 if (this.widgetLayout) {
268 209 mobileHeight = this.widgetLayout.mobileHeight;
... ... @@ -285,7 +226,7 @@ export class DashboardWidget implements GridsterItem {
285 226 }
286 227
287 228 set rows(rows: number) {
288   - if (!this.dashboard.isMobileSize && !this.dashboard.options.autofillHeight) {
  229 + if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) {
289 230 if (this.widgetLayout) {
290 231 this.widgetLayout.sizeY = rows;
291 232 } else {
... ...
... ... @@ -14,23 +14,75 @@
14 14 /// limitations under the License.
15 15 ///
16 16
  17 +import { ExceptionData } from '@shared/models/error.models';
  18 +import { IDashboardComponent } from '@home/models/dashboard-component.models';
  19 +import { WidgetActionDescriptor, WidgetConfig, WidgetConfigSettings, widgetType } from '@shared/models/widget.models';
  20 +import { Timewindow } from '@shared/models/time/time.models';
  21 +import {
  22 + EntityInfo,
  23 + IWidgetSubscription,
  24 + SubscriptionInfo,
  25 + WidgetSubscriptionOptions,
  26 + IStateController,
  27 + IAliasController,
  28 + TimewindowFunctions,
  29 + WidgetSubscriptionApi,
  30 + RpcApi,
  31 + WidgetActionsApi,
  32 + IWidgetUtils
  33 +} from '@core/api/widget-api.models';
  34 +import { Observable } from 'rxjs';
  35 +import { EntityId } from '@shared/models/id/entity-id';
  36 +
17 37 export interface IWidgetAction {
  38 + name: string;
18 39 icon: string;
19 40 onAction: ($event: Event) => void;
20 41 }
21 42
22 43 export interface WidgetHeaderAction extends IWidgetAction {
23 44 displayName: string;
  45 + descriptor: WidgetActionDescriptor;
24 46 }
25 47
26 48 export interface WidgetAction extends IWidgetAction {
27   - name: string;
28 49 show: boolean;
29 50 }
30 51
  52 +export interface IDynamicWidgetComponent {
  53 + widgetContext: WidgetContext;
  54 + widgetErrorData: ExceptionData;
  55 + loadingData: boolean;
  56 + [key: string]: any;
  57 +}
  58 +
31 59 export interface WidgetContext {
32   - widgetTitleTemplate?: string;
  60 + inited?: boolean;
  61 + $container?: any;
  62 + $containerParent?: any;
  63 + width?: number;
  64 + height?: number;
  65 + $scope?: IDynamicWidgetComponent;
33 66 hideTitlePanel?: boolean;
  67 + isEdit?: boolean;
  68 + isMobile?: boolean;
  69 + dashboard?: IDashboardComponent;
  70 + widgetConfig?: WidgetConfig;
  71 + settings?: WidgetConfigSettings;
  72 + units?: string;
  73 + decimals?: number;
  74 + subscriptions?: {[id: string]: IWidgetSubscription};
  75 + defaultSubscription?: IWidgetSubscription;
  76 + dashboardTimewindow?: Timewindow;
  77 + timewindowFunctions?: TimewindowFunctions;
  78 + subscriptionApi?: WidgetSubscriptionApi;
  79 + controlApi?: RpcApi;
  80 + utils?: IWidgetUtils;
  81 + actionsApi?: WidgetActionsApi;
  82 + stateController?: IStateController;
  83 + aliasController?: IAliasController;
  84 + activeEntityInfo?: EntityInfo;
  85 + widgetTitleTemplate?: string;
34 86 widgetTitle?: string;
35 87 customHeaderActions?: Array<WidgetHeaderAction>;
36 88 widgetActions?: Array<WidgetAction>;
... ...
... ... @@ -22,7 +22,7 @@ import {Authority} from '@shared/models/authority.enum';
22 22 import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
23 23 import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
24 24 import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
25   -import { BreadCrumbConfig } from '@shared/components/breadcrumb';
  25 +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
26 26 import { User } from '@shared/models/user.model';
27 27 import { Store } from '@ngrx/store';
28 28 import { AppState } from '@core/core.state';
... ... @@ -44,7 +44,9 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
44 44 }
45 45 }
46 46
47   -const routes: Routes = [
  47 +export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title);
  48 +
  49 +export const routes: Routes = [
48 50 {
49 51 path: 'widgets-bundles',
50 52 data: {
... ... @@ -72,7 +74,7 @@ const routes: Routes = [
72 74 auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
73 75 title: 'widget.widget-library',
74 76 breadcrumb: {
75   - labelFunction: ((route, translate) => route.data.widgetsBundle.title),
  77 + labelFunction: widgetTypesBreadcumbLabelFunction,
76 78 icon: 'now_widgets'
77 79 } as BreadCrumbConfig
78 80 },
... ...
... ... @@ -27,6 +27,12 @@
27 27 style="text-transform: uppercase; display: flex;"
28 28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
29 29 </section>
30   -<tb-dashboard [options]="dashboardOptions"></tb-dashboard>
  30 +<tb-dashboard [aliasController]="aliasController"
  31 + [widgetsData]="widgetsData"
  32 + [isEdit]="false"
  33 + [isEditActionEnabled]="true"
  34 + [isExportActionEnabled]="true"
  35 + [isRemoveActionEnabled]="!isReadOnly"
  36 + [callbacks]="dashboardCallbacks"></tb-dashboard>
31 37 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
32 38 </tb-footer-fab-buttons>
... ...
... ... @@ -24,14 +24,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 24 import { ActivatedRoute } from '@angular/router';
25 25 import { Authority } from '@shared/models/authority.enum';
26 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27   -import { Observable, of } from 'rxjs';
  27 +import { Observable } from 'rxjs';
28 28 import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models';
29 29 import { WidgetService } from '@core/http/widget.service';
30 30 import { map, share } from 'rxjs/operators';
31 31 import { DialogService } from '@core/services/dialog.service';
32   -import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
33 32 import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
34   -import { DashboardConfig } from '@home/models/dashboard-component.models';
  33 +import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models';
  34 +import { IAliasController } from '@app/core/api/widget-api.models';
35 35
36 36 @Component({
37 37 selector: 'tb-widget-library',
... ... @@ -69,19 +69,21 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
69 69 ]
70 70 };
71 71
72   - dashboardOptions: DashboardConfig = new DashboardConfig();
  72 + dashboardCallbacks: DashboardCallbacks = {
  73 + onEditWidget: this.openWidgetType.bind(this),
  74 + onExportWidget: this.exportWidgetType.bind(this),
  75 + onRemoveWidget: this.removeWidgetType.bind(this)
  76 + };
  77 +
  78 + widgetsData: Observable<WidgetsData>;
  79 +
  80 + aliasController: IAliasController = {};
73 81
74 82 constructor(protected store: Store<AppState>,
75 83 private route: ActivatedRoute,
76 84 private widgetService: WidgetService,
77 85 private dialogService: DialogService) {
78 86 super(store);
79   - this.dashboardOptions.isEdit = false;
80   - this.dashboardOptions.isEditActionEnabled = true;
81   - this.dashboardOptions.isExportActionEnabled = true;
82   - this.dashboardOptions.onEditWidget = ($event, widget) => { this.openWidgetType($event, widget); };
83   - this.dashboardOptions.onExportWidget = ($event, widget) => { this.exportWidgetType($event, widget); };
84   - this.dashboardOptions.onRemoveWidget = ($event, widget) => { this.removeWidgetType($event, widget); };
85 87
86 88 this.authUser = getCurrentAuthUser(store);
87 89 this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
... ... @@ -90,9 +92,8 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
90 92 } else {
91 93 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
92 94 }
93   - this.dashboardOptions.isRemoveActionEnabled = !this.isReadOnly;
94 95 this.loadWidgetTypes();
95   - this.dashboardOptions.widgetsData = this.widgetTypes$.pipe(
  96 + this.widgetsData = this.widgetTypes$.pipe(
96 97 map(widgets => ({ widgets })));
97 98 }
98 99
... ...
... ... @@ -113,3 +113,5 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
113 113 ]
114 114 ]
115 115 );
  116 +
  117 +export const customTranslationsPrefix = 'custom.';
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +
  18 +export interface ExceptionData {
  19 + message?: string;
  20 + name?: string;
  21 + lineNumber?: number;
  22 + columnNumber?: number;
  23 +}
... ...
... ... @@ -87,19 +87,50 @@ export interface WidgetResource {
87 87 url: string;
88 88 }
89 89
  90 +export interface WidgetActionSource {
  91 + name: string;
  92 + value: string;
  93 + multiple: boolean;
  94 +}
  95 +
  96 +export const widgetActionSources: {[key: string]: WidgetActionSource} = {
  97 + headerButton:
  98 + {
  99 + name: 'widget-action.header-button',
  100 + value: 'headerButton',
  101 + multiple: true,
  102 + }
  103 +};
  104 +
90 105 export interface WidgetTypeDescriptor {
91 106 type: widgetType;
92 107 resources: Array<WidgetResource>;
93 108 templateHtml: string;
94 109 templateCss: string;
95 110 controllerScript: string;
96   - settingsSchema: string;
97   - dataKeySettingsSchema: string;
  111 + settingsSchema?: string;
  112 + dataKeySettingsSchema?: string;
98 113 defaultConfig: string;
99 114 sizeX: number;
100 115 sizeY: number;
101 116 }
102 117
  118 +export interface WidgetTypeParameters {
  119 + useCustomDatasources?: boolean;
  120 + maxDatasources?: number;
  121 + maxDataKeys?: number;
  122 + dataKeysOptional?: boolean;
  123 + stateData?: boolean;
  124 +}
  125 +
  126 +export interface WidgetControllerDescriptor {
  127 + widgetTypeFunction?: any;
  128 + settingsSchema?: string;
  129 + dataKeySettingsSchema?: string;
  130 + typeParameters?: WidgetTypeParameters;
  131 + actionSources?: {[key: string]: WidgetActionSource};
  132 +}
  133 +
103 134 export interface WidgetType extends BaseData<WidgetTypeId> {
104 135 tenantId: TenantId;
105 136 bundleAlias: string;
... ... @@ -108,9 +139,67 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
108 139 descriptor: WidgetTypeDescriptor;
109 140 }
110 141
111   -export interface WidgetInfo extends WidgetTypeDescriptor {
  142 +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
112 143 widgetName: string;
113 144 alias: string;
  145 + typeSettingsSchema?: string;
  146 + typeDataKeySettingsSchema?: string;
  147 +}
  148 +
  149 +export const MissingWidgetType: WidgetInfo = {
  150 + type: widgetType.latest,
  151 + widgetName: 'Widget type not found',
  152 + alias: 'undefined',
  153 + sizeX: 8,
  154 + sizeY: 6,
  155 + resources: [],
  156 + templateHtml: '<div class="tb-widget-error-container">' +
  157 + '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' +
  158 + '</div>',
  159 + templateCss: '',
  160 + controllerScript: 'self.onInit = function() {}',
  161 + settingsSchema: '{}\n',
  162 + dataKeySettingsSchema: '{}\n',
  163 + defaultConfig: '{\n' +
  164 + '"title": "Widget type not found",\n' +
  165 + '"datasources": [],\n' +
  166 + '"settings": {}\n' +
  167 + '}\n'
  168 +};
  169 +
  170 +export const ErrorWidgetType: WidgetInfo = {
  171 + type: widgetType.latest,
  172 + widgetName: 'Error loading widget',
  173 + alias: 'error',
  174 + sizeX: 8,
  175 + sizeY: 6,
  176 + resources: [],
  177 + templateHtml: '<div class="tb-widget-error-container">' +
  178 + '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
  179 + templateCss: '',
  180 + controllerScript: 'self.onInit = function() {}',
  181 + settingsSchema: '{}\n',
  182 + dataKeySettingsSchema: '{}\n',
  183 + defaultConfig: '{\n' +
  184 + '"title": "Widget failed to load",\n' +
  185 + '"datasources": [],\n' +
  186 + '"settings": {}\n' +
  187 + '}\n'
  188 +};
  189 +
  190 +export interface WidgetTypeInstance {
  191 + getSettingsSchema?: () => string;
  192 + getDataKeySettingsSchema?: () => string;
  193 + typeParameters?: () => WidgetTypeParameters;
  194 + useCustomDatasources?: () => boolean;
  195 + actionSources?: () => {[key: string]: WidgetActionSource};
  196 +
  197 + onInit?: () => void;
  198 + onDataUpdated?: () => void;
  199 + onResize?: () => void;
  200 + onEditModeChanged?: () => void;
  201 + onMobileModeChanged?: () => void;
  202 + onDestroy?: () => void;
114 203 }
115 204
116 205 export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
... ... @@ -130,6 +219,107 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
130 219 };
131 220 }
132 221
  222 +export enum LegendDirection {
  223 + column = 'column',
  224 + row = 'row'
  225 +}
  226 +
  227 +export const legendDirectionTranslationMap = new Map<LegendDirection, string>(
  228 + [
  229 + [ LegendDirection.column, 'direction.column' ],
  230 + [ LegendDirection.row, 'direction.row' ]
  231 + ]
  232 +);
  233 +
  234 +export enum LegendPosition {
  235 + top = 'top',
  236 + bottom = 'bottom',
  237 + left = 'left',
  238 + right = 'right'
  239 +}
  240 +
  241 +export const legendPositionTranslationMap = new Map<LegendPosition, string>(
  242 + [
  243 + [ LegendPosition.top, 'position.top' ],
  244 + [ LegendPosition.bottom, 'position.bottom' ],
  245 + [ LegendPosition.left, 'position.left' ],
  246 + [ LegendPosition.right, 'position.right' ]
  247 + ]
  248 +);
  249 +
  250 +export interface LegendConfig {
  251 + position: LegendPosition;
  252 + direction?: LegendDirection;
  253 + showMin: boolean;
  254 + showMax: boolean;
  255 + showAvg: boolean;
  256 + showTotal: boolean;
  257 +}
  258 +
  259 +export interface DataKey {
  260 + label: string;
  261 + color: string;
  262 + hidden?: boolean;
  263 + [key: string]: any;
  264 + // TODO:
  265 +}
  266 +
  267 +export interface LegendKey {
  268 + dataKey: DataKey;
  269 + dataIndex: number;
  270 +}
  271 +
  272 +export interface LegendKeyData {
  273 + min: number;
  274 + max: number;
  275 + avg: number;
  276 + total: number;
  277 +}
  278 +
  279 +export interface LegendData {
  280 + keys: Array<LegendKey>;
  281 + data: Array<LegendKeyData>;
  282 +}
  283 +
  284 +export interface WidgetConfigSettings {
  285 + [key: string]: any;
  286 + // TODO:
  287 +}
  288 +
  289 +export enum WidgetActionType {
  290 + openDashboardState = 'openDashboardState',
  291 + updateDashboardState = 'updateDashboardState',
  292 + openDashboard = 'openDashboard',
  293 + custom = 'custom',
  294 + customPretty = 'customPretty'
  295 +}
  296 +
  297 +export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>(
  298 + [
  299 + [ WidgetActionType.openDashboardState, 'widget-action.open-dashboard-state' ],
  300 + [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ],
  301 + [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ],
  302 + [ WidgetActionType.custom, 'widget-action.custom' ],
  303 + [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ]
  304 + ]
  305 +);
  306 +
  307 +export interface WidgetActionDescriptor {
  308 + name: string;
  309 + icon: string;
  310 + displayName?: string;
  311 + type: WidgetActionType;
  312 + targetDashboardId?: string;
  313 + targetDashboardStateId?: string;
  314 + openRightLayout?: boolean;
  315 + setEntityId?: boolean;
  316 + stateEntityParamName?: string;
  317 + customFunction?: string;
  318 + customResources?: Array<WidgetResource>;
  319 + customHtml?: string;
  320 + customCss?: string;
  321 +}
  322 +
133 323 export interface WidgetConfig {
134 324 title?: string;
135 325 titleIcon?: string;
... ... @@ -141,6 +331,8 @@ export interface WidgetConfig {
141 331 enableFullscreen?: boolean;
142 332 useDashboardTimewindow?: boolean;
143 333 displayTimewindow?: boolean;
  334 + showLegend?: boolean;
  335 + legendConfig?: LegendConfig;
144 336 timewindow?: Timewindow;
145 337 mobileHeight?: number;
146 338 mobileOrder?: number;
... ... @@ -150,6 +342,10 @@ export interface WidgetConfig {
150 342 margin?: string;
151 343 widgetStyle?: {[klass: string]: any};
152 344 titleStyle?: {[klass: string]: any};
  345 + units?: string;
  346 + decimals?: number;
  347 + actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
  348 + settings?: WidgetConfigSettings;
153 349 [key: string]: any;
154 350
155 351 // TODO:
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +export type WindowMessageType = 'widgetException';
  18 +
  19 +export interface WindowMessage {
  20 + type: WindowMessageType;
  21 + data: any;
  22 +}
... ...