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