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