30 changed files with 3188 additions and 169 deletions
@ -0,0 +1,123 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { EntityId } from '@app/shared/models/id/entity-id'; |
||||
|
import { WidgetActionDescriptor, widgetType } from '@shared/models/widget.models'; |
||||
|
import { TimeService } from '../services/time.service'; |
||||
|
import { DeviceService } from '../http/device.service'; |
||||
|
import { AlarmService } from '../http/alarm.service'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
|
||||
|
export interface TimewindowFunctions { |
||||
|
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void; |
||||
|
onResetTimewindow: () => void; |
||||
|
} |
||||
|
|
||||
|
export interface WidgetSubscriptionApi { |
||||
|
createSubscription: (options: WidgetSubscriptionOptions, subscribe: boolean) => Observable<IWidgetSubscription>; |
||||
|
createSubscriptionFromInfo: (type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>, |
||||
|
options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean) |
||||
|
=> Observable<IWidgetSubscription>; |
||||
|
removeSubscription: (id: string) => void; |
||||
|
} |
||||
|
|
||||
|
export interface RpcApi { |
||||
|
sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>; |
||||
|
sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>; |
||||
|
} |
||||
|
|
||||
|
export interface IWidgetUtils { |
||||
|
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined; |
||||
|
} |
||||
|
|
||||
|
export interface WidgetActionsApi { |
||||
|
actionDescriptorsBySourceId: {[sourceId: string]: Array<WidgetActionDescriptor>}; |
||||
|
getActionDescriptors: (actionSourceId: string) => Array<WidgetActionDescriptor>; |
||||
|
handleWidgetAction: ($event: Event, descriptor: WidgetActionDescriptor, |
||||
|
entityId?: EntityId, entityName?: string, additionalParams?: any) => void; |
||||
|
elementClick: ($event: Event) => void; |
||||
|
} |
||||
|
|
||||
|
export interface IAliasController { |
||||
|
[key: string]: any | null; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface StateObject { |
||||
|
id?: string; |
||||
|
params?: StateParams; |
||||
|
} |
||||
|
|
||||
|
export interface StateParams { |
||||
|
entityName?: string; |
||||
|
targetEntityParamName?: string; |
||||
|
entityId?: EntityId; |
||||
|
[key: string]: any | null; |
||||
|
} |
||||
|
|
||||
|
export interface IStateController { |
||||
|
getStateParams: () => StateParams; |
||||
|
openState: (id: string, params?: StateParams, openRightLayout?: boolean) => void; |
||||
|
updateState: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface EntityInfo { |
||||
|
entityId: EntityId; |
||||
|
entityName: string; |
||||
|
} |
||||
|
|
||||
|
export interface SubscriptionInfo { |
||||
|
[key: string]: any; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface WidgetSubscriptionContext { |
||||
|
timeService: TimeService; |
||||
|
deviceService: DeviceService; |
||||
|
alarmService: AlarmService; |
||||
|
utils: UtilsService; |
||||
|
widgetUtils: IWidgetUtils; |
||||
|
dashboardTimewindowApi: TimewindowFunctions; |
||||
|
getServerTimeDiff: Observable<number>; |
||||
|
aliasController: IAliasController; |
||||
|
[key: string]: any; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface WidgetSubscriptionOptions { |
||||
|
[key: string]: any; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface IWidgetSubscription { |
||||
|
|
||||
|
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval: number) => void; |
||||
|
onResetTimewindow: () => void; |
||||
|
|
||||
|
sendOneWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>; |
||||
|
sendTwoWayCommand: (method: string, params?: any, timeout?: number) => Observable<any>; |
||||
|
|
||||
|
clearRpcError: () => void; |
||||
|
|
||||
|
getFirstEntityInfo: () => EntityInfo; |
||||
|
|
||||
|
destroy(): void; |
||||
|
|
||||
|
[key: string]: any; |
||||
|
// TODO:
|
||||
|
} |
||||
@ -0,0 +1,688 @@ |
|||||
|
/* |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
/* eslint-disable */ |
||||
|
|
||||
|
/* jshint unused:false */ |
||||
|
/* global base64_decode, CSSWizardView, window, console, jQuery */ |
||||
|
var fi = function () { |
||||
|
|
||||
|
this.cssImportStatements = []; |
||||
|
this.cssKeyframeStatements = []; |
||||
|
|
||||
|
this.cssRegex = new RegExp('([\\s\\S]*?){([\\s\\S]*?)}', 'gi'); |
||||
|
this.cssMediaQueryRegex = '((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})'; |
||||
|
this.cssKeyframeRegex = '((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})'; |
||||
|
this.combinedCSSRegex = '((\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})'; //to match css & media queries together
|
||||
|
this.cssCommentsRegex = '(\\/\\*[\\s\\S]*?\\*\\/)'; |
||||
|
this.cssImportStatementRegex = new RegExp('@import .*?;', 'gi'); |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Strip outs css comments and returns cleaned css string |
||||
|
|
||||
|
@param css, the original css string to be stipped out of comments |
||||
|
|
||||
|
@return cleanedCSS contains no css comments |
||||
|
*/ |
||||
|
fi.prototype.stripComments = function (cssString) { |
||||
|
var regex = new RegExp(this.cssCommentsRegex, 'gi'); |
||||
|
|
||||
|
return cssString.replace(regex, ''); |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Parses given css string, and returns css object |
||||
|
keys as selectors and values are css rules |
||||
|
eliminates all css comments before parsing |
||||
|
|
||||
|
@param source css string to be parsed |
||||
|
|
||||
|
@return object css |
||||
|
*/ |
||||
|
fi.prototype.parseCSS = function (source) { |
||||
|
|
||||
|
if (source === undefined) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
var css = []; |
||||
|
//strip out comments
|
||||
|
//source = this.stripComments(source);
|
||||
|
|
||||
|
//get import statements
|
||||
|
|
||||
|
while (true) { |
||||
|
var imports = this.cssImportStatementRegex.exec(source); |
||||
|
if (imports !== null) { |
||||
|
this.cssImportStatements.push(imports[0]); |
||||
|
css.push({ |
||||
|
selector: '@imports', |
||||
|
type: 'imports', |
||||
|
styles: imports[0] |
||||
|
}); |
||||
|
} else { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
source = source.replace(this.cssImportStatementRegex, ''); |
||||
|
//get keyframe statements
|
||||
|
var keyframesRegex = new RegExp(this.cssKeyframeRegex, 'gi'); |
||||
|
var arr; |
||||
|
while (true) { |
||||
|
arr = keyframesRegex.exec(source); |
||||
|
if (arr === null) { |
||||
|
break; |
||||
|
} |
||||
|
css.push({ |
||||
|
selector: '@keyframes', |
||||
|
type: 'keyframes', |
||||
|
styles: arr[0] |
||||
|
}); |
||||
|
} |
||||
|
source = source.replace(keyframesRegex, ''); |
||||
|
|
||||
|
//unified regex
|
||||
|
var unified = new RegExp(this.combinedCSSRegex, 'gi'); |
||||
|
|
||||
|
while (true) { |
||||
|
arr = unified.exec(source); |
||||
|
if (arr === null) { |
||||
|
break; |
||||
|
} |
||||
|
var selector = ''; |
||||
|
if (arr[2] === undefined) { |
||||
|
selector = arr[5].split('\r\n').join('\n').trim(); |
||||
|
} else { |
||||
|
selector = arr[2].split('\r\n').join('\n').trim(); |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
fetch comments and associate it with current selector |
||||
|
*/ |
||||
|
var commentsRegex = new RegExp(this.cssCommentsRegex, 'gi'); |
||||
|
var comments = commentsRegex.exec(selector); |
||||
|
if (comments !== null) { |
||||
|
selector = selector.replace(commentsRegex, '').trim(); |
||||
|
} |
||||
|
|
||||
|
//determine the type
|
||||
|
if (selector.indexOf('@media') !== -1) { |
||||
|
//we have a media query
|
||||
|
var cssObject = { |
||||
|
selector: selector, |
||||
|
type: 'media', |
||||
|
subStyles: this.parseCSS(arr[3] + '\n}') //recursively parse media query inner css
|
||||
|
}; |
||||
|
if (comments !== null) { |
||||
|
cssObject.comments = comments[0]; |
||||
|
} |
||||
|
css.push(cssObject); |
||||
|
} else { |
||||
|
//we have standart css
|
||||
|
var rules = this.parseRules(arr[6]); |
||||
|
var style = { |
||||
|
selector: selector, |
||||
|
rules: rules |
||||
|
}; |
||||
|
if (selector === '@font-face') { |
||||
|
style.type = 'font-face'; |
||||
|
} |
||||
|
if (comments !== null) { |
||||
|
style.comments = comments[0]; |
||||
|
} |
||||
|
css.push(style); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return css; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
parses given string containing css directives |
||||
|
and returns an array of objects containing ruleName:ruleValue pairs |
||||
|
|
||||
|
@param rules, css directive string example |
||||
|
\n\ncolor:white;\n font-size:18px;\n |
||||
|
*/ |
||||
|
fi.prototype.parseRules = function (rules) { |
||||
|
//convert all windows style line endings to unix style line endings
|
||||
|
rules = rules.split('\r\n').join('\n'); |
||||
|
var ret = []; |
||||
|
|
||||
|
// Split all rules but keep semicolon for base64 url data
|
||||
|
rules = rules.split(/;(?![^\(]*\))/); |
||||
|
|
||||
|
//proccess rules line by line
|
||||
|
for (var i = 0; i < rules.length; i++) { |
||||
|
var line = rules[i]; |
||||
|
|
||||
|
//determine if line is a valid css directive, ie color:white;
|
||||
|
line = line.trim(); |
||||
|
if (line.indexOf(':') !== -1) { |
||||
|
//line contains :
|
||||
|
line = line.split(':'); |
||||
|
var cssDirective = line[0].trim(); |
||||
|
var cssValue = line.slice(1).join(':').trim(); |
||||
|
|
||||
|
//more checks
|
||||
|
if (cssDirective.length < 1 || cssValue.length < 1) { |
||||
|
continue; //there is no css directive or value that is of length 1 or 0
|
||||
|
// PLAIN WRONG WHAT ABOUT margin:0; ?
|
||||
|
} |
||||
|
|
||||
|
//push rule
|
||||
|
ret.push({ |
||||
|
directive: cssDirective, |
||||
|
value: cssValue |
||||
|
}); |
||||
|
} else { |
||||
|
//if there is no ':', but what if it was mis splitted value which starts with base64
|
||||
|
if (line.trim().substr(0, 7) == 'base64,') { //hack :)
|
||||
|
ret[ret.length - 1].value += line.trim(); |
||||
|
} else { |
||||
|
//add rule, even if it is defective
|
||||
|
if (line.length > 0) { |
||||
|
ret.push({ |
||||
|
directive: '', |
||||
|
value: line, |
||||
|
defective: true |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ret; //we are done!
|
||||
|
}; |
||||
|
/* |
||||
|
just returns the rule having given directive |
||||
|
if not found returns false; |
||||
|
*/ |
||||
|
fi.prototype.findCorrespondingRule = function (rules, directive, value) { |
||||
|
if (value === undefined) { |
||||
|
value = false; |
||||
|
} |
||||
|
var ret = false; |
||||
|
for (var i = 0; i < rules.length; i++) { |
||||
|
if (rules[i].directive == directive) { |
||||
|
ret = rules[i]; |
||||
|
if (value === rules[i].value) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return ret; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Finds styles that have given selector, compress them, |
||||
|
and returns them |
||||
|
*/ |
||||
|
fi.prototype.findBySelector = function (cssObjectArray, selector, contains) { |
||||
|
if (contains === undefined) { |
||||
|
contains = false; |
||||
|
} |
||||
|
|
||||
|
var found = []; |
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
if (contains === false) { |
||||
|
if (cssObjectArray[i].selector === selector) { |
||||
|
found.push(cssObjectArray[i]); |
||||
|
} |
||||
|
} else { |
||||
|
if (cssObjectArray[i].selector.indexOf(selector) !== -1) { |
||||
|
found.push(cssObjectArray[i]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
if (found.length < 2) { |
||||
|
return found; |
||||
|
} else { |
||||
|
var base = found[0]; |
||||
|
for (i = 1; i < found.length; i++) { |
||||
|
this.intelligentCSSPush([base], found[i]); |
||||
|
} |
||||
|
return [base]; //we are done!! all properties merged into base!
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
deletes cssObjects having given selector, and returns new array |
||||
|
*/ |
||||
|
fi.prototype.deleteBySelector = function (cssObjectArray, selector) { |
||||
|
var ret = []; |
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
if (cssObjectArray[i].selector !== selector) { |
||||
|
ret.push(cssObjectArray[i]); |
||||
|
} |
||||
|
} |
||||
|
return ret; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Compresses given cssObjectArray and tries to minimize |
||||
|
selector redundence. |
||||
|
*/ |
||||
|
fi.prototype.compressCSS = function (cssObjectArray) { |
||||
|
var compressed = []; |
||||
|
var done = {}; |
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
var obj = cssObjectArray[i]; |
||||
|
if (done[obj.selector] === true) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var found = this.findBySelector(cssObjectArray, obj.selector); //found compressed
|
||||
|
if (found.length !== 0) { |
||||
|
compressed.push(found[0]); |
||||
|
done[obj.selector] = true; |
||||
|
} |
||||
|
} |
||||
|
return compressed; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Received 2 css objects with following structure |
||||
|
{ |
||||
|
rules : [{directive:"", value:""}, {directive:"", value:""}, ...] |
||||
|
selector : "SOMESELECTOR" |
||||
|
} |
||||
|
|
||||
|
returns the changed(new,removed,updated) values on css1 parameter, on same structure |
||||
|
|
||||
|
if two css objects are the same, then returns false |
||||
|
|
||||
|
if a css directive exists in css1 and css2, and its value is different, it is included in diff |
||||
|
if a css directive exists in css1 and not css2, it is then included in diff |
||||
|
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' |
||||
|
|
||||
|
@object css1 css object |
||||
|
@object css2 css object |
||||
|
|
||||
|
@return diff css object contains changed values in css1 in regards to css2 see test input output in /test/data/css.js |
||||
|
*/ |
||||
|
fi.prototype.cssDiff = function (css1, css2) { |
||||
|
if (css1.selector !== css2.selector) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
//if one of them is media query return false, because diff function can not operate on media queries
|
||||
|
if ((css1.type === 'media' || css2.type === 'media')) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var diff = { |
||||
|
selector: css1.selector, |
||||
|
rules: [] |
||||
|
}; |
||||
|
var rule1, rule2; |
||||
|
for (var i = 0; i < css1.rules.length; i++) { |
||||
|
rule1 = css1.rules[i]; |
||||
|
//find rule2 which has the same directive as rule1
|
||||
|
rule2 = this.findCorrespondingRule(css2.rules, rule1.directive, rule1.value); |
||||
|
if (rule2 === false) { |
||||
|
//rule1 is a new rule in css1
|
||||
|
diff.rules.push(rule1); |
||||
|
} else { |
||||
|
//rule2 was found only push if its value is different too
|
||||
|
if (rule1.value !== rule2.value) { |
||||
|
diff.rules.push(rule1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//now for rules exists in css2 but not in css1, which means deleted rules
|
||||
|
for (var ii = 0; ii < css2.rules.length; ii++) { |
||||
|
rule2 = css2.rules[ii]; |
||||
|
//find rule2 which has the same directive as rule1
|
||||
|
rule1 = this.findCorrespondingRule(css1.rules, rule2.directive); |
||||
|
if (rule1 === false) { |
||||
|
//rule1 is a new rule
|
||||
|
rule2.type = 'DELETED'; //mark it as a deleted rule, so that other merge operations could be true
|
||||
|
diff.rules.push(rule2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
if (diff.rules.length === 0) { |
||||
|
return false; |
||||
|
} |
||||
|
return diff; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Merges 2 different css objects together |
||||
|
using intelligentCSSPush, |
||||
|
|
||||
|
@param cssObjectArray, target css object array |
||||
|
@param newArray, source array that will be pushed into cssObjectArray parameter |
||||
|
@param reverse, [optional], if given true, first parameter will be traversed on reversed order |
||||
|
effectively giving priority to the styles in newArray |
||||
|
*/ |
||||
|
fi.prototype.intelligentMerge = function (cssObjectArray, newArray, reverse) { |
||||
|
if (reverse === undefined) { |
||||
|
reverse = false; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
for (var i = 0; i < newArray.length; i++) { |
||||
|
this.intelligentCSSPush(cssObjectArray, newArray[i], reverse); |
||||
|
} |
||||
|
for (i = 0; i < cssObjectArray.length; i++) { |
||||
|
var cobj = cssObjectArray[i]; |
||||
|
if (cobj.type === 'media' || (cobj.type === 'keyframes')) { |
||||
|
continue; |
||||
|
} |
||||
|
cobj.rules = this.compactRules(cobj.rules); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
inserts new css objects into a bigger css object |
||||
|
with same selectors groupped together |
||||
|
|
||||
|
@param cssObjectArray, array of bigger css object to be pushed into |
||||
|
@param minimalObject, single css object |
||||
|
@param reverse [optional] default is false, if given, cssObjectArray will be reversly traversed |
||||
|
resulting more priority in minimalObject's styles |
||||
|
*/ |
||||
|
fi.prototype.intelligentCSSPush = function (cssObjectArray, minimalObject, reverse) { |
||||
|
var pushSelector = minimalObject.selector; |
||||
|
//find correct selector if not found just push minimalObject into cssObject
|
||||
|
var cssObject = false; |
||||
|
|
||||
|
if (reverse === undefined) { |
||||
|
reverse = false; |
||||
|
} |
||||
|
|
||||
|
if (reverse === false) { |
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
if (cssObjectArray[i].selector === minimalObject.selector) { |
||||
|
cssObject = cssObjectArray[i]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
for (var j = cssObjectArray.length - 1; j > -1; j--) { |
||||
|
if (cssObjectArray[j].selector === minimalObject.selector) { |
||||
|
cssObject = cssObjectArray[j]; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (cssObject === false) { |
||||
|
cssObjectArray.push(minimalObject); //just push, because cssSelector is new
|
||||
|
} else { |
||||
|
if (minimalObject.type !== 'media') { |
||||
|
for (var ii = 0; ii < minimalObject.rules.length; ii++) { |
||||
|
var rule = minimalObject.rules[ii]; |
||||
|
//find rule inside cssObject
|
||||
|
var oldRule = this.findCorrespondingRule(cssObject.rules, rule.directive); |
||||
|
if (oldRule === false) { |
||||
|
cssObject.rules.push(rule); |
||||
|
} else if (rule.type == 'DELETED') { |
||||
|
oldRule.type = 'DELETED'; |
||||
|
} else { |
||||
|
//rule found just update value
|
||||
|
|
||||
|
oldRule.value = rule.value; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
cssObject.subStyles = minimalObject.subStyles; //TODO, make this intelligent too
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
filter outs rule objects whose type param equal to DELETED |
||||
|
|
||||
|
@param rules, array of rules |
||||
|
|
||||
|
@returns rules array, compacted by deleting all unneccessary rules |
||||
|
*/ |
||||
|
fi.prototype.compactRules = function (rules) { |
||||
|
var newRules = []; |
||||
|
for (var i = 0; i < rules.length; i++) { |
||||
|
if (rules[i].type !== 'DELETED') { |
||||
|
newRules.push(rules[i]); |
||||
|
} |
||||
|
} |
||||
|
return newRules; |
||||
|
}; |
||||
|
/* |
||||
|
computes string for ace editor using this.css or given cssBase optional parameter |
||||
|
|
||||
|
@param [optional] cssBase, if given computes cssString from cssObject array |
||||
|
*/ |
||||
|
fi.prototype.getCSSForEditor = function (cssBase, depth) { |
||||
|
if (depth === undefined) { |
||||
|
depth = 0; |
||||
|
} |
||||
|
var ret = ''; |
||||
|
if (cssBase === undefined) { |
||||
|
cssBase = this.css; |
||||
|
} |
||||
|
//append imports
|
||||
|
for (var i = 0; i < cssBase.length; i++) { |
||||
|
if (cssBase[i].type == 'imports') { |
||||
|
ret += cssBase[i].styles + '\n\n'; |
||||
|
} |
||||
|
} |
||||
|
for (i = 0; i < cssBase.length; i++) { |
||||
|
var tmp = cssBase[i]; |
||||
|
if (tmp.selector === undefined) { //temporarily omit media queries
|
||||
|
continue; |
||||
|
} |
||||
|
var comments = ""; |
||||
|
if (tmp.comments !== undefined) { |
||||
|
comments = tmp.comments + '\n'; |
||||
|
} |
||||
|
|
||||
|
if (tmp.type == 'media') { //also put media queries to output
|
||||
|
ret += comments + tmp.selector + '{\n'; |
||||
|
ret += this.getCSSForEditor(tmp.subStyles, depth + 1); |
||||
|
ret += '}\n\n'; |
||||
|
} else if (tmp.type !== 'keyframes' && tmp.type !== 'imports') { |
||||
|
ret += this.getSpaces(depth) + comments + tmp.selector + ' {\n'; |
||||
|
ret += this.getCSSOfRules(tmp.rules, depth + 1); |
||||
|
ret += this.getSpaces(depth) + '}\n\n'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//append keyFrames
|
||||
|
for (i = 0; i < cssBase.length; i++) { |
||||
|
if (cssBase[i].type == 'keyframes') { |
||||
|
ret += cssBase[i].styles + '\n\n'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ret; |
||||
|
}; |
||||
|
|
||||
|
fi.prototype.getImports = function (cssObjectArray) { |
||||
|
var imps = []; |
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
if (cssObjectArray[i].type == 'imports') { |
||||
|
imps.push(cssObjectArray[i].styles); |
||||
|
} |
||||
|
} |
||||
|
return imps; |
||||
|
}; |
||||
|
/* |
||||
|
given rules array, returns visually formatted css string |
||||
|
to be used inside editor |
||||
|
*/ |
||||
|
fi.prototype.getCSSOfRules = function (rules, depth) { |
||||
|
var ret = ''; |
||||
|
for (var i = 0; i < rules.length; i++) { |
||||
|
if (rules[i] === undefined) { |
||||
|
continue; |
||||
|
} |
||||
|
if (rules[i].defective === undefined) { |
||||
|
ret += this.getSpaces(depth) + rules[i].directive + ' : ' + rules[i].value + ';\n'; |
||||
|
} else { |
||||
|
ret += this.getSpaces(depth) + rules[i].value + ';\n'; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
return ret || '\n'; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
A very simple helper function returns number of spaces appended in a single string, |
||||
|
the number depends input parameter, namely input*2 |
||||
|
*/ |
||||
|
fi.prototype.getSpaces = function (num) { |
||||
|
var ret = ''; |
||||
|
for (var i = 0; i < num * 4; i++) { |
||||
|
ret += ' '; |
||||
|
} |
||||
|
return ret; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
Given css string or objectArray, parses it and then for every selector, |
||||
|
prepends this.cssPreviewNamespace to prevent css collision issues |
||||
|
|
||||
|
@returns css string in which this.cssPreviewNamespace prepended |
||||
|
*/ |
||||
|
fi.prototype.applyNamespacing = function (css, forcedNamespace) { |
||||
|
var cssObjectArray = css; |
||||
|
var namespaceClass = '.' + this.cssPreviewNamespace; |
||||
|
if (forcedNamespace !== undefined) { |
||||
|
namespaceClass = forcedNamespace; |
||||
|
} |
||||
|
|
||||
|
if (typeof css === 'string') { |
||||
|
cssObjectArray = this.parseCSS(css); |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
var obj = cssObjectArray[i]; |
||||
|
|
||||
|
//bypass namespacing for @font-face @keyframes @import
|
||||
|
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) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (obj.type !== 'media') { |
||||
|
var selector = obj.selector.split(','); |
||||
|
var newSelector = []; |
||||
|
for (var j = 0; j < selector.length; j++) { |
||||
|
if (selector[j].indexOf('.supernova') === -1) { //do not apply namespacing to selectors including supernova
|
||||
|
newSelector.push(namespaceClass + ' ' + selector[j]); |
||||
|
} else { |
||||
|
newSelector.push(selector[j]); |
||||
|
} |
||||
|
} |
||||
|
obj.selector = newSelector.join(','); |
||||
|
} else { |
||||
|
obj.subStyles = this.applyNamespacing(obj.subStyles, forcedNamespace); //handle media queries as well
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return cssObjectArray; |
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
given css string or object array, clears possible namespacing from |
||||
|
all of the selectors inside the css |
||||
|
*/ |
||||
|
fi.prototype.clearNamespacing = function (css, returnObj) { |
||||
|
if (returnObj === undefined) { |
||||
|
returnObj = false; |
||||
|
} |
||||
|
var cssObjectArray = css; |
||||
|
var namespaceClass = '.' + this.cssPreviewNamespace; |
||||
|
if (typeof css === 'string') { |
||||
|
cssObjectArray = this.parseCSS(css); |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < cssObjectArray.length; i++) { |
||||
|
var obj = cssObjectArray[i]; |
||||
|
|
||||
|
if (obj.type !== 'media') { |
||||
|
var selector = obj.selector.split(','); |
||||
|
var newSelector = []; |
||||
|
for (var j = 0; j < selector.length; j++) { |
||||
|
newSelector.push(selector[j].split(namespaceClass + ' ').join('')); |
||||
|
} |
||||
|
obj.selector = newSelector.join(','); |
||||
|
} else { |
||||
|
obj.subStyles = this.clearNamespacing(obj.subStyles, true); //handle media queries as well
|
||||
|
} |
||||
|
} |
||||
|
if (returnObj === false) { |
||||
|
return this.getCSSForEditor(cssObjectArray); |
||||
|
} else { |
||||
|
return cssObjectArray; |
||||
|
} |
||||
|
|
||||
|
}; |
||||
|
|
||||
|
/* |
||||
|
creates a new style tag (also destroys the previous one) |
||||
|
and injects given css string into that css tag |
||||
|
*/ |
||||
|
fi.prototype.createStyleElement = function (id, css, format) { |
||||
|
if (format === undefined) { |
||||
|
format = false; |
||||
|
} |
||||
|
|
||||
|
if (this.testMode === false && format !== 'nonamespace') { |
||||
|
//apply namespacing classes
|
||||
|
css = this.applyNamespacing(css); |
||||
|
} |
||||
|
|
||||
|
if (typeof css != 'string') { |
||||
|
css = this.getCSSForEditor(css); |
||||
|
} |
||||
|
//apply formatting for css
|
||||
|
if (format === true) { |
||||
|
css = this.getCSSForEditor(this.parseCSS(css)); |
||||
|
} |
||||
|
|
||||
|
if (this.testMode !== false) { |
||||
|
return this.testMode('create style #' + id, css); //if test mode, just pass result to callback
|
||||
|
} |
||||
|
|
||||
|
var __el = document.getElementById(id); |
||||
|
if (__el) { |
||||
|
__el.parentNode.removeChild(__el); |
||||
|
} |
||||
|
|
||||
|
var head = document.head || document.getElementsByTagName('head')[0], |
||||
|
style = document.createElement('style'); |
||||
|
|
||||
|
style.id = id; |
||||
|
style.type = 'text/css'; |
||||
|
|
||||
|
head.appendChild(style); |
||||
|
|
||||
|
if (style.styleSheet && !style.sheet) { |
||||
|
style.styleSheet.cssText = css; |
||||
|
} else { |
||||
|
style.appendChild(document.createTextNode(css)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default fi; |
||||
|
|
||||
|
/* eslint-enable */ |
||||
@ -0,0 +1,83 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable, Inject } from '@angular/core'; |
||||
|
import { DOCUMENT } from '@angular/common'; |
||||
|
import { ReplaySubject, Observable, throwError } from 'rxjs'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class ResourcesService { |
||||
|
|
||||
|
private loadedResources: { [url: string]: ReplaySubject<any> } = {}; |
||||
|
|
||||
|
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; |
||||
|
|
||||
|
constructor(@Inject(DOCUMENT) private readonly document: any) {} |
||||
|
|
||||
|
public loadResource(url: string): Observable<any> { |
||||
|
if (this.loadedResources[url]) { |
||||
|
return this.loadedResources[url].asObservable(); |
||||
|
} |
||||
|
|
||||
|
let fileType; |
||||
|
const match = /[.](css|less|html|htm|js)?((\?|#).*)?$/.exec(url); |
||||
|
if (match !== null) { |
||||
|
fileType = match[1]; |
||||
|
} |
||||
|
if (!fileType) { |
||||
|
return throwError(new Error(`Unable to detect file type from url: ${url}`)); |
||||
|
} else if (fileType !== 'css' && fileType !== 'js') { |
||||
|
return throwError(new Error(`Unsupported file type: ${fileType}`)); |
||||
|
} |
||||
|
return this.loadResourceByType(fileType, url); |
||||
|
} |
||||
|
|
||||
|
private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> { |
||||
|
const subject = new ReplaySubject(); |
||||
|
this.loadedResources[url] = subject; |
||||
|
let el; |
||||
|
let loaded = false; |
||||
|
switch (type) { |
||||
|
case 'js': |
||||
|
el = this.document.createElement('script'); |
||||
|
el.type = 'text/javascript'; |
||||
|
el.async = true; |
||||
|
el.src = url; |
||||
|
break; |
||||
|
case 'css': |
||||
|
el = this.document.createElement('link'); |
||||
|
el.type = 'text/css'; |
||||
|
el.rel = 'stylesheet'; |
||||
|
el.href = url; |
||||
|
break; |
||||
|
} |
||||
|
el.onload = el.onreadystatechange = (e) => { |
||||
|
if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; } |
||||
|
el.onload = el.onreadystatechange = null; |
||||
|
loaded = true; |
||||
|
this.loadedResources[url].next(); |
||||
|
this.loadedResources[url].complete(); |
||||
|
}; |
||||
|
el.onerror = () => { |
||||
|
this.loadedResources[url].error(new Error(`Unable to load ${url}`)); |
||||
|
delete this.loadedResources[url]; |
||||
|
}; |
||||
|
this.anchor.appendChild(el); |
||||
|
return subject.asObservable(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Inject, Injectable } from '@angular/core'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { WidgetInfo } from '@shared/models/widget.models'; |
||||
|
import { ExceptionData } from '@app/shared/models/error.models'; |
||||
|
import { isUndefined } from '@core/utils'; |
||||
|
import { WindowMessage } from '@shared/models/window-message.model'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { customTranslationsPrefix } from '@app/shared/models/constants'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class UtilsService { |
||||
|
|
||||
|
iframeMode = false; |
||||
|
widgetEditMode = false; |
||||
|
editWidgetInfo: WidgetInfo = null; |
||||
|
|
||||
|
constructor(@Inject(WINDOW) private window: Window, |
||||
|
private translate: TranslateService) { |
||||
|
let frame: Element = null; |
||||
|
try { |
||||
|
frame = window.frameElement; |
||||
|
} catch (e) { |
||||
|
// ie11 fix
|
||||
|
} |
||||
|
if (frame) { |
||||
|
this.iframeMode = true; |
||||
|
const dataWidgetAttr = frame.getAttribute('data-widget'); |
||||
|
if (dataWidgetAttr && dataWidgetAttr.length) { |
||||
|
this.editWidgetInfo = JSON.parse(dataWidgetAttr); |
||||
|
this.widgetEditMode = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public processWidgetException(exception: any): ExceptionData { |
||||
|
const data = this.parseException(exception, -5); |
||||
|
if (this.widgetEditMode) { |
||||
|
const message: WindowMessage = { |
||||
|
type: 'widgetException', |
||||
|
data |
||||
|
}; |
||||
|
this.window.parent.postMessage(message, '*'); |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
public parseException(exception: any, lineOffset?: number): ExceptionData { |
||||
|
const data: ExceptionData = {}; |
||||
|
if (exception) { |
||||
|
if (typeof exception === 'string') { |
||||
|
data.message = exception; |
||||
|
} else if (exception instanceof String) { |
||||
|
data.message = exception.toString(); |
||||
|
} else { |
||||
|
if (exception.name) { |
||||
|
data.name = exception.name; |
||||
|
} else { |
||||
|
data.name = 'UnknownError'; |
||||
|
} |
||||
|
if (exception.message) { |
||||
|
data.message = exception.message; |
||||
|
} |
||||
|
if (exception.lineNumber) { |
||||
|
data.lineNumber = exception.lineNumber; |
||||
|
if (exception.columnNumber) { |
||||
|
data.columnNumber = exception.columnNumber; |
||||
|
} |
||||
|
} else if (exception.stack) { |
||||
|
const lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g; |
||||
|
const lineInfoGroups = lineInfoRegexp.exec(exception.stack); |
||||
|
if (lineInfoGroups != null && lineInfoGroups.length >= 3) { |
||||
|
if (isUndefined(lineOffset)) { |
||||
|
lineOffset = -2; |
||||
|
} |
||||
|
data.lineNumber = Number(lineInfoGroups[2]) + lineOffset; |
||||
|
if (lineInfoGroups.length >= 5) { |
||||
|
data.columnNumber = Number(lineInfoGroups[4]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
public customTranslation(translationValue: string, defaultValue: string): string { |
||||
|
let result = ''; |
||||
|
const translationId = customTranslationsPrefix + translationValue; |
||||
|
const translation = this.translate.instant(translationId); |
||||
|
if (translation !== translationId) { |
||||
|
result = translation + ''; |
||||
|
} else { |
||||
|
result = defaultValue; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Compiler, |
||||
|
Component, |
||||
|
ComponentFactory, |
||||
|
Injectable, |
||||
|
Injector, |
||||
|
NgModule, |
||||
|
NgModuleRef, |
||||
|
Type, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
DynamicWidgetComponent, |
||||
|
DynamicWidgetComponentModule |
||||
|
} from '@home/components/widget/dynamic-widget.component'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { Observable, ReplaySubject } from 'rxjs'; |
||||
|
import { HomeComponentsModule } from '../home-components.module'; |
||||
|
import { WidgetComponentsModule } from './widget-components.module'; |
||||
|
|
||||
|
interface DynamicWidgetComponentModuleData { |
||||
|
moduleRef: NgModuleRef<DynamicWidgetComponentModule>; |
||||
|
moduleType: Type<DynamicWidgetComponentModule>; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class DynamicWidgetComponentFactoryService { |
||||
|
|
||||
|
private dynamicComponentModulesMap = new Map<ComponentFactory<DynamicWidgetComponent>, DynamicWidgetComponentModuleData>(); |
||||
|
|
||||
|
constructor(private compiler: Compiler, |
||||
|
private injector: Injector) { |
||||
|
} |
||||
|
|
||||
|
public createDynamicWidgetComponentFactory(template: string): Observable<ComponentFactory<DynamicWidgetComponent>> { |
||||
|
const dymamicWidgetComponentFactorySubject = new ReplaySubject<ComponentFactory<DynamicWidgetComponent>>(); |
||||
|
const comp = this.createDynamicWidgetComponent(template); |
||||
|
// noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent
|
||||
|
@NgModule({ |
||||
|
declarations: [comp], |
||||
|
entryComponents: [comp], |
||||
|
imports: [CommonModule, SharedModule, WidgetComponentsModule], |
||||
|
}) |
||||
|
class DynamicWidgetComponentInstanceModule extends DynamicWidgetComponentModule {} |
||||
|
this.compiler.compileModuleAsync(DynamicWidgetComponentInstanceModule).then( |
||||
|
(module) => { |
||||
|
const moduleRef = module.create(this.injector); |
||||
|
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); |
||||
|
this.dynamicComponentModulesMap.set(factory, { |
||||
|
moduleRef, |
||||
|
moduleType: module.moduleType |
||||
|
}); |
||||
|
dymamicWidgetComponentFactorySubject.next(factory); |
||||
|
dymamicWidgetComponentFactorySubject.complete(); |
||||
|
} |
||||
|
).catch( |
||||
|
(e) => { |
||||
|
dymamicWidgetComponentFactorySubject.error(`Failed to create dynamic widget component factory: ${e}`); |
||||
|
} |
||||
|
); |
||||
|
return dymamicWidgetComponentFactorySubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
public destroyDynamicWidgetComponentFactory(factory: ComponentFactory<DynamicWidgetComponent>) { |
||||
|
const moduleData = this.dynamicComponentModulesMap.get(factory); |
||||
|
if (moduleData) { |
||||
|
moduleData.moduleRef.destroy(); |
||||
|
this.compiler.clearCacheFor(moduleData.moduleType); |
||||
|
this.dynamicComponentModulesMap.delete(factory); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private createDynamicWidgetComponent(template: string): Type<DynamicWidgetComponent> { |
||||
|
// noinspection AngularMissingOrInvalidDeclarationInModule
|
||||
|
@Component({ |
||||
|
template |
||||
|
}) |
||||
|
class DynamicWidgetInstanceComponent extends DynamicWidgetComponent { } |
||||
|
|
||||
|
return DynamicWidgetInstanceComponent; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models'; |
||||
|
import { ExceptionData } from '@shared/models/error.models'; |
||||
|
|
||||
|
export abstract class DynamicWidgetComponentModule implements OnDestroy { |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
console.log('Module destroyed!'); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export abstract class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
widgetContext: WidgetContext; |
||||
|
|
||||
|
@Input() |
||||
|
widgetErrorData: ExceptionData; |
||||
|
|
||||
|
@Input() |
||||
|
loadingData: boolean; |
||||
|
|
||||
|
[key: string]: any; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
console.log('Component destroyed!'); |
||||
|
} |
||||
|
|
||||
|
clearRpcError() { |
||||
|
if (this.widgetContext.defaultSubscription) { |
||||
|
this.widgetContext.defaultSubscription.clearRpcError(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<table class="tb-legend"> |
||||
|
<thead> |
||||
|
<tr class="tb-legend-header" *ngIf="!isRowDirection"> |
||||
|
<th colspan="2"></th> |
||||
|
<th *ngIf="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th> |
||||
|
<th *ngIf="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th> |
||||
|
<th *ngIf="legendConfig.showAvg === true">{{ 'legend.avg' | translate }}</th> |
||||
|
<th *ngIf="legendConfig.showTotal === true">{{ 'legend.total' | translate }}</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr class="tb-legend-keys" *ngFor="let legendKey of legendData.keys" [ngClass]="{ 'tb-row-direction': isRowDirection }"> |
||||
|
<td><span class="tb-legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span></td> |
||||
|
<td class="tb-legend-label" |
||||
|
(click)="toggleHideData(legendKey.dataIndex)" |
||||
|
[ngClass]="{ 'tb-hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden, 'tb-horizontal': isHorizontal }"> |
||||
|
{{ legendKey.dataKey.label }} |
||||
|
</td> |
||||
|
<td class="tb-legend-value" *ngIf="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td> |
||||
|
<td class="tb-legend-value" *ngIf="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td> |
||||
|
<td class="tb-legend-value" *ngIf="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td> |
||||
|
<td class="tb-legend-value" *ngIf="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
@ -0,0 +1,68 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
table.tb-legend { |
||||
|
width: 100%; |
||||
|
font-size: 12px; |
||||
|
|
||||
|
.tb-legend-header, |
||||
|
.tb-legend-value { |
||||
|
text-align: right; |
||||
|
} |
||||
|
|
||||
|
.tb-legend-header { |
||||
|
th { |
||||
|
padding: 0 10px 1px 0; |
||||
|
color: rgb(255, 110, 64); |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-legend-keys { |
||||
|
td.tb-legend-label, |
||||
|
td.tb-legend-value { |
||||
|
padding: 2px 10px; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.tb-legend-line { |
||||
|
display: inline-block; |
||||
|
width: 15px; |
||||
|
height: 3px; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.tb-legend-label { |
||||
|
text-align: left; |
||||
|
outline: none; |
||||
|
|
||||
|
&.tb-horizontal { |
||||
|
width: 95%; |
||||
|
} |
||||
|
|
||||
|
&.tb-hidden-label { |
||||
|
text-decoration: line-through; |
||||
|
opacity: .6; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.tb-row-direction { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Input, OnInit } from '@angular/core'; |
||||
|
import { LegendConfig, LegendData, LegendDirection, LegendPosition } from '@shared/models/widget.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-legend', |
||||
|
templateUrl: './legend.component.html', |
||||
|
styleUrls: ['./legend.component.scss'] |
||||
|
}) |
||||
|
export class LegendComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
legendConfig: LegendConfig; |
||||
|
|
||||
|
@Input() |
||||
|
legendData: LegendData; |
||||
|
|
||||
|
displayHeader: boolean; |
||||
|
|
||||
|
isHorizontal: boolean; |
||||
|
|
||||
|
isRowDirection: boolean; |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.displayHeader = this.legendConfig.showMin === true || |
||||
|
this.legendConfig.showMax === true || |
||||
|
this.legendConfig.showAvg === true || |
||||
|
this.legendConfig.showTotal === true; |
||||
|
|
||||
|
this.isHorizontal = this.legendConfig.position === LegendPosition.bottom || |
||||
|
this.legendConfig.position === LegendPosition.top; |
||||
|
|
||||
|
this.isRowDirection = this.legendConfig.direction === LegendDirection.row; |
||||
|
} |
||||
|
|
||||
|
toggleHideData(index: number) { |
||||
|
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@app/shared/shared.module'; |
||||
|
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component'; |
||||
|
import { LegendComponent } from '@home/components/widget/legend.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
entryComponents: [ |
||||
|
], |
||||
|
declarations: |
||||
|
[ |
||||
|
LegendComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule |
||||
|
], |
||||
|
exports: [ |
||||
|
LegendComponent |
||||
|
] |
||||
|
}) |
||||
|
export class WidgetComponentsModule { } |
||||
@ -0,0 +1,18 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<ng-container #widgetContent></ng-container> |
||||
@ -0,0 +1,47 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
.tb-widget { |
||||
|
.tb-widget-error { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(255, 255, 255, .5); |
||||
|
|
||||
|
span { |
||||
|
color: #f00; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-widget-loading { |
||||
|
z-index: 3; |
||||
|
background: rgba(255, 255, 255, .15); |
||||
|
} |
||||
|
|
||||
|
.tb-widget-error-container { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
.tb-widget-error-msg { |
||||
|
padding: 5px; |
||||
|
font-size: 16px; |
||||
|
color: #f00; |
||||
|
word-wrap: break-word; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,758 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ComponentFactory, |
||||
|
ComponentFactoryResolver, |
||||
|
ComponentRef, |
||||
|
ElementRef, |
||||
|
Injector, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
SimpleChanges, |
||||
|
ViewChild, |
||||
|
ViewContainerRef, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models'; |
||||
|
import { |
||||
|
LegendConfig, |
||||
|
LegendData, |
||||
|
LegendPosition, |
||||
|
Widget, |
||||
|
WidgetActionDescriptor, |
||||
|
WidgetActionType, |
||||
|
WidgetInfo, WidgetResource, |
||||
|
widgetType, |
||||
|
WidgetTypeInstance, |
||||
|
widgetActionSources |
||||
|
} from '@shared/models/widget.models'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { WidgetService } from '@core/http/widget.service'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; |
||||
|
import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; |
||||
|
import { DynamicWidgetComponentFactoryService } from '@home/components/widget/dynamic-widget-component-factory.service'; |
||||
|
import { isDefined, objToBase64 } from '@core/utils'; |
||||
|
import * as $ from 'jquery'; |
||||
|
import { WidgetContext, WidgetHeaderAction } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
EntityInfo, |
||||
|
IWidgetSubscription, |
||||
|
SubscriptionInfo, |
||||
|
WidgetSubscriptionOptions, |
||||
|
StateObject, |
||||
|
StateParams, |
||||
|
WidgetSubscriptionContext |
||||
|
} from '@core/api/widget-api.models'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { ActivatedRoute, Router, UrlSegment } from '@angular/router'; |
||||
|
import cssjs from '@core/css/css'; |
||||
|
import { ResourcesService } from '@core/services/resources.service'; |
||||
|
import { catchError, switchMap } from 'rxjs/operators'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
import { DeviceService } from '@app/core/http/device.service'; |
||||
|
import { AlarmService } from '@app/core/http/alarm.service'; |
||||
|
import { ExceptionData } from '@shared/models/error.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-widget', |
||||
|
templateUrl: './widget.component.html', |
||||
|
styleUrls: ['./widget.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
isEdit: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
isMobile: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
dashboard: IDashboardComponent; |
||||
|
|
||||
|
@Input() |
||||
|
dashboardWidget: DashboardWidget; |
||||
|
|
||||
|
@ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef; |
||||
|
|
||||
|
widget: Widget; |
||||
|
widgetInfo: WidgetInfo; |
||||
|
widgetContext: WidgetContext; |
||||
|
widgetType: any; |
||||
|
widgetTypeInstance: WidgetTypeInstance; |
||||
|
widgetErrorData: ExceptionData; |
||||
|
|
||||
|
dynamicWidgetComponentFactory: ComponentFactory<DynamicWidgetComponent>; |
||||
|
dynamicWidgetComponentRef: ComponentRef<DynamicWidgetComponent>; |
||||
|
dynamicWidgetComponent: DynamicWidgetComponent; |
||||
|
|
||||
|
subscriptionContext: WidgetSubscriptionContext; |
||||
|
|
||||
|
subscriptionInited = false; |
||||
|
widgetSizeDetected = false; |
||||
|
|
||||
|
onResizeListener = this.onResize.bind(this); |
||||
|
|
||||
|
private cssParser = new cssjs(); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private route: ActivatedRoute, |
||||
|
private router: Router, |
||||
|
private dynamicWidgetComponentFactoryService: DynamicWidgetComponentFactoryService, |
||||
|
private componentFactoryResolver: ComponentFactoryResolver, |
||||
|
private elementRef: ElementRef, |
||||
|
private injector: Injector, |
||||
|
private widgetService: WidgetService, |
||||
|
private resources: ResourcesService, |
||||
|
private timeService: TimeService, |
||||
|
private deviceService: DeviceService, |
||||
|
private alarmService: AlarmService, |
||||
|
private utils: UtilsService) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.widget = this.dashboardWidget.widget; |
||||
|
|
||||
|
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {}; |
||||
|
if (this.widget.config.actions) { |
||||
|
for (const actionSourceId of Object.keys(this.widget.config.actions)) { |
||||
|
const descriptors = this.widget.config.actions[actionSourceId]; |
||||
|
const actionDescriptors: Array<WidgetActionDescriptor> = []; |
||||
|
descriptors.forEach((descriptor) => { |
||||
|
const actionDescriptor: WidgetActionDescriptor = {...descriptor}; |
||||
|
actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name); |
||||
|
actionDescriptors.push(actionDescriptor); |
||||
|
}); |
||||
|
actionDescriptorsBySourceId[actionSourceId] = actionDescriptors; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.widgetContext = this.dashboardWidget.widgetContext; |
||||
|
this.widgetContext.inited = false; |
||||
|
this.widgetContext.hideTitlePanel = false; |
||||
|
this.widgetContext.isEdit = this.isEdit; |
||||
|
this.widgetContext.isMobile = this.isMobile; |
||||
|
this.widgetContext.dashboard = this.dashboard; |
||||
|
this.widgetContext.widgetConfig = this.widget.config; |
||||
|
this.widgetContext.settings = this.widget.config.settings; |
||||
|
this.widgetContext.units = this.widget.config.units || ''; |
||||
|
this.widgetContext.decimals = isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; |
||||
|
this.widgetContext.subscriptions = {}; |
||||
|
this.widgetContext.defaultSubscription = null; |
||||
|
this.widgetContext.dashboardTimewindow = this.dashboard.dashboardTimewindow; |
||||
|
this.widgetContext.timewindowFunctions = { |
||||
|
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { |
||||
|
if (this.widgetContext.defaultSubscription) { |
||||
|
this.widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); |
||||
|
} |
||||
|
}, |
||||
|
onResetTimewindow: () => { |
||||
|
if (this.widgetContext.defaultSubscription) { |
||||
|
this.widgetContext.defaultSubscription.onResetTimewindow(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
this.widgetContext.subscriptionApi = { |
||||
|
createSubscription: this.createSubscription.bind(this), |
||||
|
createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this), |
||||
|
removeSubscription: (id) => { |
||||
|
const subscription = this.widgetContext.subscriptions[id]; |
||||
|
if (subscription) { |
||||
|
subscription.destroy(); |
||||
|
delete this.widgetContext.subscriptions[id]; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
this.widgetContext.controlApi = { |
||||
|
sendOneWayCommand: (method, params, timeout) => { |
||||
|
if (this.widgetContext.defaultSubscription) { |
||||
|
return this.widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
}, |
||||
|
sendTwoWayCommand: (method, params, timeout) => { |
||||
|
if (this.widgetContext.defaultSubscription) { |
||||
|
return this.widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
this.widgetContext.utils = { |
||||
|
formatValue: this.formatValue |
||||
|
}; |
||||
|
this.widgetContext.actionsApi = { |
||||
|
actionDescriptorsBySourceId, |
||||
|
getActionDescriptors: this.getActionDescriptors.bind(this), |
||||
|
handleWidgetAction: this.handleWidgetAction.bind(this), |
||||
|
elementClick: this.elementClick.bind(this) |
||||
|
}; |
||||
|
this.widgetContext.stateController = this.dashboard.stateController; |
||||
|
this.widgetContext.aliasController = this.dashboard.aliasController; |
||||
|
|
||||
|
this.widgetContext.customHeaderActions = []; |
||||
|
const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); |
||||
|
headerActionsDescriptors.forEach((descriptor) => { |
||||
|
const headerAction: WidgetHeaderAction = { |
||||
|
name: descriptor.name, |
||||
|
displayName: descriptor.displayName, |
||||
|
icon: descriptor.icon, |
||||
|
descriptor, |
||||
|
onAction: $event => { |
||||
|
const entityInfo = this.getActiveEntityInfo(); |
||||
|
const entityId = entityInfo ? entityInfo.entityId : null; |
||||
|
const entityName = entityInfo ? entityInfo.entityName : null; |
||||
|
this.handleWidgetAction($event, descriptor, entityId, entityName); |
||||
|
} |
||||
|
}; |
||||
|
this.widgetContext.customHeaderActions.push(headerAction); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
this.subscriptionContext = { |
||||
|
timeService: this.timeService, |
||||
|
deviceService: this.deviceService, |
||||
|
alarmService: this.alarmService, |
||||
|
utils: this.utils, |
||||
|
widgetUtils: this.widgetContext.utils, |
||||
|
dashboardTimewindowApi: null, // TODO:
|
||||
|
getServerTimeDiff: null, // TODO:
|
||||
|
aliasController: this.dashboard.aliasController |
||||
|
}; |
||||
|
|
||||
|
this.widgetService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( |
||||
|
(widgetInfo) => { |
||||
|
this.widgetInfo = widgetInfo; |
||||
|
this.loadFromWidgetInfo(); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
|
||||
|
for (const id of Object.keys(this.widgetContext.subscriptions)) { |
||||
|
const subscription = this.widgetContext.subscriptions[id]; |
||||
|
subscription.destroy(); |
||||
|
} |
||||
|
this.subscriptionInited = false; |
||||
|
this.widgetContext.subscriptions = {}; |
||||
|
if (this.widgetContext.inited) { |
||||
|
this.widgetContext.inited = false; |
||||
|
// TODO:
|
||||
|
try { |
||||
|
this.widgetTypeInstance.onDestroy(); |
||||
|
} catch (e) { |
||||
|
this.handleWidgetException(e); |
||||
|
} |
||||
|
} |
||||
|
this.destroyDynamicWidgetComponent(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (propName === 'isEdit') { |
||||
|
console.log(`isEdit changed: ${this.isEdit}`); |
||||
|
this.onEditModeChanged(); |
||||
|
} else if (propName === 'isMobile') { |
||||
|
console.log(`isMobile changed: ${this.isMobile}`); |
||||
|
this.onMobileModeChanged(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onEditModeChanged() { |
||||
|
if (this.widgetContext.isEdit !== this.isEdit) { |
||||
|
this.widgetContext.isEdit = this.isEdit; |
||||
|
if (this.widgetContext.inited) { |
||||
|
// TODO:
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onMobileModeChanged() { |
||||
|
if (this.widgetContext.isMobile !== this.isMobile) { |
||||
|
this.widgetContext.isMobile = this.isMobile; |
||||
|
if (this.widgetContext.inited) { |
||||
|
// TODO:
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onResize() { |
||||
|
if (this.checkSize()) { |
||||
|
if (this.widgetContext.inited) { |
||||
|
// TODO:
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private loadFromWidgetInfo() { |
||||
|
const widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; |
||||
|
const elem = this.elementRef.nativeElement; |
||||
|
elem.classList.add('tb-widget'); |
||||
|
elem.classList.add(widgetNamespace); |
||||
|
this.widgetType = this.widgetInfo.widgetTypeFunction; |
||||
|
|
||||
|
if (!this.widgetType) { |
||||
|
this.widgetTypeInstance = {}; |
||||
|
} else { |
||||
|
try { |
||||
|
this.widgetTypeInstance = new this.widgetType(this.widgetContext); |
||||
|
} catch (e) { |
||||
|
this.handleWidgetException(e); |
||||
|
this.widgetTypeInstance = {}; |
||||
|
} |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onInit) { |
||||
|
this.widgetTypeInstance.onInit = () => {}; |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onDataUpdated) { |
||||
|
this.widgetTypeInstance.onDataUpdated = () => {}; |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onResize) { |
||||
|
this.widgetTypeInstance.onResize = () => {}; |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onEditModeChanged) { |
||||
|
this.widgetTypeInstance.onEditModeChanged = () => {}; |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onMobileModeChanged) { |
||||
|
this.widgetTypeInstance.onMobileModeChanged = () => {}; |
||||
|
} |
||||
|
if (!this.widgetTypeInstance.onDestroy) { |
||||
|
this.widgetTypeInstance.onDestroy = () => {}; |
||||
|
} |
||||
|
|
||||
|
this.initialize(); |
||||
|
} |
||||
|
|
||||
|
private reInit() { |
||||
|
this.ngOnDestroy(); |
||||
|
this.initialize(); |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
private initialize() { |
||||
|
this.configureDynamicWidgetComponent().subscribe( |
||||
|
() => { |
||||
|
this.dynamicWidgetComponent.loadingData = false; |
||||
|
}, |
||||
|
(error) => { |
||||
|
// TODO:
|
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private destroyDynamicWidgetComponent() { |
||||
|
if (this.widgetContext.$containerParent) { |
||||
|
// @ts-ignore
|
||||
|
removeResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); |
||||
|
} |
||||
|
if (this.dynamicWidgetComponentRef) { |
||||
|
this.dynamicWidgetComponentRef.destroy(); |
||||
|
} |
||||
|
if (this.dynamicWidgetComponentFactory) { |
||||
|
this.dynamicWidgetComponentFactoryService.destroyDynamicWidgetComponentFactory(this.dynamicWidgetComponentFactory); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private handleWidgetException(e) { |
||||
|
console.error(e); |
||||
|
this.widgetErrorData = this.utils.processWidgetException(e); |
||||
|
if (this.dynamicWidgetComponent) { |
||||
|
this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private configureDynamicWidgetComponent(): Observable<any> { |
||||
|
|
||||
|
const dynamicWidgetComponentSubject = new ReplaySubject(); |
||||
|
|
||||
|
let html = '<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">' + |
||||
|
'<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' + |
||||
|
'</div>' + |
||||
|
'<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">' + |
||||
|
'<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>' + |
||||
|
'</div>'; |
||||
|
|
||||
|
let containerHtml = `<div id="container">${this.widgetInfo.templateHtml}</div>`; |
||||
|
|
||||
|
const displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend |
||||
|
: this.widget.type === widgetType.timeseries; |
||||
|
|
||||
|
let legendConfig: LegendConfig; |
||||
|
let legendData: LegendData; |
||||
|
if (displayLegend) { |
||||
|
legendConfig = this.widget.config.legendConfig || |
||||
|
{ |
||||
|
position: LegendPosition.bottom, |
||||
|
showMin: false, |
||||
|
showMax: false, |
||||
|
showAvg: this.widget.type === widgetType.timeseries, |
||||
|
showTotal: false |
||||
|
}; |
||||
|
legendData = { |
||||
|
keys: [], |
||||
|
data: [] |
||||
|
}; |
||||
|
let layoutType; |
||||
|
if (legendConfig.position === LegendPosition.top || |
||||
|
legendConfig.position === LegendPosition.bottom) { |
||||
|
layoutType = 'column'; |
||||
|
} else { |
||||
|
layoutType = 'row'; |
||||
|
} |
||||
|
let legendStyle; |
||||
|
switch (legendConfig.position) { |
||||
|
case LegendPosition.top: |
||||
|
legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;'; |
||||
|
break; |
||||
|
case LegendPosition.bottom: |
||||
|
legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;'; |
||||
|
break; |
||||
|
case LegendPosition.left: |
||||
|
legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;'; |
||||
|
break; |
||||
|
case LegendPosition.right: |
||||
|
legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;'; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
const legendHtml = `<tb-legend style="${legendStyle}" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>`; |
||||
|
containerHtml = `<div fxFlex id="widget-container">${containerHtml}</div>`; |
||||
|
html += `<div class="tb-absolute-fill" fxLayout="${layoutType}">`; |
||||
|
if (legendConfig.position === LegendPosition.top || |
||||
|
legendConfig.position === LegendPosition.left) { |
||||
|
html += legendHtml; |
||||
|
html += containerHtml; |
||||
|
} else { |
||||
|
html += containerHtml; |
||||
|
html += legendHtml; |
||||
|
} |
||||
|
html += '</div>'; |
||||
|
} else { |
||||
|
html += containerHtml; |
||||
|
} |
||||
|
|
||||
|
this.dynamicWidgetComponentFactoryService.createDynamicWidgetComponentFactory(html).subscribe( |
||||
|
(componentFactory) => { |
||||
|
this.dynamicWidgetComponentFactory = componentFactory; |
||||
|
this.widgetContentContainer.clear(); |
||||
|
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.dynamicWidgetComponentFactory); |
||||
|
this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; |
||||
|
|
||||
|
this.dynamicWidgetComponent.loadingData = true; |
||||
|
this.dynamicWidgetComponent.widgetContext = this.widgetContext; |
||||
|
this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; |
||||
|
this.dynamicWidgetComponent.displayLegend = displayLegend; |
||||
|
this.dynamicWidgetComponent.legendConfig = legendConfig; |
||||
|
this.dynamicWidgetComponent.legendData = legendData; |
||||
|
|
||||
|
this.widgetContext.$scope = this.dynamicWidgetComponent; |
||||
|
|
||||
|
const containerElement = displayLegend ? $(this.elementRef.nativeElement.querySelector('#widget-container')) |
||||
|
: $(this.elementRef.nativeElement); |
||||
|
|
||||
|
this.widgetContext.$container = $('#container', containerElement); |
||||
|
this.widgetContext.$containerParent = $(containerElement); |
||||
|
|
||||
|
if (this.widgetSizeDetected) { |
||||
|
this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); |
||||
|
this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); |
||||
|
} |
||||
|
|
||||
|
// @ts-ignore
|
||||
|
addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); |
||||
|
|
||||
|
dynamicWidgetComponentSubject.next(); |
||||
|
dynamicWidgetComponentSubject.complete(); |
||||
|
}, |
||||
|
(e) => { |
||||
|
dynamicWidgetComponentSubject.error(e); |
||||
|
} |
||||
|
); |
||||
|
return dynamicWidgetComponentSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> { |
||||
|
// TODO:
|
||||
|
return of(null); |
||||
|
} |
||||
|
|
||||
|
private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array<SubscriptionInfo>, |
||||
|
options: WidgetSubscriptionOptions, useDefaultComponents: boolean, |
||||
|
subscribe: boolean): Observable<IWidgetSubscription> { |
||||
|
// TODO:
|
||||
|
return of(null); |
||||
|
} |
||||
|
|
||||
|
private isNumeric(value: any): boolean { |
||||
|
return (value - parseFloat( value ) + 1) >= 0; |
||||
|
} |
||||
|
|
||||
|
private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { |
||||
|
if (isDefined(value) && |
||||
|
value != null && this.isNumeric(value)) { |
||||
|
let formatted: string | number = Number(value); |
||||
|
if (isDefined(dec)) { |
||||
|
formatted = formatted.toFixed(dec); |
||||
|
} |
||||
|
if (!showZeroDecimals) { |
||||
|
formatted = (Number(formatted) * 1); |
||||
|
} |
||||
|
formatted = formatted.toString(); |
||||
|
if (isDefined(units) && units.length > 0) { |
||||
|
formatted += ' ' + units; |
||||
|
} |
||||
|
return formatted; |
||||
|
} else { |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getActionDescriptors(actionSourceId: string): Array<WidgetActionDescriptor> { |
||||
|
let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId]; |
||||
|
if (!result) { |
||||
|
result = []; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, |
||||
|
entityId?: EntityId, entityName?: string, additionalParams?: any): void { |
||||
|
const type = descriptor.type; |
||||
|
const targetEntityParamName = descriptor.stateEntityParamName; |
||||
|
let targetEntityId: EntityId; |
||||
|
if (descriptor.setEntityId) { |
||||
|
targetEntityId = entityId; |
||||
|
} |
||||
|
switch (type) { |
||||
|
case WidgetActionType.openDashboardState: |
||||
|
case WidgetActionType.updateDashboardState: |
||||
|
let targetDashboardStateId = descriptor.targetDashboardStateId; |
||||
|
const params = {...this.widgetContext.stateController.getStateParams()}; |
||||
|
this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName); |
||||
|
if (type === WidgetActionType.openDashboardState) { |
||||
|
this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); |
||||
|
} else { |
||||
|
this.widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout); |
||||
|
} |
||||
|
break; |
||||
|
case WidgetActionType.openDashboard: |
||||
|
const targetDashboardId = descriptor.targetDashboardId; |
||||
|
targetDashboardStateId = descriptor.targetDashboardStateId; |
||||
|
const stateObject: StateObject = {}; |
||||
|
stateObject.params = {}; |
||||
|
this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName); |
||||
|
if (targetDashboardStateId) { |
||||
|
stateObject.id = targetDashboardStateId; |
||||
|
} |
||||
|
const stateParams = { |
||||
|
dashboardId: targetDashboardId, |
||||
|
state: objToBase64([ stateObject ]) |
||||
|
}; |
||||
|
const state = objToBase64([ stateObject ]); |
||||
|
const currentUrl = this.route.snapshot.url; |
||||
|
let url; |
||||
|
if (currentUrl.length > 1) { |
||||
|
if (currentUrl[currentUrl.length - 2].path === 'dashboard') { |
||||
|
url = `/dashboard/${targetDashboardId}?state=${state}`; |
||||
|
} else { |
||||
|
url = `/dashboards/${targetDashboardId}?state=${state}`; |
||||
|
} |
||||
|
} |
||||
|
if (url) { |
||||
|
const urlTree = this.router.parseUrl(url); |
||||
|
this.router.navigateByUrl(url); |
||||
|
} |
||||
|
break; |
||||
|
case WidgetActionType.custom: |
||||
|
const customFunction = descriptor.customFunction; |
||||
|
if (isDefined(customFunction) && customFunction.length > 0) { |
||||
|
try { |
||||
|
if (!additionalParams) { |
||||
|
additionalParams = {}; |
||||
|
} |
||||
|
const customActionFunction = new Function('$event', 'widgetContext', 'entityId', |
||||
|
'entityName', 'additionalParams', customFunction); |
||||
|
customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams); |
||||
|
} catch (e) { |
||||
|
//
|
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
case WidgetActionType.customPretty: |
||||
|
const customPrettyFunction = descriptor.customFunction; |
||||
|
const customHtml = descriptor.customHtml; |
||||
|
const customCss = descriptor.customCss; |
||||
|
const customResources = descriptor.customResources; |
||||
|
const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; |
||||
|
let htmlTemplate = ''; |
||||
|
if (isDefined(customHtml) && customHtml.length > 0) { |
||||
|
htmlTemplate = customHtml; |
||||
|
} |
||||
|
this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( |
||||
|
() => { |
||||
|
if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { |
||||
|
try { |
||||
|
if (!additionalParams) { |
||||
|
additionalParams = {}; |
||||
|
} |
||||
|
const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', |
||||
|
'entityName', 'htmlTemplate', 'additionalParams', customPrettyFunction); |
||||
|
customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams); |
||||
|
} catch (e) { |
||||
|
//
|
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
(errorMessages: string[]) => { |
||||
|
this.processResourcesLoadErrors(errorMessages); |
||||
|
} |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private elementClick($event: Event) { |
||||
|
$event.stopPropagation(); |
||||
|
const e = ($event.target || $event.srcElement) as Element; |
||||
|
if (e.id) { |
||||
|
const descriptors = this.getActionDescriptors('elementClick'); |
||||
|
if (descriptors.length) { |
||||
|
descriptors.forEach((descriptor) => { |
||||
|
if (descriptor.name === e.id) { |
||||
|
const entityInfo = this.getActiveEntityInfo(); |
||||
|
const entityId = entityInfo ? entityInfo.entityId : null; |
||||
|
const entityName = entityInfo ? entityInfo.entityName : null; |
||||
|
this.handleWidgetAction(event, descriptor, entityId, entityName); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateEntityParams(params: StateParams, targetEntityParamName?: string, targetEntityId?: EntityId, entityName?: string) { |
||||
|
if (targetEntityId) { |
||||
|
let targetEntityParams: StateParams; |
||||
|
if (targetEntityParamName && targetEntityParamName.length) { |
||||
|
targetEntityParams = params[targetEntityParamName]; |
||||
|
if (!targetEntityParams) { |
||||
|
targetEntityParams = {}; |
||||
|
params[targetEntityParamName] = targetEntityParams; |
||||
|
params.targetEntityParamName = targetEntityParamName; |
||||
|
} |
||||
|
} else { |
||||
|
targetEntityParams = params; |
||||
|
} |
||||
|
targetEntityParams.entityId = targetEntityId; |
||||
|
if (entityName) { |
||||
|
targetEntityParams.entityName = entityName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> { |
||||
|
if (isDefined(customCss) && customCss.length > 0) { |
||||
|
this.cssParser.cssPreviewNamespace = actionNamespace; |
||||
|
this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); |
||||
|
} |
||||
|
const resourceTasks: Observable<string>[] = []; |
||||
|
if (customResources.length > 0) { |
||||
|
customResources.forEach((resource) => { |
||||
|
resourceTasks.push( |
||||
|
this.resources.loadResource(resource.url).pipe( |
||||
|
catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) |
||||
|
) |
||||
|
); |
||||
|
}); |
||||
|
return forkJoin(resourceTasks).pipe( |
||||
|
switchMap(msgs => { |
||||
|
let errors: string[]; |
||||
|
if (msgs && msgs.length) { |
||||
|
errors = msgs.filter(msg => msg && msg.length > 0); |
||||
|
} |
||||
|
if (errors && errors.length) { |
||||
|
return throwError(errors); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
)); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private processResourcesLoadErrors(errorMessages: string[]) { |
||||
|
let messageToShow = ''; |
||||
|
errorMessages.forEach(error => { |
||||
|
messageToShow += `<div>${error}</div>`; |
||||
|
}); |
||||
|
this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); |
||||
|
} |
||||
|
|
||||
|
private getActiveEntityInfo(): EntityInfo { |
||||
|
let entityInfo = this.widgetContext.activeEntityInfo; |
||||
|
if (!entityInfo) { |
||||
|
for (const id of Object.keys(this.widgetContext.subscriptions)) { |
||||
|
const subscription = this.widgetContext.subscriptions[id]; |
||||
|
entityInfo = subscription.getFirstEntityInfo(); |
||||
|
if (entityInfo) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return entityInfo; |
||||
|
} |
||||
|
|
||||
|
private checkSize(): boolean { |
||||
|
const width = this.widgetContext.$containerParent.width(); |
||||
|
const height = this.widgetContext.$containerParent.height(); |
||||
|
let sizeChanged = false; |
||||
|
|
||||
|
if (!this.widgetContext.width || this.widgetContext.width !== width || |
||||
|
!this.widgetContext.height || this.widgetContext.height !== height) { |
||||
|
if (width > 0 && height > 0) { |
||||
|
this.widgetContext.$container.css('height', height + 'px'); |
||||
|
this.widgetContext.$container.css('width', width + 'px'); |
||||
|
this.widgetContext.width = width; |
||||
|
this.widgetContext.height = height; |
||||
|
sizeChanged = true; |
||||
|
this.widgetSizeDetected = true; |
||||
|
} |
||||
|
} |
||||
|
return sizeChanged; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
|
||||
|
export interface ExceptionData { |
||||
|
message?: string; |
||||
|
name?: string; |
||||
|
lineNumber?: number; |
||||
|
columnNumber?: number; |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
export type WindowMessageType = 'widgetException'; |
||||
|
|
||||
|
export interface WindowMessage { |
||||
|
type: WindowMessageType; |
||||
|
data: any; |
||||
|
} |
||||
Loading…
Reference in new issue