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,6 +30,8 @@
30 "src/styles.scss" 30 "src/styles.scss"
31 ], 31 ],
32 "scripts": [ 32 "scripts": [
  33 + "node_modules/javascript-detect-element-resize/detect-element-resize.js",
  34 + "node_modules/jquery/dist/jquery.min.js",
33 "node_modules/ace-builds/src-min/ace.js", 35 "node_modules/ace-builds/src-min/ace.js",
34 "node_modules/ace-builds/src-min/ext-language_tools.js", 36 "node_modules/ace-builds/src-min/ext-language_tools.js",
35 "node_modules/ace-builds/src-min/ext-searchbox.js", 37 "node_modules/ace-builds/src-min/ext-searchbox.js",
@@ -71,10 +73,10 @@ @@ -71,10 +73,10 @@
71 "sourceMap": false, 73 "sourceMap": false,
72 "extractCss": true, 74 "extractCss": true,
73 "namedChunks": false, 75 "namedChunks": false,
74 - "aot": true, 76 + "aot": false,
75 "extractLicenses": true, 77 "extractLicenses": true,
76 "vendorChunk": false, 78 "vendorChunk": false,
77 - "buildOptimizer": true, 79 + "buildOptimizer": false,
78 "budgets": [ 80 "budgets": [
79 { 81 {
80 "type": "initial", 82 "type": "initial",
@@ -1993,8 +1993,7 @@ @@ -1993,8 +1993,7 @@
1993 "base64-js": { 1993 "base64-js": {
1994 "version": "1.3.1", 1994 "version": "1.3.1",
1995 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 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 "base64id": { 1998 "base64id": {
2000 "version": "1.0.0", 1999 "version": "1.0.0",
@@ -6336,6 +6335,16 @@ @@ -6336,6 +6335,16 @@
6336 "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", 6335 "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
6337 "dev": true 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 "js-tokens": { 6348 "js-tokens": {
6340 "version": "3.0.2", 6349 "version": "3.0.2",
6341 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 6350 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -33,11 +33,14 @@ @@ -33,11 +33,14 @@
33 "@ngx-translate/http-loader": "^4.0.0", 33 "@ngx-translate/http-loader": "^4.0.0",
34 "ace-builds": "^1.4.5", 34 "ace-builds": "^1.4.5",
35 "angular-gridster2": "^8.1.0", 35 "angular-gridster2": "^8.1.0",
  36 + "base64-js": "^1.3.1",
36 "compass-sass-mixins": "^0.12.7", 37 "compass-sass-mixins": "^0.12.7",
37 "core-js": "^3.1.4", 38 "core-js": "^3.1.4",
38 "deep-equal": "^1.0.1", 39 "deep-equal": "^1.0.1",
39 "font-awesome": "^4.7.0", 40 "font-awesome": "^4.7.0",
40 "hammerjs": "^2.0.8", 41 "hammerjs": "^2.0.8",
  42 + "javascript-detect-element-resize": "^0.5.3",
  43 + "jquery": "^3.4.1",
41 "material-design-icons": "^3.0.1", 44 "material-design-icons": "^3.0.1",
42 "messageformat": "^2.3.0", 45 "messageformat": "^2.3.0",
43 "ngx-clipboard": "^12.2.0", 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,21 +16,46 @@
16 16
17 import {Injectable} from '@angular/core'; 17 import {Injectable} from '@angular/core';
18 import {defaultHttpOptions} from './http-utils'; 18 import {defaultHttpOptions} from './http-utils';
19 -import {Observable} from 'rxjs/index'; 19 +import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } from 'rxjs/index';
20 import {HttpClient} from '@angular/common/http'; 20 import {HttpClient} from '@angular/common/http';
21 import {PageLink} from '@shared/models/page/page-link'; 21 import {PageLink} from '@shared/models/page/page-link';
22 import {PageData} from '@shared/models/page/page-data'; 22 import {PageData} from '@shared/models/page/page-data';
23 import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; 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 @Injectable({ 40 @Injectable({
27 providedIn: 'root' 41 providedIn: 'root'
28 }) 42 })
29 export class WidgetService { 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 constructor( 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 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, 60 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
36 ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> { 61 ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> {
@@ -58,4 +83,266 @@ export class WidgetService { @@ -58,4 +83,266 @@ export class WidgetService {
58 defaultHttpOptions(ignoreLoading, ignoreErrors)); 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,6 +16,7 @@
16 16
17 import { BehaviorSubject, Observable, Subject } from 'rxjs'; 17 import { BehaviorSubject, Observable, Subject } from 'rxjs';
18 import { finalize, share } from 'rxjs/operators'; 18 import { finalize, share } from 'rxjs/operators';
  19 +import base64js from 'base64-js';
19 20
20 export function onParentScrollOrWindowResize(el: Node): Observable<Event> { 21 export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
21 const scrollSubject = new Subject<Event>(); 22 const scrollSubject = new Subject<Event>();
@@ -78,6 +79,38 @@ export function isDefined(value: any): boolean { @@ -78,6 +79,38 @@ export function isDefined(value: any): boolean {
78 return typeof value !== 'undefined'; 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 const scrollRegex = /(auto|scroll)/; 114 const scrollRegex = /(auto|scroll)/;
82 115
83 function parentNodes(node: Node, nodes: Node[]): Node[] { 116 function parentNodes(node: Node, nodes: Node[]): Node[] {
@@ -138,3 +171,126 @@ function easeInOut( @@ -138,3 +171,126 @@ function easeInOut(
138 (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime 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,15 +16,15 @@
16 16
17 --> 17 -->
18 <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" 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 <mat-spinner color="warn" mode="indeterminate" diameter="100"> 21 <mat-spinner color="warn" mode="indeterminate" diameter="100">
22 </mat-spinner> 22 </mat-spinner>
23 </div> 23 </div>
24 <div id="gridster-parent" 24 <div id="gridster-parent"
25 fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" 25 fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
26 (contextmenu)="openDashboardContextMenu($event)"> 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 <gridster #gridster id="gridster-child" [options]="gridsterOpts"> 28 <gridster #gridster id="gridster-child" [options]="gridsterOpts">
29 <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async"> 29 <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async">
30 <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" 30 <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
@@ -58,42 +58,42 @@ @@ -58,42 +58,42 @@
58 fxLayoutAlign="start center" 58 fxLayoutAlign="start center"
59 (mousedown)="$event.stopPropagation()"> 59 (mousedown)="$event.stopPropagation()">
60 <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions" 60 <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
61 - [fxShow]="!options.isEdit" 61 + [fxShow]="!isEdit"
62 (click)="action.onAction($event)" 62 (click)="action.onAction($event)"
63 matTooltip="{{ action.displayName }}" 63 matTooltip="{{ action.displayName }}"
64 matTooltipPosition="above"> 64 matTooltipPosition="above">
65 <mat-icon>{{ action.icon }}</mat-icon> 65 <mat-icon>{{ action.icon }}</mat-icon>
66 </button> 66 </button>
67 <button mat-button mat-icon-button *ngFor="let action of widget.widgetActions" 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 (click)="action.onAction($event)" 69 (click)="action.onAction($event)"
70 matTooltip="{{ action.name | translate }}" 70 matTooltip="{{ action.name | translate }}"
71 matTooltipPosition="above"> 71 matTooltipPosition="above">
72 <mat-icon>{{ action.icon }}</mat-icon> 72 <mat-icon>{{ action.icon }}</mat-icon>
73 </button> 73 </button>
74 <button mat-button mat-icon-button 74 <button mat-button mat-icon-button
75 - [fxShow]="!options.isEdit && widget.enableFullscreen" 75 + [fxShow]="!isEdit && widget.enableFullscreen"
76 (click)="widget.isFullscreen = !widget.isFullscreen" 76 (click)="widget.isFullscreen = !widget.isFullscreen"
77 matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" 77 matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
78 matTooltipPosition="above"> 78 matTooltipPosition="above">
79 <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 79 <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
80 </button> 80 </button>
81 <button mat-button mat-icon-button 81 <button mat-button mat-icon-button
82 - [fxShow]="options.isEditActionEnabled && !widget.isFullscreen" 82 + [fxShow]="isEditActionEnabled && !widget.isFullscreen"
83 (click)="editWidget($event, widget)" 83 (click)="editWidget($event, widget)"
84 matTooltip="{{ 'widget.edit' | translate }}" 84 matTooltip="{{ 'widget.edit' | translate }}"
85 matTooltipPosition="above"> 85 matTooltipPosition="above">
86 <mat-icon>edit</mat-icon> 86 <mat-icon>edit</mat-icon>
87 </button> 87 </button>
88 <button mat-button mat-icon-button 88 <button mat-button mat-icon-button
89 - [fxShow]="options.isExportActionEnabled && !widget.isFullscreen" 89 + [fxShow]="isExportActionEnabled && !widget.isFullscreen"
90 (click)="exportWidget($event, widget)" 90 (click)="exportWidget($event, widget)"
91 matTooltip="{{ 'widget.export' | translate }}" 91 matTooltip="{{ 'widget.export' | translate }}"
92 matTooltipPosition="above"> 92 matTooltipPosition="above">
93 <mat-icon>file_download</mat-icon> 93 <mat-icon>file_download</mat-icon>
94 </button> 94 </button>
95 <button mat-button mat-icon-button 95 <button mat-button mat-icon-button
96 - [fxShow]="options.isRemoveActionEnabled && !widget.isFullscreen" 96 + [fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
97 (click)="removeWidget($event, widget)" 97 (click)="removeWidget($event, widget)"
98 matTooltip="{{ 'widget.remove' | translate }}" 98 matTooltip="{{ 'widget.remove' | translate }}"
99 matTooltipPosition="above"> 99 matTooltipPosition="above">
@@ -102,7 +102,12 @@ @@ -102,7 +102,12 @@
102 </div> 102 </div>
103 </div> 103 </div>
104 <div fxFlex fxLayout="column" class="tb-widget-content"> 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 </div> 111 </div>
107 </div> 112 </div>
108 </gridster-item> 113 </gridster-item>
@@ -14,40 +14,108 @@ @@ -14,40 +14,108 @@
14 /// limitations under the License. 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 import { Store } from '@ngrx/store'; 28 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 29 import { AppState } from '@core/core.state';
20 import { PageComponent } from '@shared/components/page.component'; 30 import { PageComponent } from '@shared/components/page.component';
21 import { AuthUser } from '@shared/models/user.model'; 31 import { AuthUser } from '@shared/models/user.model';
22 import { getCurrentAuthUser } from '@core/auth/auth.selectors'; 32 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
23 -import { coerceBooleanProperty } from '@angular/cdk/coercion';  
24 import { Timewindow } from '@shared/models/time/time.models'; 33 import { Timewindow } from '@shared/models/time/time.models';
25 import { TimeService } from '@core/services/time.service'; 34 import { TimeService } from '@core/services/time.service';
26 import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2'; 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 import { map, share, tap } from 'rxjs/operators'; 43 import { map, share, tap } from 'rxjs/operators';
32 import { WidgetLayout } from '@shared/models/dashboard.models'; 44 import { WidgetLayout } from '@shared/models/dashboard.models';
33 import { DialogService } from '@core/services/dialog.service'; 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 import { animatedScroll, isDefined } from '@app/core/utils'; 46 import { animatedScroll, isDefined } from '@app/core/utils';
37 -import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 47 +import { BreakpointObserver } from '@angular/cdk/layout';
38 import { MediaBreakpoints } from '@shared/models/constants'; 48 import { MediaBreakpoints } from '@shared/models/constants';
  49 +import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
39 50
40 @Component({ 51 @Component({
41 selector: 'tb-dashboard', 52 selector: 'tb-dashboard',
42 templateUrl: './dashboard.component.html', 53 templateUrl: './dashboard.component.html',
43 styleUrls: ['./dashboard.component.scss'] 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 authUser: AuthUser; 58 authUser: AuthUser;
48 59
49 @Input() 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 gridsterOpts: GridsterConfig; 120 gridsterOpts: GridsterConfig;
53 121
@@ -77,8 +145,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -77,8 +145,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
77 } 145 }
78 146
79 ngOnInit(): void { 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 this.gridsterOpts = { 151 this.gridsterOpts = {
84 gridType: 'scrollVertical', 152 gridType: 'scrollVertical',
@@ -86,35 +154,65 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -86,35 +154,65 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
86 pushItems: false, 154 pushItems: false,
87 swap: false, 155 swap: false,
88 maxRows: 100, 156 maxRows: 100,
89 - minCols: this.options.columns ? this.options.columns : 24, 157 + minCols: this.columns ? this.columns : 24,
90 outerMargin: true, 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 minItemCols: 1, 163 minItemCols: 1,
96 minItemRows: 1, 164 minItemRows: 1,
97 defaultItemCols: 8, 165 defaultItemCols: 8,
98 defaultItemRows: 6, 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 this.loadDashboard(); 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 loadDashboard() { 214 loadDashboard() {
117 - this.widgets$ = this.options.widgetsData.pipe( 215 + this.widgets$ = this.widgetsData.pipe(
118 map(widgetsData => { 216 map(widgetsData => {
119 const dashboardWidgets = new Array<DashboardWidget>(); 217 const dashboardWidgets = new Array<DashboardWidget>();
120 let maxRows = this.gridsterOpts.maxRows; 218 let maxRows = this.gridsterOpts.maxRows;
@@ -164,19 +262,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -164,19 +262,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
164 262
165 isAutofillHeight(): boolean { 263 isAutofillHeight(): boolean {
166 if (this.isMobileSize) { 264 if (this.isMobileSize) {
167 - return isDefined(this.options.mobileAutofillHeight) ? this.options.mobileAutofillHeight : false; 265 + return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false;
168 } else { 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 openDashboardContextMenu($event: Event) { 271 openDashboardContextMenu($event: Event) {
181 // TODO: 272 // TODO:
182 // this.dialogService.todo(); 273 // this.dialogService.todo();
@@ -192,14 +283,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -192,14 +283,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
192 } 283 }
193 284
194 widgetMouseDown($event: Event, widget: DashboardWidget) { 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 widgetClicked($event: Event, widget: DashboardWidget) { 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,8 +298,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
207 if ($event) { 298 if ($event) {
208 $event.stopPropagation(); 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,8 +307,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
216 if ($event) { 307 if ($event) {
217 $event.stopPropagation(); 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,8 +316,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
225 if ($event) { 316 if ($event) {
226 $event.stopPropagation(); 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,7 +363,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
272 } 363 }
273 } 364 }
274 365
275 - private updateGridsterOpts() { 366 + private updateMobileOpts() {
276 this.isMobileSize = this.checkIsMobileSize(); 367 this.isMobileSize = this.checkIsMobileSize();
277 const mobileBreakPoint = this.isMobileSize ? 20000 : 0; 368 const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
278 this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; 369 this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
@@ -285,6 +376,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -285,6 +376,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
285 } else { 376 } else {
286 this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; 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 if (this.gridster && this.gridster.options) { 394 if (this.gridster && this.gridster.options) {
289 this.gridster.optionsChanged(); 395 this.gridster.optionsChanged();
290 } 396 }
@@ -294,15 +400,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -294,15 +400,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
294 let rowHeight = null; 400 let rowHeight = null;
295 if (!this.isAutofillHeight()) { 401 if (!this.isAutofillHeight()) {
296 if (isMobile) { 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 return rowHeight; 406 return rowHeight;
301 } 407 }
302 408
303 private checkIsMobileSize(): boolean { 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 if (!isMobileSize && !isMobileDisabled) { 412 if (!isMobileSize && !isMobileDisabled) {
307 isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); 413 isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
308 } 414 }
@@ -14,14 +14,14 @@ @@ -14,14 +14,14 @@
14 /// limitations under the License. 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 import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-dialog.component'; 25 import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-dialog.component';
26 import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; 26 import { AuditLogTableComponent } from './audit-log/audit-log-table.component';
27 import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; 27 import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
@@ -35,6 +35,8 @@ import { AttributeTableComponent } from '@home/components/attribute/attribute-ta @@ -35,6 +35,8 @@ import { AttributeTableComponent } from '@home/components/attribute/attribute-ta
35 import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component'; 35 import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
36 import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; 36 import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
37 import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; 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 @NgModule({ 41 @NgModule({
40 entryComponents: [ 42 entryComponents: [
@@ -66,7 +68,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone @@ -66,7 +68,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
66 AttributeTableComponent, 68 AttributeTableComponent,
67 AddAttributeDialogComponent, 69 AddAttributeDialogComponent,
68 EditAttributeValuePanelComponent, 70 EditAttributeValuePanelComponent,
69 - DashboardComponent 71 + DashboardComponent,
  72 + WidgetComponent
70 ], 73 ],
71 imports: [ 74 imports: [
72 CommonModule, 75 CommonModule,
@@ -84,7 +87,11 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone @@ -84,7 +87,11 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone
84 AlarmTableComponent, 87 AlarmTableComponent,
85 AlarmDetailsDialogComponent, 88 AlarmDetailsDialogComponent,
86 AttributeTableComponent, 89 AttributeTableComponent,
87 - DashboardComponent 90 + DashboardComponent,
  91 + WidgetComponent
  92 + ],
  93 + providers: [
  94 + DynamicWidgetComponentFactoryService
88 ] 95 ]
89 }) 96 })
90 export class HomeComponentsModule { } 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,92 +22,33 @@ import { Timewindow } from '@shared/models/time/time.models';
22 import { Observable } from 'rxjs'; 22 import { Observable } from 'rxjs';
23 import { isDefined, isUndefined } from '@app/core/utils'; 23 import { isDefined, isUndefined } from '@app/core/utils';
24 import { EventEmitter } from '@angular/core'; 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 export interface WidgetsData { 28 export interface WidgetsData {
32 widgets: Array<Widget>; 29 widgets: Array<Widget>;
33 widgetLayouts?: WidgetLayouts; 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 onEditWidget?: ($event: Event, widget: Widget) => void; 34 onEditWidget?: ($event: Event, widget: Widget) => void;
43 onExportWidget?: ($event: Event, widget: Widget) => void; 35 onExportWidget?: ($event: Event, widget: Widget) => void;
44 onRemoveWidget?: ($event: Event, widget: Widget) => void; 36 onRemoveWidget?: ($event: Event, widget: Widget) => void;
45 onWidgetMouseDown?: ($event: Event, widget: Widget) => void; 37 onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
46 onWidgetClicked?: ($event: Event, widget: Widget) => void; 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 export interface IDashboardComponent { 43 export interface IDashboardComponent {
107 - options: DashboardConfig;  
108 gridsterOpts: GridsterConfig; 44 gridsterOpts: GridsterConfig;
109 gridster: GridsterComponent; 45 gridster: GridsterComponent;
  46 + mobileAutofillHeight: boolean;
110 isMobileSize: boolean; 47 isMobileSize: boolean;
  48 + autofillHeight: boolean;
  49 + dashboardTimewindow: Timewindow;
  50 + aliasController: IAliasController;
  51 + stateController: IStateController;
111 } 52 }
112 53
113 export class DashboardWidget implements GridsterItem { 54 export class DashboardWidget implements GridsterItem {
@@ -262,7 +203,7 @@ export class DashboardWidget implements GridsterItem { @@ -262,7 +203,7 @@ export class DashboardWidget implements GridsterItem {
262 } 203 }
263 204
264 get rows(): number { 205 get rows(): number {
265 - if (this.dashboard.isMobileSize && !this.dashboard.options.mobileAutofillHeight) { 206 + if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) {
266 let mobileHeight; 207 let mobileHeight;
267 if (this.widgetLayout) { 208 if (this.widgetLayout) {
268 mobileHeight = this.widgetLayout.mobileHeight; 209 mobileHeight = this.widgetLayout.mobileHeight;
@@ -285,7 +226,7 @@ export class DashboardWidget implements GridsterItem { @@ -285,7 +226,7 @@ export class DashboardWidget implements GridsterItem {
285 } 226 }
286 227
287 set rows(rows: number) { 228 set rows(rows: number) {
288 - if (!this.dashboard.isMobileSize && !this.dashboard.options.autofillHeight) { 229 + if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) {
289 if (this.widgetLayout) { 230 if (this.widgetLayout) {
290 this.widgetLayout.sizeY = rows; 231 this.widgetLayout.sizeY = rows;
291 } else { 232 } else {
@@ -14,23 +14,75 @@ @@ -14,23 +14,75 @@
14 /// limitations under the License. 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 export interface IWidgetAction { 37 export interface IWidgetAction {
  38 + name: string;
18 icon: string; 39 icon: string;
19 onAction: ($event: Event) => void; 40 onAction: ($event: Event) => void;
20 } 41 }
21 42
22 export interface WidgetHeaderAction extends IWidgetAction { 43 export interface WidgetHeaderAction extends IWidgetAction {
23 displayName: string; 44 displayName: string;
  45 + descriptor: WidgetActionDescriptor;
24 } 46 }
25 47
26 export interface WidgetAction extends IWidgetAction { 48 export interface WidgetAction extends IWidgetAction {
27 - name: string;  
28 show: boolean; 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 export interface WidgetContext { 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 hideTitlePanel?: boolean; 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 widgetTitle?: string; 86 widgetTitle?: string;
35 customHeaderActions?: Array<WidgetHeaderAction>; 87 customHeaderActions?: Array<WidgetHeaderAction>;
36 widgetActions?: Array<WidgetAction>; 88 widgetActions?: Array<WidgetAction>;
@@ -22,7 +22,7 @@ import {Authority} from '@shared/models/authority.enum'; @@ -22,7 +22,7 @@ import {Authority} from '@shared/models/authority.enum';
22 import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; 22 import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver';
23 import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; 23 import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
24 import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; 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 import { User } from '@shared/models/user.model'; 26 import { User } from '@shared/models/user.model';
27 import { Store } from '@ngrx/store'; 27 import { Store } from '@ngrx/store';
28 import { AppState } from '@core/core.state'; 28 import { AppState } from '@core/core.state';
@@ -44,7 +44,9 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { @@ -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 path: 'widgets-bundles', 51 path: 'widgets-bundles',
50 data: { 52 data: {
@@ -72,7 +74,7 @@ const routes: Routes = [ @@ -72,7 +74,7 @@ const routes: Routes = [
72 auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], 74 auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
73 title: 'widget.widget-library', 75 title: 'widget.widget-library',
74 breadcrumb: { 76 breadcrumb: {
75 - labelFunction: ((route, translate) => route.data.widgetsBundle.title), 77 + labelFunction: widgetTypesBreadcumbLabelFunction,
76 icon: 'now_widgets' 78 icon: 'now_widgets'
77 } as BreadCrumbConfig 79 } as BreadCrumbConfig
78 }, 80 },
@@ -27,6 +27,12 @@ @@ -27,6 +27,12 @@
27 style="text-transform: uppercase; display: flex;" 27 style="text-transform: uppercase; display: flex;"
28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> 28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
29 </section> 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 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> 37 <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
32 </tb-footer-fab-buttons> 38 </tb-footer-fab-buttons>
@@ -24,14 +24,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -24,14 +24,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 import { ActivatedRoute } from '@angular/router'; 24 import { ActivatedRoute } from '@angular/router';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 import { NULL_UUID } from '@shared/models/id/has-uuid'; 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 -import { Observable, of } from 'rxjs'; 27 +import { Observable } from 'rxjs';
28 import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models'; 28 import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models';
29 import { WidgetService } from '@core/http/widget.service'; 29 import { WidgetService } from '@core/http/widget.service';
30 import { map, share } from 'rxjs/operators'; 30 import { map, share } from 'rxjs/operators';
31 import { DialogService } from '@core/services/dialog.service'; 31 import { DialogService } from '@core/services/dialog.service';
32 -import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';  
33 import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; 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 @Component({ 36 @Component({
37 selector: 'tb-widget-library', 37 selector: 'tb-widget-library',
@@ -69,19 +69,21 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -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 constructor(protected store: Store<AppState>, 82 constructor(protected store: Store<AppState>,
75 private route: ActivatedRoute, 83 private route: ActivatedRoute,
76 private widgetService: WidgetService, 84 private widgetService: WidgetService,
77 private dialogService: DialogService) { 85 private dialogService: DialogService) {
78 super(store); 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 this.authUser = getCurrentAuthUser(store); 88 this.authUser = getCurrentAuthUser(store);
87 this.widgetsBundle = this.route.snapshot.data.widgetsBundle; 89 this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
@@ -90,9 +92,8 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -90,9 +92,8 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
90 } else { 92 } else {
91 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; 93 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
92 } 94 }
93 - this.dashboardOptions.isRemoveActionEnabled = !this.isReadOnly;  
94 this.loadWidgetTypes(); 95 this.loadWidgetTypes();
95 - this.dashboardOptions.widgetsData = this.widgetTypes$.pipe( 96 + this.widgetsData = this.widgetTypes$.pipe(
96 map(widgets => ({ widgets }))); 97 map(widgets => ({ widgets })));
97 } 98 }
98 99
@@ -113,3 +113,5 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>( @@ -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,19 +87,50 @@ export interface WidgetResource {
87 url: string; 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 export interface WidgetTypeDescriptor { 105 export interface WidgetTypeDescriptor {
91 type: widgetType; 106 type: widgetType;
92 resources: Array<WidgetResource>; 107 resources: Array<WidgetResource>;
93 templateHtml: string; 108 templateHtml: string;
94 templateCss: string; 109 templateCss: string;
95 controllerScript: string; 110 controllerScript: string;
96 - settingsSchema: string;  
97 - dataKeySettingsSchema: string; 111 + settingsSchema?: string;
  112 + dataKeySettingsSchema?: string;
98 defaultConfig: string; 113 defaultConfig: string;
99 sizeX: number; 114 sizeX: number;
100 sizeY: number; 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 export interface WidgetType extends BaseData<WidgetTypeId> { 134 export interface WidgetType extends BaseData<WidgetTypeId> {
104 tenantId: TenantId; 135 tenantId: TenantId;
105 bundleAlias: string; 136 bundleAlias: string;
@@ -108,9 +139,67 @@ export interface WidgetType extends BaseData<WidgetTypeId> { @@ -108,9 +139,67 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
108 descriptor: WidgetTypeDescriptor; 139 descriptor: WidgetTypeDescriptor;
109 } 140 }
110 141
111 -export interface WidgetInfo extends WidgetTypeDescriptor { 142 +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
112 widgetName: string; 143 widgetName: string;
113 alias: string; 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 export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { 205 export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
@@ -130,6 +219,107 @@ 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 export interface WidgetConfig { 323 export interface WidgetConfig {
134 title?: string; 324 title?: string;
135 titleIcon?: string; 325 titleIcon?: string;
@@ -141,6 +331,8 @@ export interface WidgetConfig { @@ -141,6 +331,8 @@ export interface WidgetConfig {
141 enableFullscreen?: boolean; 331 enableFullscreen?: boolean;
142 useDashboardTimewindow?: boolean; 332 useDashboardTimewindow?: boolean;
143 displayTimewindow?: boolean; 333 displayTimewindow?: boolean;
  334 + showLegend?: boolean;
  335 + legendConfig?: LegendConfig;
144 timewindow?: Timewindow; 336 timewindow?: Timewindow;
145 mobileHeight?: number; 337 mobileHeight?: number;
146 mobileOrder?: number; 338 mobileOrder?: number;
@@ -150,6 +342,10 @@ export interface WidgetConfig { @@ -150,6 +342,10 @@ export interface WidgetConfig {
150 margin?: string; 342 margin?: string;
151 widgetStyle?: {[klass: string]: any}; 343 widgetStyle?: {[klass: string]: any};
152 titleStyle?: {[klass: string]: any}; 344 titleStyle?: {[klass: string]: any};
  345 + units?: string;
  346 + decimals?: number;
  347 + actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
  348 + settings?: WidgetConfigSettings;
153 [key: string]: any; 349 [key: string]: any;
154 350
155 // TODO: 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 +}