Commit 9ec843cbfacc8fcfef106eaff278caa78ba1e2b4
1 parent
2e7070a9
Widget component initial implementation
Showing
30 changed files
with
3188 additions
and
169 deletions
@@ -30,6 +30,8 @@ | @@ -30,6 +30,8 @@ | ||
30 | "src/styles.scss" | 30 | "src/styles.scss" |
31 | ], | 31 | ], |
32 | "scripts": [ | 32 | "scripts": [ |
33 | + "node_modules/javascript-detect-element-resize/detect-element-resize.js", | ||
34 | + "node_modules/jquery/dist/jquery.min.js", | ||
33 | "node_modules/ace-builds/src-min/ace.js", | 35 | "node_modules/ace-builds/src-min/ace.js", |
34 | "node_modules/ace-builds/src-min/ext-language_tools.js", | 36 | "node_modules/ace-builds/src-min/ext-language_tools.js", |
35 | "node_modules/ace-builds/src-min/ext-searchbox.js", | 37 | "node_modules/ace-builds/src-min/ext-searchbox.js", |
@@ -71,10 +73,10 @@ | @@ -71,10 +73,10 @@ | ||
71 | "sourceMap": false, | 73 | "sourceMap": false, |
72 | "extractCss": true, | 74 | "extractCss": true, |
73 | "namedChunks": false, | 75 | "namedChunks": false, |
74 | - "aot": true, | 76 | + "aot": false, |
75 | "extractLicenses": true, | 77 | "extractLicenses": true, |
76 | "vendorChunk": false, | 78 | "vendorChunk": false, |
77 | - "buildOptimizer": true, | 79 | + "buildOptimizer": false, |
78 | "budgets": [ | 80 | "budgets": [ |
79 | { | 81 | { |
80 | "type": "initial", | 82 | "type": "initial", |
@@ -1993,8 +1993,7 @@ | @@ -1993,8 +1993,7 @@ | ||
1993 | "base64-js": { | 1993 | "base64-js": { |
1994 | "version": "1.3.1", | 1994 | "version": "1.3.1", |
1995 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", | 1995 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", |
1996 | - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", | ||
1997 | - "dev": true | 1996 | + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" |
1998 | }, | 1997 | }, |
1999 | "base64id": { | 1998 | "base64id": { |
2000 | "version": "1.0.0", | 1999 | "version": "1.0.0", |
@@ -6336,6 +6335,16 @@ | @@ -6336,6 +6335,16 @@ | ||
6336 | "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", | 6335 | "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", |
6337 | "dev": true | 6336 | "dev": true |
6338 | }, | 6337 | }, |
6338 | + "javascript-detect-element-resize": { | ||
6339 | + "version": "0.5.3", | ||
6340 | + "resolved": "https://registry.npmjs.org/javascript-detect-element-resize/-/javascript-detect-element-resize-0.5.3.tgz", | ||
6341 | + "integrity": "sha1-GnHNUd/lZZB/KZAS/nOilBBAJd4=" | ||
6342 | + }, | ||
6343 | + "jquery": { | ||
6344 | + "version": "3.4.1", | ||
6345 | + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", | ||
6346 | + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" | ||
6347 | + }, | ||
6339 | "js-tokens": { | 6348 | "js-tokens": { |
6340 | "version": "3.0.2", | 6349 | "version": "3.0.2", |
6341 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", | 6350 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", |
@@ -33,11 +33,14 @@ | @@ -33,11 +33,14 @@ | ||
33 | "@ngx-translate/http-loader": "^4.0.0", | 33 | "@ngx-translate/http-loader": "^4.0.0", |
34 | "ace-builds": "^1.4.5", | 34 | "ace-builds": "^1.4.5", |
35 | "angular-gridster2": "^8.1.0", | 35 | "angular-gridster2": "^8.1.0", |
36 | + "base64-js": "^1.3.1", | ||
36 | "compass-sass-mixins": "^0.12.7", | 37 | "compass-sass-mixins": "^0.12.7", |
37 | "core-js": "^3.1.4", | 38 | "core-js": "^3.1.4", |
38 | "deep-equal": "^1.0.1", | 39 | "deep-equal": "^1.0.1", |
39 | "font-awesome": "^4.7.0", | 40 | "font-awesome": "^4.7.0", |
40 | "hammerjs": "^2.0.8", | 41 | "hammerjs": "^2.0.8", |
42 | + "javascript-detect-element-resize": "^0.5.3", | ||
43 | + "jquery": "^3.4.1", | ||
41 | "material-design-icons": "^3.0.1", | 44 | "material-design-icons": "^3.0.1", |
42 | "messageformat": "^2.3.0", | 45 | "messageformat": "^2.3.0", |
43 | "ngx-clipboard": "^12.2.0", | 46 | "ngx-clipboard": "^12.2.0", |
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,21 +16,46 @@ | ||
16 | 16 | ||
17 | import {Injectable} from '@angular/core'; | 17 | import {Injectable} from '@angular/core'; |
18 | import {defaultHttpOptions} from './http-utils'; | 18 | import {defaultHttpOptions} from './http-utils'; |
19 | -import {Observable} from 'rxjs/index'; | 19 | +import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } from 'rxjs/index'; |
20 | import {HttpClient} from '@angular/common/http'; | 20 | import {HttpClient} from '@angular/common/http'; |
21 | import {PageLink} from '@shared/models/page/page-link'; | 21 | import {PageLink} from '@shared/models/page/page-link'; |
22 | import {PageData} from '@shared/models/page/page-data'; | 22 | import {PageData} from '@shared/models/page/page-data'; |
23 | import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; | 23 | import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; |
24 | -import { WidgetType } from '@shared/models/widget.models'; | 24 | +import { |
25 | + WidgetControllerDescriptor, | ||
26 | + WidgetInfo, | ||
27 | + WidgetType, | ||
28 | + WidgetTypeInstance, | ||
29 | + widgetActionSources, | ||
30 | + MissingWidgetType, toWidgetInfo, ErrorWidgetType | ||
31 | +} from '@shared/models/widget.models'; | ||
32 | +import { UtilsService } from '@core/services/utils.service'; | ||
33 | +import { isFunction, isUndefined } from '@core/utils'; | ||
34 | +import { TranslateService } from '@ngx-translate/core'; | ||
35 | +import { AuthPayload } from '@core/auth/auth.models'; | ||
36 | +import cssjs from '@core/css/css'; | ||
37 | +import { ResourcesService } from '../services/resources.service'; | ||
38 | +import { catchError, map, switchMap } from 'rxjs/operators'; | ||
25 | 39 | ||
26 | @Injectable({ | 40 | @Injectable({ |
27 | providedIn: 'root' | 41 | providedIn: 'root' |
28 | }) | 42 | }) |
29 | export class WidgetService { | 43 | export class WidgetService { |
30 | 44 | ||
45 | + private cssParser = new cssjs(); | ||
46 | + | ||
47 | + private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>(); | ||
48 | + | ||
49 | + private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>(); | ||
50 | + | ||
31 | constructor( | 51 | constructor( |
32 | - private http: HttpClient | ||
33 | - ) { } | 52 | + private http: HttpClient, |
53 | + private utils: UtilsService, | ||
54 | + private resources: ResourcesService, | ||
55 | + private translate: TranslateService | ||
56 | + ) { | ||
57 | + this.cssParser.testMode = false; | ||
58 | + } | ||
34 | 59 | ||
35 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, | 60 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, |
36 | ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> { | 61 | ignoreLoading: boolean = false): Observable<PageData<WidgetsBundle>> { |
@@ -58,4 +83,266 @@ export class WidgetService { | @@ -58,4 +83,266 @@ export class WidgetService { | ||
58 | defaultHttpOptions(ignoreLoading, ignoreErrors)); | 83 | defaultHttpOptions(ignoreLoading, ignoreErrors)); |
59 | } | 84 | } |
60 | 85 | ||
86 | + public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, | ||
87 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | ||
88 | + return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, | ||
89 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
90 | + } | ||
91 | + | ||
92 | + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { | ||
93 | + const widgetInfoSubject = new ReplaySubject<WidgetInfo>(); | ||
94 | + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); | ||
95 | + if (widgetInfo) { | ||
96 | + widgetInfoSubject.next(widgetInfo); | ||
97 | + widgetInfoSubject.complete(); | ||
98 | + } else { | ||
99 | + if (this.utils.widgetEditMode) { | ||
100 | + // TODO: | ||
101 | + } else { | ||
102 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
103 | + let fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
104 | + if (fetchQueue) { | ||
105 | + fetchQueue.push(widgetInfoSubject); | ||
106 | + } else { | ||
107 | + fetchQueue = new Array<Subject<WidgetInfo>>(); | ||
108 | + this.widgetsInfoFetchQueue.set(key, fetchQueue); | ||
109 | + this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( | ||
110 | + (widgetType) => { | ||
111 | + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); | ||
112 | + }, | ||
113 | + () => { | ||
114 | + widgetInfoSubject.next(MissingWidgetType); | ||
115 | + widgetInfoSubject.complete(); | ||
116 | + this.resolveWidgetsInfoFetchQueue(key, MissingWidgetType); | ||
117 | + } | ||
118 | + ); | ||
119 | + } | ||
120 | + } | ||
121 | + } | ||
122 | + return widgetInfoSubject.asObservable(); | ||
123 | + } | ||
124 | + | ||
125 | + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) { | ||
126 | + const widgetInfo = toWidgetInfo(widgetType); | ||
127 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); | ||
128 | + this.loadWidgetResources(widgetInfo, bundleAlias, isSystem).subscribe( | ||
129 | + () => { | ||
130 | + let widgetControllerDescriptor: WidgetControllerDescriptor = null; | ||
131 | + try { | ||
132 | + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); | ||
133 | + } catch (e) { | ||
134 | + const details = this.utils.parseException(e); | ||
135 | + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; | ||
136 | + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); | ||
137 | + } | ||
138 | + if (widgetControllerDescriptor) { | ||
139 | + if (widgetControllerDescriptor.settingsSchema) { | ||
140 | + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; | ||
141 | + } | ||
142 | + if (widgetControllerDescriptor.dataKeySettingsSchema) { | ||
143 | + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; | ||
144 | + } | ||
145 | + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; | ||
146 | + widgetInfo.actionSources = widgetControllerDescriptor.actionSources; | ||
147 | + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; | ||
148 | + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); | ||
149 | + if (widgetInfoSubject) { | ||
150 | + widgetInfoSubject.next(widgetInfo); | ||
151 | + widgetInfoSubject.complete(); | ||
152 | + } | ||
153 | + this.resolveWidgetsInfoFetchQueue(key, widgetInfo); | ||
154 | + } | ||
155 | + }, | ||
156 | + (errorMessages: string[]) => { | ||
157 | + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); | ||
158 | + } | ||
159 | + ); | ||
160 | + } | ||
161 | + | ||
162 | + private loadWidgetResources(widgetInfo: WidgetInfo, bundleAlias: string, isSystem: boolean): Observable<any> { | ||
163 | + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; | ||
164 | + this.cssParser.cssPreviewNamespace = widgetNamespace; | ||
165 | + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); | ||
166 | + const resourceTasks: Observable<string>[] = []; | ||
167 | + if (widgetInfo.resources.length > 0) { | ||
168 | + widgetInfo.resources.forEach((resource) => { | ||
169 | + resourceTasks.push( | ||
170 | + this.resources.loadResource(resource.url).pipe( | ||
171 | + catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) | ||
172 | + ) | ||
173 | + ); | ||
174 | + }); | ||
175 | + return forkJoin(resourceTasks).pipe( | ||
176 | + switchMap(msgs => { | ||
177 | + let errors: string[]; | ||
178 | + if (msgs && msgs.length) { | ||
179 | + errors = msgs.filter(msg => msg && msg.length > 0); | ||
180 | + } | ||
181 | + if (errors && errors.length) { | ||
182 | + return throwError(errors); | ||
183 | + } else { | ||
184 | + return of(null); | ||
185 | + } | ||
186 | + } | ||
187 | + )); | ||
188 | + } else { | ||
189 | + return of(null); | ||
190 | + } | ||
191 | + } | ||
192 | + | ||
193 | + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { | ||
194 | + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + | ||
195 | + ' var self = this;\n' + | ||
196 | + ' self.ctx = ctx;\n\n'; /*+ | ||
197 | + | ||
198 | + ' self.onInit = function() {\n\n' + | ||
199 | + | ||
200 | + ' }\n\n' + | ||
201 | + | ||
202 | + ' self.onDataUpdated = function() {\n\n' + | ||
203 | + | ||
204 | + ' }\n\n' + | ||
205 | + | ||
206 | + ' self.useCustomDatasources = function() {\n\n' + | ||
207 | + | ||
208 | + ' }\n\n' + | ||
209 | + | ||
210 | + ' self.typeParameters = function() {\n\n' + | ||
211 | + return { | ||
212 | + useCustomDatasources: false, | ||
213 | + maxDatasources: -1, //unlimited | ||
214 | + maxDataKeys: -1, //unlimited | ||
215 | + dataKeysOptional: false, | ||
216 | + stateData: false | ||
217 | + }; | ||
218 | + ' }\n\n' + | ||
219 | + | ||
220 | + ' self.actionSources = function() {\n\n' + | ||
221 | + return { | ||
222 | + 'headerButton': { | ||
223 | + name: 'Header button', | ||
224 | + multiple: true | ||
225 | + } | ||
226 | + }; | ||
227 | + }\n\n' + | ||
228 | + ' self.onResize = function() {\n\n' + | ||
229 | + | ||
230 | + ' }\n\n' + | ||
231 | + | ||
232 | + ' self.onEditModeChanged = function() {\n\n' + | ||
233 | + | ||
234 | + ' }\n\n' + | ||
235 | + | ||
236 | + ' self.onMobileModeChanged = function() {\n\n' + | ||
237 | + | ||
238 | + ' }\n\n' + | ||
239 | + | ||
240 | + ' self.getSettingsSchema = function() {\n\n' + | ||
241 | + | ||
242 | + ' }\n\n' + | ||
243 | + | ||
244 | + ' self.getDataKeySettingsSchema = function() {\n\n' + | ||
245 | + | ||
246 | + ' }\n\n' + | ||
247 | + | ||
248 | + ' self.onDestroy = function() {\n\n' + | ||
249 | + | ||
250 | + ' }\n\n' + | ||
251 | + '}';*/ | ||
252 | + | ||
253 | + widgetTypeFunctionBody += widgetInfo.controllerScript; | ||
254 | + widgetTypeFunctionBody += '\n};\n'; | ||
255 | + | ||
256 | + try { | ||
257 | + | ||
258 | + const widgetTypeFunction = new Function(widgetTypeFunctionBody); | ||
259 | + const widgetType = widgetTypeFunction.apply(this); | ||
260 | + const widgetTypeInstance: WidgetTypeInstance = new widgetType(); | ||
261 | + const result: WidgetControllerDescriptor = { | ||
262 | + widgetTypeFunction: widgetType | ||
263 | + }; | ||
264 | + if (isFunction(widgetTypeInstance.getSettingsSchema)) { | ||
265 | + result.settingsSchema = widgetTypeInstance.getSettingsSchema(); | ||
266 | + } | ||
267 | + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { | ||
268 | + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); | ||
269 | + } | ||
270 | + if (isFunction(widgetTypeInstance.typeParameters)) { | ||
271 | + result.typeParameters = widgetTypeInstance.typeParameters(); | ||
272 | + } else { | ||
273 | + result.typeParameters = {}; | ||
274 | + } | ||
275 | + if (isFunction(widgetTypeInstance.useCustomDatasources)) { | ||
276 | + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); | ||
277 | + } else { | ||
278 | + result.typeParameters.useCustomDatasources = false; | ||
279 | + } | ||
280 | + if (isUndefined(result.typeParameters.maxDatasources)) { | ||
281 | + result.typeParameters.maxDatasources = -1; | ||
282 | + } | ||
283 | + if (isUndefined(result.typeParameters.maxDataKeys)) { | ||
284 | + result.typeParameters.maxDataKeys = -1; | ||
285 | + } | ||
286 | + if (isUndefined(result.typeParameters.dataKeysOptional)) { | ||
287 | + result.typeParameters.dataKeysOptional = false; | ||
288 | + } | ||
289 | + if (isUndefined(result.typeParameters.stateData)) { | ||
290 | + result.typeParameters.stateData = false; | ||
291 | + } | ||
292 | + if (isFunction(widgetTypeInstance.actionSources)) { | ||
293 | + result.actionSources = widgetTypeInstance.actionSources(); | ||
294 | + } else { | ||
295 | + result.actionSources = {}; | ||
296 | + } | ||
297 | + for (const actionSourceId of Object.keys(widgetActionSources)) { | ||
298 | + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; | ||
299 | + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); | ||
300 | + } | ||
301 | + return result; | ||
302 | + } catch (e) { | ||
303 | + this.utils.processWidgetException(e); | ||
304 | + throw e; | ||
305 | + } | ||
306 | + } | ||
307 | + | ||
308 | + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) { | ||
309 | + const widgetInfo = {...ErrorWidgetType}; | ||
310 | + errorMessages.forEach(error => { | ||
311 | + widgetInfo.templateHtml += `<div class="tb-widget-error-msg">${error}</div>`; | ||
312 | + }); | ||
313 | + widgetInfo.templateHtml += '</div>'; | ||
314 | + if (widgetInfoSubject) { | ||
315 | + widgetInfoSubject.next(widgetInfo); | ||
316 | + widgetInfoSubject.complete(); | ||
317 | + } | ||
318 | + this.resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo); | ||
319 | + } | ||
320 | + | ||
321 | + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo) { | ||
322 | + const fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
323 | + if (fetchQueue) { | ||
324 | + fetchQueue.forEach(subject => { | ||
325 | + subject.next(widgetInfo); | ||
326 | + subject.complete(); | ||
327 | + }); | ||
328 | + this.widgetsInfoFetchQueue.delete(key); | ||
329 | + } | ||
330 | + } | ||
331 | + | ||
332 | + // Cache functions | ||
333 | + | ||
334 | + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { | ||
335 | + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; | ||
336 | + } | ||
337 | + | ||
338 | + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { | ||
339 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
340 | + return this.widgetsInfoInMemoryCache.get(key); | ||
341 | + } | ||
342 | + | ||
343 | + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { | ||
344 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
345 | + this.widgetsInfoInMemoryCache.set(key, widgetInfo); | ||
346 | + } | ||
347 | + | ||
61 | } | 348 | } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable, Inject } from '@angular/core'; | ||
18 | +import { DOCUMENT } from '@angular/common'; | ||
19 | +import { ReplaySubject, Observable, throwError } from 'rxjs'; | ||
20 | + | ||
21 | +@Injectable({ | ||
22 | + providedIn: 'root' | ||
23 | +}) | ||
24 | +export class ResourcesService { | ||
25 | + | ||
26 | + private loadedResources: { [url: string]: ReplaySubject<any> } = {}; | ||
27 | + | ||
28 | + private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; | ||
29 | + | ||
30 | + constructor(@Inject(DOCUMENT) private readonly document: any) {} | ||
31 | + | ||
32 | + public loadResource(url: string): Observable<any> { | ||
33 | + if (this.loadedResources[url]) { | ||
34 | + return this.loadedResources[url].asObservable(); | ||
35 | + } | ||
36 | + | ||
37 | + let fileType; | ||
38 | + const match = /[.](css|less|html|htm|js)?((\?|#).*)?$/.exec(url); | ||
39 | + if (match !== null) { | ||
40 | + fileType = match[1]; | ||
41 | + } | ||
42 | + if (!fileType) { | ||
43 | + return throwError(new Error(`Unable to detect file type from url: ${url}`)); | ||
44 | + } else if (fileType !== 'css' && fileType !== 'js') { | ||
45 | + return throwError(new Error(`Unsupported file type: ${fileType}`)); | ||
46 | + } | ||
47 | + return this.loadResourceByType(fileType, url); | ||
48 | + } | ||
49 | + | ||
50 | + private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> { | ||
51 | + const subject = new ReplaySubject(); | ||
52 | + this.loadedResources[url] = subject; | ||
53 | + let el; | ||
54 | + let loaded = false; | ||
55 | + switch (type) { | ||
56 | + case 'js': | ||
57 | + el = this.document.createElement('script'); | ||
58 | + el.type = 'text/javascript'; | ||
59 | + el.async = true; | ||
60 | + el.src = url; | ||
61 | + break; | ||
62 | + case 'css': | ||
63 | + el = this.document.createElement('link'); | ||
64 | + el.type = 'text/css'; | ||
65 | + el.rel = 'stylesheet'; | ||
66 | + el.href = url; | ||
67 | + break; | ||
68 | + } | ||
69 | + el.onload = el.onreadystatechange = (e) => { | ||
70 | + if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; } | ||
71 | + el.onload = el.onreadystatechange = null; | ||
72 | + loaded = true; | ||
73 | + this.loadedResources[url].next(); | ||
74 | + this.loadedResources[url].complete(); | ||
75 | + }; | ||
76 | + el.onerror = () => { | ||
77 | + this.loadedResources[url].error(new Error(`Unable to load ${url}`)); | ||
78 | + delete this.loadedResources[url]; | ||
79 | + }; | ||
80 | + this.anchor.appendChild(el); | ||
81 | + return subject.asObservable(); | ||
82 | + } | ||
83 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Inject, Injectable } from '@angular/core'; | ||
18 | +import { WINDOW } from '@core/services/window.service'; | ||
19 | +import { WidgetInfo } from '@shared/models/widget.models'; | ||
20 | +import { ExceptionData } from '@app/shared/models/error.models'; | ||
21 | +import { isUndefined } from '@core/utils'; | ||
22 | +import { WindowMessage } from '@shared/models/window-message.model'; | ||
23 | +import { TranslateService } from '@ngx-translate/core'; | ||
24 | +import { customTranslationsPrefix } from '@app/shared/models/constants'; | ||
25 | + | ||
26 | +@Injectable({ | ||
27 | + providedIn: 'root' | ||
28 | +}) | ||
29 | +export class UtilsService { | ||
30 | + | ||
31 | + iframeMode = false; | ||
32 | + widgetEditMode = false; | ||
33 | + editWidgetInfo: WidgetInfo = null; | ||
34 | + | ||
35 | + constructor(@Inject(WINDOW) private window: Window, | ||
36 | + private translate: TranslateService) { | ||
37 | + let frame: Element = null; | ||
38 | + try { | ||
39 | + frame = window.frameElement; | ||
40 | + } catch (e) { | ||
41 | + // ie11 fix | ||
42 | + } | ||
43 | + if (frame) { | ||
44 | + this.iframeMode = true; | ||
45 | + const dataWidgetAttr = frame.getAttribute('data-widget'); | ||
46 | + if (dataWidgetAttr && dataWidgetAttr.length) { | ||
47 | + this.editWidgetInfo = JSON.parse(dataWidgetAttr); | ||
48 | + this.widgetEditMode = true; | ||
49 | + } | ||
50 | + } | ||
51 | + } | ||
52 | + | ||
53 | + public processWidgetException(exception: any): ExceptionData { | ||
54 | + const data = this.parseException(exception, -5); | ||
55 | + if (this.widgetEditMode) { | ||
56 | + const message: WindowMessage = { | ||
57 | + type: 'widgetException', | ||
58 | + data | ||
59 | + }; | ||
60 | + this.window.parent.postMessage(message, '*'); | ||
61 | + } | ||
62 | + return data; | ||
63 | + } | ||
64 | + | ||
65 | + public parseException(exception: any, lineOffset?: number): ExceptionData { | ||
66 | + const data: ExceptionData = {}; | ||
67 | + if (exception) { | ||
68 | + if (typeof exception === 'string') { | ||
69 | + data.message = exception; | ||
70 | + } else if (exception instanceof String) { | ||
71 | + data.message = exception.toString(); | ||
72 | + } else { | ||
73 | + if (exception.name) { | ||
74 | + data.name = exception.name; | ||
75 | + } else { | ||
76 | + data.name = 'UnknownError'; | ||
77 | + } | ||
78 | + if (exception.message) { | ||
79 | + data.message = exception.message; | ||
80 | + } | ||
81 | + if (exception.lineNumber) { | ||
82 | + data.lineNumber = exception.lineNumber; | ||
83 | + if (exception.columnNumber) { | ||
84 | + data.columnNumber = exception.columnNumber; | ||
85 | + } | ||
86 | + } else if (exception.stack) { | ||
87 | + const lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g; | ||
88 | + const lineInfoGroups = lineInfoRegexp.exec(exception.stack); | ||
89 | + if (lineInfoGroups != null && lineInfoGroups.length >= 3) { | ||
90 | + if (isUndefined(lineOffset)) { | ||
91 | + lineOffset = -2; | ||
92 | + } | ||
93 | + data.lineNumber = Number(lineInfoGroups[2]) + lineOffset; | ||
94 | + if (lineInfoGroups.length >= 5) { | ||
95 | + data.columnNumber = Number(lineInfoGroups[4]); | ||
96 | + } | ||
97 | + } | ||
98 | + } | ||
99 | + } | ||
100 | + } | ||
101 | + return data; | ||
102 | + } | ||
103 | + | ||
104 | + public customTranslation(translationValue: string, defaultValue: string): string { | ||
105 | + let result = ''; | ||
106 | + const translationId = customTranslationsPrefix + translationValue; | ||
107 | + const translation = this.translate.instant(translationId); | ||
108 | + if (translation !== translationId) { | ||
109 | + result = translation + ''; | ||
110 | + } else { | ||
111 | + result = defaultValue; | ||
112 | + } | ||
113 | + return result; | ||
114 | + } | ||
115 | + | ||
116 | +} |
@@ -16,6 +16,7 @@ | @@ -16,6 +16,7 @@ | ||
16 | 16 | ||
17 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; | 17 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; |
18 | import { finalize, share } from 'rxjs/operators'; | 18 | import { finalize, share } from 'rxjs/operators'; |
19 | +import base64js from 'base64-js'; | ||
19 | 20 | ||
20 | export function onParentScrollOrWindowResize(el: Node): Observable<Event> { | 21 | export function onParentScrollOrWindowResize(el: Node): Observable<Event> { |
21 | const scrollSubject = new Subject<Event>(); | 22 | const scrollSubject = new Subject<Event>(); |
@@ -78,6 +79,38 @@ export function isDefined(value: any): boolean { | @@ -78,6 +79,38 @@ export function isDefined(value: any): boolean { | ||
78 | return typeof value !== 'undefined'; | 79 | return typeof value !== 'undefined'; |
79 | } | 80 | } |
80 | 81 | ||
82 | +export function isFunction(value: any): boolean { | ||
83 | + return typeof value === 'function'; | ||
84 | +} | ||
85 | + | ||
86 | +export function objToBase64(obj: any): string { | ||
87 | + const json = JSON.stringify(obj); | ||
88 | + const encoded = utf8Encode(json); | ||
89 | + const b64Encoded: string = base64js.fromByteArray(encoded); | ||
90 | + return b64Encoded; | ||
91 | +} | ||
92 | + | ||
93 | +export function base64toObj(b64Encoded: string): any { | ||
94 | + const encoded: Uint8Array | number[] = base64js.toByteArray(b64Encoded); | ||
95 | + const json = utf8Decode(encoded); | ||
96 | + const obj = JSON.parse(json); | ||
97 | + return obj; | ||
98 | +} | ||
99 | + | ||
100 | +function utf8Encode(str: string): Uint8Array | number[] { | ||
101 | + let result: Uint8Array | number[]; | ||
102 | + if (isUndefined(Uint8Array)) { | ||
103 | + result = utf8ToBytes(str); | ||
104 | + } else { | ||
105 | + result = new Uint8Array(utf8ToBytes(str)); | ||
106 | + } | ||
107 | + return result; | ||
108 | +} | ||
109 | + | ||
110 | +function utf8Decode(bytes: Uint8Array | number[]): string { | ||
111 | + return utf8Slice(bytes, 0, bytes.length); | ||
112 | +} | ||
113 | + | ||
81 | const scrollRegex = /(auto|scroll)/; | 114 | const scrollRegex = /(auto|scroll)/; |
82 | 115 | ||
83 | function parentNodes(node: Node, nodes: Node[]): Node[] { | 116 | function parentNodes(node: Node, nodes: Node[]): Node[] { |
@@ -138,3 +171,126 @@ function easeInOut( | @@ -138,3 +171,126 @@ function easeInOut( | ||
138 | (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime | 171 | (-remainingTime / 2) * (currentTime * (currentTime - 2) - 1) + startTime |
139 | ); | 172 | ); |
140 | } | 173 | } |
174 | + | ||
175 | +function utf8Slice(buf: Uint8Array | number[], start: number, end: number): string { | ||
176 | + let res = ''; | ||
177 | + let tmp = ''; | ||
178 | + end = Math.min(buf.length, end || Infinity); | ||
179 | + start = start || 0; | ||
180 | + | ||
181 | + for (let i = start; i < end; i++) { | ||
182 | + if (buf[i] <= 0x7F) { | ||
183 | + res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]); | ||
184 | + tmp = ''; | ||
185 | + } else { | ||
186 | + tmp += '%' + buf[i].toString(16); | ||
187 | + } | ||
188 | + } | ||
189 | + return res + decodeUtf8Char(tmp); | ||
190 | +} | ||
191 | + | ||
192 | +function decodeUtf8Char(str: string): string { | ||
193 | + try { | ||
194 | + return decodeURIComponent(str); | ||
195 | + } catch (err) { | ||
196 | + return String.fromCharCode(0xFFFD); // UTF 8 invalid char | ||
197 | + } | ||
198 | +} | ||
199 | + | ||
200 | +function utf8ToBytes(input: string, units?: number): number[] { | ||
201 | + units = units || Infinity; | ||
202 | + let codePoint: number; | ||
203 | + const length = input.length; | ||
204 | + let leadSurrogate: number = null; | ||
205 | + const bytes: number[] = []; | ||
206 | + let i = 0; | ||
207 | + | ||
208 | + for (; i < length; i++) { | ||
209 | + codePoint = input.charCodeAt(i); | ||
210 | + | ||
211 | + // is surrogate component | ||
212 | + if (codePoint > 0xD7FF && codePoint < 0xE000) { | ||
213 | + // last char was a lead | ||
214 | + if (leadSurrogate) { | ||
215 | + // 2 leads in a row | ||
216 | + if (codePoint < 0xDC00) { | ||
217 | + units -= 3; | ||
218 | + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); } | ||
219 | + leadSurrogate = codePoint; | ||
220 | + continue; | ||
221 | + } else { | ||
222 | + // valid surrogate pair | ||
223 | + // tslint:disable-next-line:no-bitwise | ||
224 | + codePoint = leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00 | 0x10000; | ||
225 | + leadSurrogate = null; | ||
226 | + } | ||
227 | + } else { | ||
228 | + // no lead yet | ||
229 | + | ||
230 | + if (codePoint > 0xDBFF) { | ||
231 | + // unexpected trail | ||
232 | + units -= 3; | ||
233 | + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); } | ||
234 | + continue; | ||
235 | + } else if (i + 1 === length) { | ||
236 | + // unpaired lead | ||
237 | + units -= 3; | ||
238 | + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); } | ||
239 | + continue; | ||
240 | + } else { | ||
241 | + // valid lead | ||
242 | + leadSurrogate = codePoint; | ||
243 | + continue; | ||
244 | + } | ||
245 | + } | ||
246 | + } else if (leadSurrogate) { | ||
247 | + // valid bmp char, but last char was a lead | ||
248 | + units -= 3; | ||
249 | + if (units > -1) { bytes.push(0xEF, 0xBF, 0xBD); } | ||
250 | + leadSurrogate = null; | ||
251 | + } | ||
252 | + | ||
253 | + // encode utf8 | ||
254 | + if (codePoint < 0x80) { | ||
255 | + units -= 1; | ||
256 | + if (units < 0) { break; } | ||
257 | + bytes.push(codePoint); | ||
258 | + } else if (codePoint < 0x800) { | ||
259 | + units -= 2; | ||
260 | + if (units < 0) { break; } | ||
261 | + bytes.push( | ||
262 | + // tslint:disable-next-line:no-bitwise | ||
263 | + codePoint >> 0x6 | 0xC0, | ||
264 | + // tslint:disable-next-line:no-bitwise | ||
265 | + codePoint & 0x3F | 0x80 | ||
266 | + ); | ||
267 | + } else if (codePoint < 0x10000) { | ||
268 | + units -= 3; | ||
269 | + if (units < 0) { break; } | ||
270 | + bytes.push( | ||
271 | + // tslint:disable-next-line:no-bitwise | ||
272 | + codePoint >> 0xC | 0xE0, | ||
273 | + // tslint:disable-next-line:no-bitwise | ||
274 | + codePoint >> 0x6 & 0x3F | 0x80, | ||
275 | + // tslint:disable-next-line:no-bitwise | ||
276 | + codePoint & 0x3F | 0x80 | ||
277 | + ); | ||
278 | + } else if (codePoint < 0x200000) { | ||
279 | + units -= 4; | ||
280 | + if (units < 0) { break; } | ||
281 | + bytes.push( | ||
282 | + // tslint:disable-next-line:no-bitwise | ||
283 | + codePoint >> 0x12 | 0xF0, | ||
284 | + // tslint:disable-next-line:no-bitwise | ||
285 | + codePoint >> 0xC & 0x3F | 0x80, | ||
286 | + // tslint:disable-next-line:no-bitwise | ||
287 | + codePoint >> 0x6 & 0x3F | 0x80, | ||
288 | + // tslint:disable-next-line:no-bitwise | ||
289 | + codePoint & 0x3F | 0x80 | ||
290 | + ); | ||
291 | + } else { | ||
292 | + throw new Error('Invalid code point'); | ||
293 | + } | ||
294 | + } | ||
295 | + return bytes; | ||
296 | +} |
@@ -16,15 +16,15 @@ | @@ -16,15 +16,15 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" | 18 | <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" |
19 | - [ngStyle]="options.dashboardStyle" | ||
20 | - [fxShow]="(loading() | async) && !options.isEdit"> | 19 | + [ngStyle]="dashboardStyle" |
20 | + [fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit"> | ||
21 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> | 21 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> |
22 | </mat-spinner> | 22 | </mat-spinner> |
23 | </div> | 23 | </div> |
24 | <div id="gridster-parent" | 24 | <div id="gridster-parent" |
25 | fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" | 25 | fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" |
26 | (contextmenu)="openDashboardContextMenu($event)"> | 26 | (contextmenu)="openDashboardContextMenu($event)"> |
27 | - <div [ngClass]="options.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> | 27 | + <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> |
28 | <gridster #gridster id="gridster-child" [options]="gridsterOpts"> | 28 | <gridster #gridster id="gridster-child" [options]="gridsterOpts"> |
29 | <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async"> | 29 | <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async"> |
30 | <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" | 30 | <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" |
@@ -58,42 +58,42 @@ | @@ -58,42 +58,42 @@ | ||
58 | fxLayoutAlign="start center" | 58 | fxLayoutAlign="start center" |
59 | (mousedown)="$event.stopPropagation()"> | 59 | (mousedown)="$event.stopPropagation()"> |
60 | <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions" | 60 | <button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions" |
61 | - [fxShow]="!options.isEdit" | 61 | + [fxShow]="!isEdit" |
62 | (click)="action.onAction($event)" | 62 | (click)="action.onAction($event)" |
63 | matTooltip="{{ action.displayName }}" | 63 | matTooltip="{{ action.displayName }}" |
64 | matTooltipPosition="above"> | 64 | matTooltipPosition="above"> |
65 | <mat-icon>{{ action.icon }}</mat-icon> | 65 | <mat-icon>{{ action.icon }}</mat-icon> |
66 | </button> | 66 | </button> |
67 | <button mat-button mat-icon-button *ngFor="let action of widget.widgetActions" | 67 | <button mat-button mat-icon-button *ngFor="let action of widget.widgetActions" |
68 | - [fxShow]="!options.isEdit && action.show" | 68 | + [fxShow]="!isEdit && action.show" |
69 | (click)="action.onAction($event)" | 69 | (click)="action.onAction($event)" |
70 | matTooltip="{{ action.name | translate }}" | 70 | matTooltip="{{ action.name | translate }}" |
71 | matTooltipPosition="above"> | 71 | matTooltipPosition="above"> |
72 | <mat-icon>{{ action.icon }}</mat-icon> | 72 | <mat-icon>{{ action.icon }}</mat-icon> |
73 | </button> | 73 | </button> |
74 | <button mat-button mat-icon-button | 74 | <button mat-button mat-icon-button |
75 | - [fxShow]="!options.isEdit && widget.enableFullscreen" | 75 | + [fxShow]="!isEdit && widget.enableFullscreen" |
76 | (click)="widget.isFullscreen = !widget.isFullscreen" | 76 | (click)="widget.isFullscreen = !widget.isFullscreen" |
77 | matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | 77 | matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
78 | matTooltipPosition="above"> | 78 | matTooltipPosition="above"> |
79 | <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | 79 | <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
80 | </button> | 80 | </button> |
81 | <button mat-button mat-icon-button | 81 | <button mat-button mat-icon-button |
82 | - [fxShow]="options.isEditActionEnabled && !widget.isFullscreen" | 82 | + [fxShow]="isEditActionEnabled && !widget.isFullscreen" |
83 | (click)="editWidget($event, widget)" | 83 | (click)="editWidget($event, widget)" |
84 | matTooltip="{{ 'widget.edit' | translate }}" | 84 | matTooltip="{{ 'widget.edit' | translate }}" |
85 | matTooltipPosition="above"> | 85 | matTooltipPosition="above"> |
86 | <mat-icon>edit</mat-icon> | 86 | <mat-icon>edit</mat-icon> |
87 | </button> | 87 | </button> |
88 | <button mat-button mat-icon-button | 88 | <button mat-button mat-icon-button |
89 | - [fxShow]="options.isExportActionEnabled && !widget.isFullscreen" | 89 | + [fxShow]="isExportActionEnabled && !widget.isFullscreen" |
90 | (click)="exportWidget($event, widget)" | 90 | (click)="exportWidget($event, widget)" |
91 | matTooltip="{{ 'widget.export' | translate }}" | 91 | matTooltip="{{ 'widget.export' | translate }}" |
92 | matTooltipPosition="above"> | 92 | matTooltipPosition="above"> |
93 | <mat-icon>file_download</mat-icon> | 93 | <mat-icon>file_download</mat-icon> |
94 | </button> | 94 | </button> |
95 | <button mat-button mat-icon-button | 95 | <button mat-button mat-icon-button |
96 | - [fxShow]="options.isRemoveActionEnabled && !widget.isFullscreen" | 96 | + [fxShow]="isRemoveActionEnabled && !widget.isFullscreen" |
97 | (click)="removeWidget($event, widget)" | 97 | (click)="removeWidget($event, widget)" |
98 | matTooltip="{{ 'widget.remove' | translate }}" | 98 | matTooltip="{{ 'widget.remove' | translate }}" |
99 | matTooltipPosition="above"> | 99 | matTooltipPosition="above"> |
@@ -102,7 +102,12 @@ | @@ -102,7 +102,12 @@ | ||
102 | </div> | 102 | </div> |
103 | </div> | 103 | </div> |
104 | <div fxFlex fxLayout="column" class="tb-widget-content"> | 104 | <div fxFlex fxLayout="column" class="tb-widget-content"> |
105 | - | 105 | + <tb-widget fxFlex |
106 | + [dashboardWidget]="widget" | ||
107 | + [isEdit]="isEdit" | ||
108 | + [isMobile]="isMobileSize" | ||
109 | + [dashboard]="this"> | ||
110 | + </tb-widget> | ||
106 | </div> | 111 | </div> |
107 | </div> | 112 | </div> |
108 | </gridster-item> | 113 | </gridster-item> |
@@ -14,40 +14,108 @@ | @@ -14,40 +14,108 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, OnInit, Input, ViewChild, AfterViewInit, ViewChildren, QueryList, ElementRef } from '@angular/core'; | 17 | +import { |
18 | + AfterViewInit, | ||
19 | + Component, | ||
20 | + Input, | ||
21 | + OnChanges, | ||
22 | + OnInit, | ||
23 | + QueryList, | ||
24 | + SimpleChanges, | ||
25 | + ViewChild, | ||
26 | + ViewChildren | ||
27 | +} from '@angular/core'; | ||
18 | import { Store } from '@ngrx/store'; | 28 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 29 | import { AppState } from '@core/core.state'; |
20 | import { PageComponent } from '@shared/components/page.component'; | 30 | import { PageComponent } from '@shared/components/page.component'; |
21 | import { AuthUser } from '@shared/models/user.model'; | 31 | import { AuthUser } from '@shared/models/user.model'; |
22 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | 32 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
23 | -import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
24 | import { Timewindow } from '@shared/models/time/time.models'; | 33 | import { Timewindow } from '@shared/models/time/time.models'; |
25 | import { TimeService } from '@core/services/time.service'; | 34 | import { TimeService } from '@core/services/time.service'; |
26 | import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2'; | 35 | import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2'; |
27 | -import { GridsterResizable } from 'angular-gridster2/lib/gridsterResizable.service'; | ||
28 | -import { IDashboardComponent, DashboardConfig, DashboardWidget } from '../../models/dashboard-component.models'; | ||
29 | -import { MatSort } from '@angular/material/sort'; | ||
30 | -import { Observable, ReplaySubject, merge } from 'rxjs'; | 36 | +import { |
37 | + DashboardCallbacks, | ||
38 | + DashboardWidget, | ||
39 | + IDashboardComponent, | ||
40 | + WidgetsData | ||
41 | +} from '../../models/dashboard-component.models'; | ||
42 | +import { merge, Observable } from 'rxjs'; | ||
31 | import { map, share, tap } from 'rxjs/operators'; | 43 | import { map, share, tap } from 'rxjs/operators'; |
32 | import { WidgetLayout } from '@shared/models/dashboard.models'; | 44 | import { WidgetLayout } from '@shared/models/dashboard.models'; |
33 | import { DialogService } from '@core/services/dialog.service'; | 45 | import { DialogService } from '@core/services/dialog.service'; |
34 | -import { Widget } from '@app/shared/models/widget.models'; | ||
35 | -import { MatTab } from '@angular/material/tabs'; | ||
36 | import { animatedScroll, isDefined } from '@app/core/utils'; | 46 | import { animatedScroll, isDefined } from '@app/core/utils'; |
37 | -import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; | 47 | +import { BreakpointObserver } from '@angular/cdk/layout'; |
38 | import { MediaBreakpoints } from '@shared/models/constants'; | 48 | import { MediaBreakpoints } from '@shared/models/constants'; |
49 | +import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | ||
39 | 50 | ||
40 | @Component({ | 51 | @Component({ |
41 | selector: 'tb-dashboard', | 52 | selector: 'tb-dashboard', |
42 | templateUrl: './dashboard.component.html', | 53 | templateUrl: './dashboard.component.html', |
43 | styleUrls: ['./dashboard.component.scss'] | 54 | styleUrls: ['./dashboard.component.scss'] |
44 | }) | 55 | }) |
45 | -export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit { | 56 | +export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit, OnChanges { |
46 | 57 | ||
47 | authUser: AuthUser; | 58 | authUser: AuthUser; |
48 | 59 | ||
49 | @Input() | 60 | @Input() |
50 | - options: DashboardConfig; | 61 | + widgetsData: Observable<WidgetsData>; |
62 | + | ||
63 | + @Input() | ||
64 | + callbacks: DashboardCallbacks; | ||
65 | + | ||
66 | + @Input() | ||
67 | + aliasController: IAliasController; | ||
68 | + | ||
69 | + @Input() | ||
70 | + stateController: IStateController; | ||
71 | + | ||
72 | + @Input() | ||
73 | + columns: number; | ||
74 | + | ||
75 | + @Input() | ||
76 | + horizontalMargin: number; | ||
77 | + | ||
78 | + @Input() | ||
79 | + verticalMargin: number; | ||
80 | + | ||
81 | + @Input() | ||
82 | + isEdit: boolean; | ||
83 | + | ||
84 | + @Input() | ||
85 | + autofillHeight: boolean; | ||
86 | + | ||
87 | + @Input() | ||
88 | + mobileAutofillHeight: boolean; | ||
89 | + | ||
90 | + @Input() | ||
91 | + mobileRowHeight: number; | ||
92 | + | ||
93 | + @Input() | ||
94 | + isMobile: boolean; | ||
95 | + | ||
96 | + @Input() | ||
97 | + isMobileDisabled: boolean; | ||
98 | + | ||
99 | + @Input() | ||
100 | + isEditActionEnabled: boolean; | ||
101 | + | ||
102 | + @Input() | ||
103 | + isExportActionEnabled: boolean; | ||
104 | + | ||
105 | + @Input() | ||
106 | + isRemoveActionEnabled: boolean; | ||
107 | + | ||
108 | + @Input() | ||
109 | + dashboardStyle: {[klass: string]: any}; | ||
110 | + | ||
111 | + @Input() | ||
112 | + dashboardClass: string; | ||
113 | + | ||
114 | + @Input() | ||
115 | + ignoreLoading: boolean; | ||
116 | + | ||
117 | + @Input() | ||
118 | + dashboardTimewindow: Timewindow; | ||
51 | 119 | ||
52 | gridsterOpts: GridsterConfig; | 120 | gridsterOpts: GridsterConfig; |
53 | 121 | ||
@@ -77,8 +145,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -77,8 +145,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
77 | } | 145 | } |
78 | 146 | ||
79 | ngOnInit(): void { | 147 | ngOnInit(): void { |
80 | - if (!this.options.dashboardTimewindow) { | ||
81 | - this.options.dashboardTimewindow = this.timeService.defaultTimewindow(); | 148 | + if (!this.dashboardTimewindow) { |
149 | + this.dashboardTimewindow = this.timeService.defaultTimewindow(); | ||
82 | } | 150 | } |
83 | this.gridsterOpts = { | 151 | this.gridsterOpts = { |
84 | gridType: 'scrollVertical', | 152 | gridType: 'scrollVertical', |
@@ -86,35 +154,65 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -86,35 +154,65 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
86 | pushItems: false, | 154 | pushItems: false, |
87 | swap: false, | 155 | swap: false, |
88 | maxRows: 100, | 156 | maxRows: 100, |
89 | - minCols: this.options.columns ? this.options.columns : 24, | 157 | + minCols: this.columns ? this.columns : 24, |
90 | outerMargin: true, | 158 | outerMargin: true, |
91 | - outerMarginLeft: this.options.margins ? this.options.margins[0] : 10, | ||
92 | - outerMarginRight: this.options.margins ? this.options.margins[0] : 10, | ||
93 | - outerMarginTop: this.options.margins ? this.options.margins[1] : 10, | ||
94 | - outerMarginBottom: this.options.margins ? this.options.margins[1] : 10, | 159 | + outerMarginLeft: this.horizontalMargin ? this.horizontalMargin : 10, |
160 | + outerMarginRight: this.horizontalMargin ? this.horizontalMargin : 10, | ||
161 | + outerMarginTop: this.verticalMargin ? this.verticalMargin : 10, | ||
162 | + outerMarginBottom: this.horizontalMargin ? this.horizontalMargin : 10, | ||
95 | minItemCols: 1, | 163 | minItemCols: 1, |
96 | minItemRows: 1, | 164 | minItemRows: 1, |
97 | defaultItemCols: 8, | 165 | defaultItemCols: 8, |
98 | defaultItemRows: 6, | 166 | defaultItemRows: 6, |
99 | - resizable: {enabled: this.options.isEdit}, | ||
100 | - draggable: {enabled: this.options.isEdit} | 167 | + resizable: {enabled: this.isEdit}, |
168 | + draggable: {enabled: this.isEdit}, | ||
169 | + itemChangeCallback: item => this.sortWidgets(this.widgets) | ||
101 | }; | 170 | }; |
102 | 171 | ||
103 | - this.updateGridsterOpts(); | 172 | + this.updateMobileOpts(); |
104 | 173 | ||
105 | this.loadDashboard(); | 174 | this.loadDashboard(); |
106 | 175 | ||
107 | - merge(this.breakpointObserver | ||
108 | - .observe(MediaBreakpoints['gt-sm']), this.options.layoutChange$).subscribe( | 176 | + this.breakpointObserver |
177 | + .observe(MediaBreakpoints['gt-sm']).subscribe( | ||
109 | () => { | 178 | () => { |
110 | - this.updateGridsterOpts(); | ||
111 | - this.sortWidgets(this.widgets); | 179 | + this.updateMobileOpts(); |
112 | } | 180 | } |
113 | ); | 181 | ); |
114 | } | 182 | } |
115 | 183 | ||
184 | + ngOnChanges(changes: SimpleChanges): void { | ||
185 | + let updateMobileOpts = false; | ||
186 | + let updateLayoutOpts = false; | ||
187 | + let updateEditingOpts = false; | ||
188 | + for (const propName of Object.keys(changes)) { | ||
189 | + const change = changes[propName]; | ||
190 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | ||
191 | + if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { | ||
192 | + updateMobileOpts = true; | ||
193 | + } else if (['horizontalMargin', 'verticalMargin'].includes(propName)) { | ||
194 | + updateLayoutOpts = true; | ||
195 | + } else if (propName === 'isEdit') { | ||
196 | + updateEditingOpts = true; | ||
197 | + } | ||
198 | + } | ||
199 | + } | ||
200 | + if (updateMobileOpts) { | ||
201 | + this.updateMobileOpts(); | ||
202 | + } | ||
203 | + if (updateLayoutOpts) { | ||
204 | + this.updateLayoutOpts(); | ||
205 | + } | ||
206 | + if (updateEditingOpts) { | ||
207 | + this.updateEditingOpts(); | ||
208 | + } | ||
209 | + if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) { | ||
210 | + this.notifyGridsterOptionsChanged(); | ||
211 | + } | ||
212 | + } | ||
213 | + | ||
116 | loadDashboard() { | 214 | loadDashboard() { |
117 | - this.widgets$ = this.options.widgetsData.pipe( | 215 | + this.widgets$ = this.widgetsData.pipe( |
118 | map(widgetsData => { | 216 | map(widgetsData => { |
119 | const dashboardWidgets = new Array<DashboardWidget>(); | 217 | const dashboardWidgets = new Array<DashboardWidget>(); |
120 | let maxRows = this.gridsterOpts.maxRows; | 218 | let maxRows = this.gridsterOpts.maxRows; |
@@ -164,19 +262,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -164,19 +262,12 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
164 | 262 | ||
165 | isAutofillHeight(): boolean { | 263 | isAutofillHeight(): boolean { |
166 | if (this.isMobileSize) { | 264 | if (this.isMobileSize) { |
167 | - return isDefined(this.options.mobileAutofillHeight) ? this.options.mobileAutofillHeight : false; | 265 | + return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false; |
168 | } else { | 266 | } else { |
169 | - return isDefined(this.options.autofillHeight) ? this.options.autofillHeight : false; | 267 | + return isDefined(this.autofillHeight) ? this.autofillHeight : false; |
170 | } | 268 | } |
171 | } | 269 | } |
172 | 270 | ||
173 | - loading(): Observable<boolean> { | ||
174 | - return this.isLoading$.pipe( | ||
175 | - map(loading => (!this.options.ignoreLoading && loading) || this.dashboardLoading), | ||
176 | - share() | ||
177 | - ); | ||
178 | - } | ||
179 | - | ||
180 | openDashboardContextMenu($event: Event) { | 271 | openDashboardContextMenu($event: Event) { |
181 | // TODO: | 272 | // TODO: |
182 | // this.dialogService.todo(); | 273 | // this.dialogService.todo(); |
@@ -192,14 +283,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -192,14 +283,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
192 | } | 283 | } |
193 | 284 | ||
194 | widgetMouseDown($event: Event, widget: DashboardWidget) { | 285 | widgetMouseDown($event: Event, widget: DashboardWidget) { |
195 | - if (this.options.onWidgetMouseDown) { | ||
196 | - this.options.onWidgetMouseDown($event, widget.widget); | 286 | + if (this.callbacks && this.callbacks.onWidgetMouseDown) { |
287 | + this.callbacks.onWidgetMouseDown($event, widget.widget); | ||
197 | } | 288 | } |
198 | } | 289 | } |
199 | 290 | ||
200 | widgetClicked($event: Event, widget: DashboardWidget) { | 291 | widgetClicked($event: Event, widget: DashboardWidget) { |
201 | - if (this.options.onWidgetClicked) { | ||
202 | - this.options.onWidgetClicked($event, widget.widget); | 292 | + if (this.callbacks && this.callbacks.onWidgetClicked) { |
293 | + this.callbacks.onWidgetClicked($event, widget.widget); | ||
203 | } | 294 | } |
204 | } | 295 | } |
205 | 296 | ||
@@ -207,8 +298,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -207,8 +298,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
207 | if ($event) { | 298 | if ($event) { |
208 | $event.stopPropagation(); | 299 | $event.stopPropagation(); |
209 | } | 300 | } |
210 | - if (this.options.isEditActionEnabled && this.options.onEditWidget) { | ||
211 | - this.options.onEditWidget($event, widget.widget); | 301 | + if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { |
302 | + this.callbacks.onEditWidget($event, widget.widget); | ||
212 | } | 303 | } |
213 | } | 304 | } |
214 | 305 | ||
@@ -216,8 +307,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -216,8 +307,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
216 | if ($event) { | 307 | if ($event) { |
217 | $event.stopPropagation(); | 308 | $event.stopPropagation(); |
218 | } | 309 | } |
219 | - if (this.options.isExportActionEnabled && this.options.onExportWidget) { | ||
220 | - this.options.onExportWidget($event, widget.widget); | 310 | + if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { |
311 | + this.callbacks.onExportWidget($event, widget.widget); | ||
221 | } | 312 | } |
222 | } | 313 | } |
223 | 314 | ||
@@ -225,8 +316,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -225,8 +316,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
225 | if ($event) { | 316 | if ($event) { |
226 | $event.stopPropagation(); | 317 | $event.stopPropagation(); |
227 | } | 318 | } |
228 | - if (this.options.isRemoveActionEnabled && this.options.onRemoveWidget) { | ||
229 | - this.options.onRemoveWidget($event, widget.widget); | 319 | + if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { |
320 | + this.callbacks.onRemoveWidget($event, widget.widget); | ||
230 | } | 321 | } |
231 | } | 322 | } |
232 | 323 | ||
@@ -272,7 +363,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -272,7 +363,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
272 | } | 363 | } |
273 | } | 364 | } |
274 | 365 | ||
275 | - private updateGridsterOpts() { | 366 | + private updateMobileOpts() { |
276 | this.isMobileSize = this.checkIsMobileSize(); | 367 | this.isMobileSize = this.checkIsMobileSize(); |
277 | const mobileBreakPoint = this.isMobileSize ? 20000 : 0; | 368 | const mobileBreakPoint = this.isMobileSize ? 20000 : 0; |
278 | this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; | 369 | this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; |
@@ -285,6 +376,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -285,6 +376,21 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
285 | } else { | 376 | } else { |
286 | this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; | 377 | this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; |
287 | } | 378 | } |
379 | + } | ||
380 | + | ||
381 | + private updateLayoutOpts() { | ||
382 | + this.gridsterOpts.outerMarginLeft = this.horizontalMargin ? this.horizontalMargin : 10; | ||
383 | + this.gridsterOpts.outerMarginRight = this.horizontalMargin ? this.horizontalMargin : 10; | ||
384 | + this.gridsterOpts.outerMarginTop = this.verticalMargin ? this.verticalMargin : 10; | ||
385 | + this.gridsterOpts.outerMarginBottom = this.horizontalMargin ? this.horizontalMargin : 10; | ||
386 | + } | ||
387 | + | ||
388 | + private updateEditingOpts() { | ||
389 | + this.gridsterOpts.resizable.enabled = this.isEdit; | ||
390 | + this.gridsterOpts.draggable.enabled = this.isEdit; | ||
391 | + } | ||
392 | + | ||
393 | + private notifyGridsterOptionsChanged() { | ||
288 | if (this.gridster && this.gridster.options) { | 394 | if (this.gridster && this.gridster.options) { |
289 | this.gridster.optionsChanged(); | 395 | this.gridster.optionsChanged(); |
290 | } | 396 | } |
@@ -294,15 +400,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -294,15 +400,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
294 | let rowHeight = null; | 400 | let rowHeight = null; |
295 | if (!this.isAutofillHeight()) { | 401 | if (!this.isAutofillHeight()) { |
296 | if (isMobile) { | 402 | if (isMobile) { |
297 | - rowHeight = isDefined(this.options.mobileRowHeight) ? this.options.mobileRowHeight : 70; | 403 | + rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70; |
298 | } | 404 | } |
299 | } | 405 | } |
300 | return rowHeight; | 406 | return rowHeight; |
301 | } | 407 | } |
302 | 408 | ||
303 | private checkIsMobileSize(): boolean { | 409 | private checkIsMobileSize(): boolean { |
304 | - const isMobileDisabled = this.options.isMobileDisabled === true; | ||
305 | - let isMobileSize = this.options.isMobile === true && !isMobileDisabled; | 410 | + const isMobileDisabled = this.isMobileDisabled === true; |
411 | + let isMobileSize = this.isMobile === true && !isMobileDisabled; | ||
306 | if (!isMobileSize && !isMobileDisabled) { | 412 | if (!isMobileSize && !isMobileDisabled) { |
307 | isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | 413 | isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); |
308 | } | 414 | } |
@@ -14,14 +14,14 @@ | @@ -14,14 +14,14 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import {NgModule} from '@angular/core'; | ||
18 | -import {CommonModule} from '@angular/common'; | ||
19 | -import {SharedModule} from '@app/shared/shared.module'; | ||
20 | -import {AddEntityDialogComponent} from './entity/add-entity-dialog.component'; | ||
21 | -import {EntitiesTableComponent} from './entity/entities-table.component'; | ||
22 | -import {DetailsPanelComponent} from './details-panel.component'; | ||
23 | -import {EntityDetailsPanelComponent} from './entity/entity-details-panel.component'; | ||
24 | -import {ContactComponent} from './contact.component'; | 17 | +import { NgModule } from '@angular/core'; |
18 | +import { CommonModule } from '@angular/common'; | ||
19 | +import { SharedModule } from '@app/shared/shared.module'; | ||
20 | +import { AddEntityDialogComponent } from './entity/add-entity-dialog.component'; | ||
21 | +import { EntitiesTableComponent } from './entity/entities-table.component'; | ||
22 | +import { DetailsPanelComponent } from './details-panel.component'; | ||
23 | +import { EntityDetailsPanelComponent } from './entity/entity-details-panel.component'; | ||
24 | +import { ContactComponent } from './contact.component'; | ||
25 | import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-dialog.component'; | 25 | import { AuditLogDetailsDialogComponent } from './audit-log/audit-log-details-dialog.component'; |
26 | import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; | 26 | import { AuditLogTableComponent } from './audit-log/audit-log-table.component'; |
27 | import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; | 27 | import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; |
@@ -35,6 +35,8 @@ import { AttributeTableComponent } from '@home/components/attribute/attribute-ta | @@ -35,6 +35,8 @@ import { AttributeTableComponent } from '@home/components/attribute/attribute-ta | ||
35 | import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component'; | 35 | import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component'; |
36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; | 36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; |
37 | import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; | 37 | import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; |
38 | +import { WidgetComponent } from '@home/components/widget/widget.component'; | ||
39 | +import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-component-factory.service'; | ||
38 | 40 | ||
39 | @NgModule({ | 41 | @NgModule({ |
40 | entryComponents: [ | 42 | entryComponents: [ |
@@ -66,7 +68,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone | @@ -66,7 +68,8 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone | ||
66 | AttributeTableComponent, | 68 | AttributeTableComponent, |
67 | AddAttributeDialogComponent, | 69 | AddAttributeDialogComponent, |
68 | EditAttributeValuePanelComponent, | 70 | EditAttributeValuePanelComponent, |
69 | - DashboardComponent | 71 | + DashboardComponent, |
72 | + WidgetComponent | ||
70 | ], | 73 | ], |
71 | imports: [ | 74 | imports: [ |
72 | CommonModule, | 75 | CommonModule, |
@@ -84,7 +87,11 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone | @@ -84,7 +87,11 @@ import { DashboardComponent } from '@home/components/dashboard/dashboard.compone | ||
84 | AlarmTableComponent, | 87 | AlarmTableComponent, |
85 | AlarmDetailsDialogComponent, | 88 | AlarmDetailsDialogComponent, |
86 | AttributeTableComponent, | 89 | AttributeTableComponent, |
87 | - DashboardComponent | 90 | + DashboardComponent, |
91 | + WidgetComponent | ||
92 | + ], | ||
93 | + providers: [ | ||
94 | + DynamicWidgetComponentFactoryService | ||
88 | ] | 95 | ] |
89 | }) | 96 | }) |
90 | export class HomeComponentsModule { } | 97 | export class HomeComponentsModule { } |
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,92 +22,33 @@ import { Timewindow } from '@shared/models/time/time.models'; | ||
22 | import { Observable } from 'rxjs'; | 22 | import { Observable } from 'rxjs'; |
23 | import { isDefined, isUndefined } from '@app/core/utils'; | 23 | import { isDefined, isUndefined } from '@app/core/utils'; |
24 | import { EventEmitter } from '@angular/core'; | 24 | import { EventEmitter } from '@angular/core'; |
25 | - | ||
26 | -export interface IAliasController { | ||
27 | - [key: string]: any | null; | ||
28 | - // TODO: | ||
29 | -} | 25 | +import { EntityId } from '@app/shared/models/id/entity-id'; |
26 | +import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | ||
30 | 27 | ||
31 | export interface WidgetsData { | 28 | export interface WidgetsData { |
32 | widgets: Array<Widget>; | 29 | widgets: Array<Widget>; |
33 | widgetLayouts?: WidgetLayouts; | 30 | widgetLayouts?: WidgetLayouts; |
34 | } | 31 | } |
35 | 32 | ||
36 | -export class DashboardConfig { | ||
37 | - widgetsData?: Observable<WidgetsData>; | ||
38 | - isEdit: boolean; | ||
39 | - isEditActionEnabled: boolean; | ||
40 | - isExportActionEnabled: boolean; | ||
41 | - isRemoveActionEnabled: boolean; | 33 | +export interface DashboardCallbacks { |
42 | onEditWidget?: ($event: Event, widget: Widget) => void; | 34 | onEditWidget?: ($event: Event, widget: Widget) => void; |
43 | onExportWidget?: ($event: Event, widget: Widget) => void; | 35 | onExportWidget?: ($event: Event, widget: Widget) => void; |
44 | onRemoveWidget?: ($event: Event, widget: Widget) => void; | 36 | onRemoveWidget?: ($event: Event, widget: Widget) => void; |
45 | onWidgetMouseDown?: ($event: Event, widget: Widget) => void; | 37 | onWidgetMouseDown?: ($event: Event, widget: Widget) => void; |
46 | onWidgetClicked?: ($event: Event, widget: Widget) => void; | 38 | onWidgetClicked?: ($event: Event, widget: Widget) => void; |
47 | - aliasController?: IAliasController; | ||
48 | - autofillHeight?: boolean; | ||
49 | - mobileAutofillHeight?: boolean; | ||
50 | - dashboardStyle?: {[klass: string]: any} | null; | ||
51 | - columns?: number; | ||
52 | - margins?: [number, number]; | ||
53 | - dashboardTimewindow?: Timewindow; | ||
54 | - ignoreLoading?: boolean; | ||
55 | - dashboardClass?: string; | ||
56 | - mobileRowHeight?: number; | ||
57 | - | ||
58 | - private isMobileValue: boolean; | ||
59 | - private isMobileDisabledValue: boolean; | ||
60 | - | ||
61 | - private layoutChange = new EventEmitter(); | ||
62 | - layoutChange$ = this.layoutChange.asObservable(); | ||
63 | - layoutChangeTimeout = null; | ||
64 | - | ||
65 | - set isMobile(isMobile: boolean) { | ||
66 | - if (this.isMobileValue !== isMobile) { | ||
67 | - const changed = isDefined(this.isMobileValue); | ||
68 | - this.isMobileValue = isMobile; | ||
69 | - if (changed) { | ||
70 | - this.notifyLayoutChanged(); | ||
71 | - } | ||
72 | - } | ||
73 | - } | ||
74 | - get isMobile(): boolean { | ||
75 | - return this.isMobileValue; | ||
76 | - } | ||
77 | - | ||
78 | - set isMobileDisabled(isMobileDisabled: boolean) { | ||
79 | - if (this.isMobileDisabledValue !== isMobileDisabled) { | ||
80 | - const changed = isDefined(this.isMobileDisabledValue); | ||
81 | - this.isMobileDisabledValue = isMobileDisabled; | ||
82 | - if (changed) { | ||
83 | - this.notifyLayoutChanged(); | ||
84 | - } | ||
85 | - } | ||
86 | - } | ||
87 | - get isMobileDisabled(): boolean { | ||
88 | - return this.isMobileDisabledValue; | ||
89 | - } | ||
90 | - | ||
91 | - private notifyLayoutChanged() { | ||
92 | - if (this.layoutChangeTimeout) { | ||
93 | - clearTimeout(this.layoutChangeTimeout); | ||
94 | - } | ||
95 | - this.layoutChangeTimeout = setTimeout(() => { | ||
96 | - this.doNotifyLayoutChanged(); | ||
97 | - }, 0); | ||
98 | - } | ||
99 | - | ||
100 | - private doNotifyLayoutChanged() { | ||
101 | - this.layoutChange.emit(); | ||
102 | - this.layoutChangeTimeout = null; | ||
103 | - } | 39 | + prepareDashboardContextMenu?: ($event: Event) => void; |
40 | + prepareWidgetContextMenu?: ($event: Event, widget: Widget) => void; | ||
104 | } | 41 | } |
105 | 42 | ||
106 | export interface IDashboardComponent { | 43 | export interface IDashboardComponent { |
107 | - options: DashboardConfig; | ||
108 | gridsterOpts: GridsterConfig; | 44 | gridsterOpts: GridsterConfig; |
109 | gridster: GridsterComponent; | 45 | gridster: GridsterComponent; |
46 | + mobileAutofillHeight: boolean; | ||
110 | isMobileSize: boolean; | 47 | isMobileSize: boolean; |
48 | + autofillHeight: boolean; | ||
49 | + dashboardTimewindow: Timewindow; | ||
50 | + aliasController: IAliasController; | ||
51 | + stateController: IStateController; | ||
111 | } | 52 | } |
112 | 53 | ||
113 | export class DashboardWidget implements GridsterItem { | 54 | export class DashboardWidget implements GridsterItem { |
@@ -262,7 +203,7 @@ export class DashboardWidget implements GridsterItem { | @@ -262,7 +203,7 @@ export class DashboardWidget implements GridsterItem { | ||
262 | } | 203 | } |
263 | 204 | ||
264 | get rows(): number { | 205 | get rows(): number { |
265 | - if (this.dashboard.isMobileSize && !this.dashboard.options.mobileAutofillHeight) { | 206 | + if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { |
266 | let mobileHeight; | 207 | let mobileHeight; |
267 | if (this.widgetLayout) { | 208 | if (this.widgetLayout) { |
268 | mobileHeight = this.widgetLayout.mobileHeight; | 209 | mobileHeight = this.widgetLayout.mobileHeight; |
@@ -285,7 +226,7 @@ export class DashboardWidget implements GridsterItem { | @@ -285,7 +226,7 @@ export class DashboardWidget implements GridsterItem { | ||
285 | } | 226 | } |
286 | 227 | ||
287 | set rows(rows: number) { | 228 | set rows(rows: number) { |
288 | - if (!this.dashboard.isMobileSize && !this.dashboard.options.autofillHeight) { | 229 | + if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { |
289 | if (this.widgetLayout) { | 230 | if (this.widgetLayout) { |
290 | this.widgetLayout.sizeY = rows; | 231 | this.widgetLayout.sizeY = rows; |
291 | } else { | 232 | } else { |
@@ -14,23 +14,75 @@ | @@ -14,23 +14,75 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | +import { ExceptionData } from '@shared/models/error.models'; | ||
18 | +import { IDashboardComponent } from '@home/models/dashboard-component.models'; | ||
19 | +import { WidgetActionDescriptor, WidgetConfig, WidgetConfigSettings, widgetType } from '@shared/models/widget.models'; | ||
20 | +import { Timewindow } from '@shared/models/time/time.models'; | ||
21 | +import { | ||
22 | + EntityInfo, | ||
23 | + IWidgetSubscription, | ||
24 | + SubscriptionInfo, | ||
25 | + WidgetSubscriptionOptions, | ||
26 | + IStateController, | ||
27 | + IAliasController, | ||
28 | + TimewindowFunctions, | ||
29 | + WidgetSubscriptionApi, | ||
30 | + RpcApi, | ||
31 | + WidgetActionsApi, | ||
32 | + IWidgetUtils | ||
33 | +} from '@core/api/widget-api.models'; | ||
34 | +import { Observable } from 'rxjs'; | ||
35 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
36 | + | ||
17 | export interface IWidgetAction { | 37 | export interface IWidgetAction { |
38 | + name: string; | ||
18 | icon: string; | 39 | icon: string; |
19 | onAction: ($event: Event) => void; | 40 | onAction: ($event: Event) => void; |
20 | } | 41 | } |
21 | 42 | ||
22 | export interface WidgetHeaderAction extends IWidgetAction { | 43 | export interface WidgetHeaderAction extends IWidgetAction { |
23 | displayName: string; | 44 | displayName: string; |
45 | + descriptor: WidgetActionDescriptor; | ||
24 | } | 46 | } |
25 | 47 | ||
26 | export interface WidgetAction extends IWidgetAction { | 48 | export interface WidgetAction extends IWidgetAction { |
27 | - name: string; | ||
28 | show: boolean; | 49 | show: boolean; |
29 | } | 50 | } |
30 | 51 | ||
52 | +export interface IDynamicWidgetComponent { | ||
53 | + widgetContext: WidgetContext; | ||
54 | + widgetErrorData: ExceptionData; | ||
55 | + loadingData: boolean; | ||
56 | + [key: string]: any; | ||
57 | +} | ||
58 | + | ||
31 | export interface WidgetContext { | 59 | export interface WidgetContext { |
32 | - widgetTitleTemplate?: string; | 60 | + inited?: boolean; |
61 | + $container?: any; | ||
62 | + $containerParent?: any; | ||
63 | + width?: number; | ||
64 | + height?: number; | ||
65 | + $scope?: IDynamicWidgetComponent; | ||
33 | hideTitlePanel?: boolean; | 66 | hideTitlePanel?: boolean; |
67 | + isEdit?: boolean; | ||
68 | + isMobile?: boolean; | ||
69 | + dashboard?: IDashboardComponent; | ||
70 | + widgetConfig?: WidgetConfig; | ||
71 | + settings?: WidgetConfigSettings; | ||
72 | + units?: string; | ||
73 | + decimals?: number; | ||
74 | + subscriptions?: {[id: string]: IWidgetSubscription}; | ||
75 | + defaultSubscription?: IWidgetSubscription; | ||
76 | + dashboardTimewindow?: Timewindow; | ||
77 | + timewindowFunctions?: TimewindowFunctions; | ||
78 | + subscriptionApi?: WidgetSubscriptionApi; | ||
79 | + controlApi?: RpcApi; | ||
80 | + utils?: IWidgetUtils; | ||
81 | + actionsApi?: WidgetActionsApi; | ||
82 | + stateController?: IStateController; | ||
83 | + aliasController?: IAliasController; | ||
84 | + activeEntityInfo?: EntityInfo; | ||
85 | + widgetTitleTemplate?: string; | ||
34 | widgetTitle?: string; | 86 | widgetTitle?: string; |
35 | customHeaderActions?: Array<WidgetHeaderAction>; | 87 | customHeaderActions?: Array<WidgetHeaderAction>; |
36 | widgetActions?: Array<WidgetAction>; | 88 | widgetActions?: Array<WidgetAction>; |
@@ -22,7 +22,7 @@ import {Authority} from '@shared/models/authority.enum'; | @@ -22,7 +22,7 @@ import {Authority} from '@shared/models/authority.enum'; | ||
22 | import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; | 22 | import {RuleChainsTableConfigResolver} from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; |
23 | import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; | 23 | import {WidgetsBundlesTableConfigResolver} from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; |
24 | import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; | 24 | import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component'; |
25 | -import { BreadCrumbConfig } from '@shared/components/breadcrumb'; | 25 | +import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; |
26 | import { User } from '@shared/models/user.model'; | 26 | import { User } from '@shared/models/user.model'; |
27 | import { Store } from '@ngrx/store'; | 27 | import { Store } from '@ngrx/store'; |
28 | import { AppState } from '@core/core.state'; | 28 | import { AppState } from '@core/core.state'; |
@@ -44,7 +44,9 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { | @@ -44,7 +44,9 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { | ||
44 | } | 44 | } |
45 | } | 45 | } |
46 | 46 | ||
47 | -const routes: Routes = [ | 47 | +export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction = ((route, translate) => route.data.widgetsBundle.title); |
48 | + | ||
49 | +export const routes: Routes = [ | ||
48 | { | 50 | { |
49 | path: 'widgets-bundles', | 51 | path: 'widgets-bundles', |
50 | data: { | 52 | data: { |
@@ -72,7 +74,7 @@ const routes: Routes = [ | @@ -72,7 +74,7 @@ const routes: Routes = [ | ||
72 | auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], | 74 | auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], |
73 | title: 'widget.widget-library', | 75 | title: 'widget.widget-library', |
74 | breadcrumb: { | 76 | breadcrumb: { |
75 | - labelFunction: ((route, translate) => route.data.widgetsBundle.title), | 77 | + labelFunction: widgetTypesBreadcumbLabelFunction, |
76 | icon: 'now_widgets' | 78 | icon: 'now_widgets' |
77 | } as BreadCrumbConfig | 79 | } as BreadCrumbConfig |
78 | }, | 80 | }, |
@@ -27,6 +27,12 @@ | @@ -27,6 +27,12 @@ | ||
27 | style="text-transform: uppercase; display: flex;" | 27 | style="text-transform: uppercase; display: flex;" |
28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> | 28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> |
29 | </section> | 29 | </section> |
30 | -<tb-dashboard [options]="dashboardOptions"></tb-dashboard> | 30 | +<tb-dashboard [aliasController]="aliasController" |
31 | + [widgetsData]="widgetsData" | ||
32 | + [isEdit]="false" | ||
33 | + [isEditActionEnabled]="true" | ||
34 | + [isExportActionEnabled]="true" | ||
35 | + [isRemoveActionEnabled]="!isReadOnly" | ||
36 | + [callbacks]="dashboardCallbacks"></tb-dashboard> | ||
31 | <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> | 37 | <tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> |
32 | </tb-footer-fab-buttons> | 38 | </tb-footer-fab-buttons> |
@@ -24,14 +24,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | @@ -24,14 +24,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
24 | import { ActivatedRoute } from '@angular/router'; | 24 | import { ActivatedRoute } from '@angular/router'; |
25 | import { Authority } from '@shared/models/authority.enum'; | 25 | import { Authority } from '@shared/models/authority.enum'; |
26 | import { NULL_UUID } from '@shared/models/id/has-uuid'; | 26 | import { NULL_UUID } from '@shared/models/id/has-uuid'; |
27 | -import { Observable, of } from 'rxjs'; | 27 | +import { Observable } from 'rxjs'; |
28 | import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models'; | 28 | import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models'; |
29 | import { WidgetService } from '@core/http/widget.service'; | 29 | import { WidgetService } from '@core/http/widget.service'; |
30 | import { map, share } from 'rxjs/operators'; | 30 | import { map, share } from 'rxjs/operators'; |
31 | import { DialogService } from '@core/services/dialog.service'; | 31 | import { DialogService } from '@core/services/dialog.service'; |
32 | -import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations'; | ||
33 | import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; | 32 | import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; |
34 | -import { DashboardConfig } from '@home/models/dashboard-component.models'; | 33 | +import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; |
34 | +import { IAliasController } from '@app/core/api/widget-api.models'; | ||
35 | 35 | ||
36 | @Component({ | 36 | @Component({ |
37 | selector: 'tb-widget-library', | 37 | selector: 'tb-widget-library', |
@@ -69,19 +69,21 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | @@ -69,19 +69,21 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | ||
69 | ] | 69 | ] |
70 | }; | 70 | }; |
71 | 71 | ||
72 | - dashboardOptions: DashboardConfig = new DashboardConfig(); | 72 | + dashboardCallbacks: DashboardCallbacks = { |
73 | + onEditWidget: this.openWidgetType.bind(this), | ||
74 | + onExportWidget: this.exportWidgetType.bind(this), | ||
75 | + onRemoveWidget: this.removeWidgetType.bind(this) | ||
76 | + }; | ||
77 | + | ||
78 | + widgetsData: Observable<WidgetsData>; | ||
79 | + | ||
80 | + aliasController: IAliasController = {}; | ||
73 | 81 | ||
74 | constructor(protected store: Store<AppState>, | 82 | constructor(protected store: Store<AppState>, |
75 | private route: ActivatedRoute, | 83 | private route: ActivatedRoute, |
76 | private widgetService: WidgetService, | 84 | private widgetService: WidgetService, |
77 | private dialogService: DialogService) { | 85 | private dialogService: DialogService) { |
78 | super(store); | 86 | super(store); |
79 | - this.dashboardOptions.isEdit = false; | ||
80 | - this.dashboardOptions.isEditActionEnabled = true; | ||
81 | - this.dashboardOptions.isExportActionEnabled = true; | ||
82 | - this.dashboardOptions.onEditWidget = ($event, widget) => { this.openWidgetType($event, widget); }; | ||
83 | - this.dashboardOptions.onExportWidget = ($event, widget) => { this.exportWidgetType($event, widget); }; | ||
84 | - this.dashboardOptions.onRemoveWidget = ($event, widget) => { this.removeWidgetType($event, widget); }; | ||
85 | 87 | ||
86 | this.authUser = getCurrentAuthUser(store); | 88 | this.authUser = getCurrentAuthUser(store); |
87 | this.widgetsBundle = this.route.snapshot.data.widgetsBundle; | 89 | this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
@@ -90,9 +92,8 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | @@ -90,9 +92,8 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | ||
90 | } else { | 92 | } else { |
91 | this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; | 93 | this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
92 | } | 94 | } |
93 | - this.dashboardOptions.isRemoveActionEnabled = !this.isReadOnly; | ||
94 | this.loadWidgetTypes(); | 95 | this.loadWidgetTypes(); |
95 | - this.dashboardOptions.widgetsData = this.widgetTypes$.pipe( | 96 | + this.widgetsData = this.widgetTypes$.pipe( |
96 | map(widgets => ({ widgets }))); | 97 | map(widgets => ({ widgets }))); |
97 | } | 98 | } |
98 | 99 |
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,19 +87,50 @@ export interface WidgetResource { | ||
87 | url: string; | 87 | url: string; |
88 | } | 88 | } |
89 | 89 | ||
90 | +export interface WidgetActionSource { | ||
91 | + name: string; | ||
92 | + value: string; | ||
93 | + multiple: boolean; | ||
94 | +} | ||
95 | + | ||
96 | +export const widgetActionSources: {[key: string]: WidgetActionSource} = { | ||
97 | + headerButton: | ||
98 | + { | ||
99 | + name: 'widget-action.header-button', | ||
100 | + value: 'headerButton', | ||
101 | + multiple: true, | ||
102 | + } | ||
103 | +}; | ||
104 | + | ||
90 | export interface WidgetTypeDescriptor { | 105 | export interface WidgetTypeDescriptor { |
91 | type: widgetType; | 106 | type: widgetType; |
92 | resources: Array<WidgetResource>; | 107 | resources: Array<WidgetResource>; |
93 | templateHtml: string; | 108 | templateHtml: string; |
94 | templateCss: string; | 109 | templateCss: string; |
95 | controllerScript: string; | 110 | controllerScript: string; |
96 | - settingsSchema: string; | ||
97 | - dataKeySettingsSchema: string; | 111 | + settingsSchema?: string; |
112 | + dataKeySettingsSchema?: string; | ||
98 | defaultConfig: string; | 113 | defaultConfig: string; |
99 | sizeX: number; | 114 | sizeX: number; |
100 | sizeY: number; | 115 | sizeY: number; |
101 | } | 116 | } |
102 | 117 | ||
118 | +export interface WidgetTypeParameters { | ||
119 | + useCustomDatasources?: boolean; | ||
120 | + maxDatasources?: number; | ||
121 | + maxDataKeys?: number; | ||
122 | + dataKeysOptional?: boolean; | ||
123 | + stateData?: boolean; | ||
124 | +} | ||
125 | + | ||
126 | +export interface WidgetControllerDescriptor { | ||
127 | + widgetTypeFunction?: any; | ||
128 | + settingsSchema?: string; | ||
129 | + dataKeySettingsSchema?: string; | ||
130 | + typeParameters?: WidgetTypeParameters; | ||
131 | + actionSources?: {[key: string]: WidgetActionSource}; | ||
132 | +} | ||
133 | + | ||
103 | export interface WidgetType extends BaseData<WidgetTypeId> { | 134 | export interface WidgetType extends BaseData<WidgetTypeId> { |
104 | tenantId: TenantId; | 135 | tenantId: TenantId; |
105 | bundleAlias: string; | 136 | bundleAlias: string; |
@@ -108,9 +139,67 @@ export interface WidgetType extends BaseData<WidgetTypeId> { | @@ -108,9 +139,67 @@ export interface WidgetType extends BaseData<WidgetTypeId> { | ||
108 | descriptor: WidgetTypeDescriptor; | 139 | descriptor: WidgetTypeDescriptor; |
109 | } | 140 | } |
110 | 141 | ||
111 | -export interface WidgetInfo extends WidgetTypeDescriptor { | 142 | +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { |
112 | widgetName: string; | 143 | widgetName: string; |
113 | alias: string; | 144 | alias: string; |
145 | + typeSettingsSchema?: string; | ||
146 | + typeDataKeySettingsSchema?: string; | ||
147 | +} | ||
148 | + | ||
149 | +export const MissingWidgetType: WidgetInfo = { | ||
150 | + type: widgetType.latest, | ||
151 | + widgetName: 'Widget type not found', | ||
152 | + alias: 'undefined', | ||
153 | + sizeX: 8, | ||
154 | + sizeY: 6, | ||
155 | + resources: [], | ||
156 | + templateHtml: '<div class="tb-widget-error-container">' + | ||
157 | + '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' + | ||
158 | + '</div>', | ||
159 | + templateCss: '', | ||
160 | + controllerScript: 'self.onInit = function() {}', | ||
161 | + settingsSchema: '{}\n', | ||
162 | + dataKeySettingsSchema: '{}\n', | ||
163 | + defaultConfig: '{\n' + | ||
164 | + '"title": "Widget type not found",\n' + | ||
165 | + '"datasources": [],\n' + | ||
166 | + '"settings": {}\n' + | ||
167 | + '}\n' | ||
168 | +}; | ||
169 | + | ||
170 | +export const ErrorWidgetType: WidgetInfo = { | ||
171 | + type: widgetType.latest, | ||
172 | + widgetName: 'Error loading widget', | ||
173 | + alias: 'error', | ||
174 | + sizeX: 8, | ||
175 | + sizeY: 6, | ||
176 | + resources: [], | ||
177 | + templateHtml: '<div class="tb-widget-error-container">' + | ||
178 | + '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', | ||
179 | + templateCss: '', | ||
180 | + controllerScript: 'self.onInit = function() {}', | ||
181 | + settingsSchema: '{}\n', | ||
182 | + dataKeySettingsSchema: '{}\n', | ||
183 | + defaultConfig: '{\n' + | ||
184 | + '"title": "Widget failed to load",\n' + | ||
185 | + '"datasources": [],\n' + | ||
186 | + '"settings": {}\n' + | ||
187 | + '}\n' | ||
188 | +}; | ||
189 | + | ||
190 | +export interface WidgetTypeInstance { | ||
191 | + getSettingsSchema?: () => string; | ||
192 | + getDataKeySettingsSchema?: () => string; | ||
193 | + typeParameters?: () => WidgetTypeParameters; | ||
194 | + useCustomDatasources?: () => boolean; | ||
195 | + actionSources?: () => {[key: string]: WidgetActionSource}; | ||
196 | + | ||
197 | + onInit?: () => void; | ||
198 | + onDataUpdated?: () => void; | ||
199 | + onResize?: () => void; | ||
200 | + onEditModeChanged?: () => void; | ||
201 | + onMobileModeChanged?: () => void; | ||
202 | + onDestroy?: () => void; | ||
114 | } | 203 | } |
115 | 204 | ||
116 | export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | 205 | export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { |
@@ -130,6 +219,107 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | @@ -130,6 +219,107 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | ||
130 | }; | 219 | }; |
131 | } | 220 | } |
132 | 221 | ||
222 | +export enum LegendDirection { | ||
223 | + column = 'column', | ||
224 | + row = 'row' | ||
225 | +} | ||
226 | + | ||
227 | +export const legendDirectionTranslationMap = new Map<LegendDirection, string>( | ||
228 | + [ | ||
229 | + [ LegendDirection.column, 'direction.column' ], | ||
230 | + [ LegendDirection.row, 'direction.row' ] | ||
231 | + ] | ||
232 | +); | ||
233 | + | ||
234 | +export enum LegendPosition { | ||
235 | + top = 'top', | ||
236 | + bottom = 'bottom', | ||
237 | + left = 'left', | ||
238 | + right = 'right' | ||
239 | +} | ||
240 | + | ||
241 | +export const legendPositionTranslationMap = new Map<LegendPosition, string>( | ||
242 | + [ | ||
243 | + [ LegendPosition.top, 'position.top' ], | ||
244 | + [ LegendPosition.bottom, 'position.bottom' ], | ||
245 | + [ LegendPosition.left, 'position.left' ], | ||
246 | + [ LegendPosition.right, 'position.right' ] | ||
247 | + ] | ||
248 | +); | ||
249 | + | ||
250 | +export interface LegendConfig { | ||
251 | + position: LegendPosition; | ||
252 | + direction?: LegendDirection; | ||
253 | + showMin: boolean; | ||
254 | + showMax: boolean; | ||
255 | + showAvg: boolean; | ||
256 | + showTotal: boolean; | ||
257 | +} | ||
258 | + | ||
259 | +export interface DataKey { | ||
260 | + label: string; | ||
261 | + color: string; | ||
262 | + hidden?: boolean; | ||
263 | + [key: string]: any; | ||
264 | + // TODO: | ||
265 | +} | ||
266 | + | ||
267 | +export interface LegendKey { | ||
268 | + dataKey: DataKey; | ||
269 | + dataIndex: number; | ||
270 | +} | ||
271 | + | ||
272 | +export interface LegendKeyData { | ||
273 | + min: number; | ||
274 | + max: number; | ||
275 | + avg: number; | ||
276 | + total: number; | ||
277 | +} | ||
278 | + | ||
279 | +export interface LegendData { | ||
280 | + keys: Array<LegendKey>; | ||
281 | + data: Array<LegendKeyData>; | ||
282 | +} | ||
283 | + | ||
284 | +export interface WidgetConfigSettings { | ||
285 | + [key: string]: any; | ||
286 | + // TODO: | ||
287 | +} | ||
288 | + | ||
289 | +export enum WidgetActionType { | ||
290 | + openDashboardState = 'openDashboardState', | ||
291 | + updateDashboardState = 'updateDashboardState', | ||
292 | + openDashboard = 'openDashboard', | ||
293 | + custom = 'custom', | ||
294 | + customPretty = 'customPretty' | ||
295 | +} | ||
296 | + | ||
297 | +export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( | ||
298 | + [ | ||
299 | + [ WidgetActionType.openDashboardState, 'widget-action.open-dashboard-state' ], | ||
300 | + [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ], | ||
301 | + [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], | ||
302 | + [ WidgetActionType.custom, 'widget-action.custom' ], | ||
303 | + [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ] | ||
304 | + ] | ||
305 | +); | ||
306 | + | ||
307 | +export interface WidgetActionDescriptor { | ||
308 | + name: string; | ||
309 | + icon: string; | ||
310 | + displayName?: string; | ||
311 | + type: WidgetActionType; | ||
312 | + targetDashboardId?: string; | ||
313 | + targetDashboardStateId?: string; | ||
314 | + openRightLayout?: boolean; | ||
315 | + setEntityId?: boolean; | ||
316 | + stateEntityParamName?: string; | ||
317 | + customFunction?: string; | ||
318 | + customResources?: Array<WidgetResource>; | ||
319 | + customHtml?: string; | ||
320 | + customCss?: string; | ||
321 | +} | ||
322 | + | ||
133 | export interface WidgetConfig { | 323 | export interface WidgetConfig { |
134 | title?: string; | 324 | title?: string; |
135 | titleIcon?: string; | 325 | titleIcon?: string; |
@@ -141,6 +331,8 @@ export interface WidgetConfig { | @@ -141,6 +331,8 @@ export interface WidgetConfig { | ||
141 | enableFullscreen?: boolean; | 331 | enableFullscreen?: boolean; |
142 | useDashboardTimewindow?: boolean; | 332 | useDashboardTimewindow?: boolean; |
143 | displayTimewindow?: boolean; | 333 | displayTimewindow?: boolean; |
334 | + showLegend?: boolean; | ||
335 | + legendConfig?: LegendConfig; | ||
144 | timewindow?: Timewindow; | 336 | timewindow?: Timewindow; |
145 | mobileHeight?: number; | 337 | mobileHeight?: number; |
146 | mobileOrder?: number; | 338 | mobileOrder?: number; |
@@ -150,6 +342,10 @@ export interface WidgetConfig { | @@ -150,6 +342,10 @@ export interface WidgetConfig { | ||
150 | margin?: string; | 342 | margin?: string; |
151 | widgetStyle?: {[klass: string]: any}; | 343 | widgetStyle?: {[klass: string]: any}; |
152 | titleStyle?: {[klass: string]: any}; | 344 | titleStyle?: {[klass: string]: any}; |
345 | + units?: string; | ||
346 | + decimals?: number; | ||
347 | + actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | ||
348 | + settings?: WidgetConfigSettings; | ||
153 | [key: string]: any; | 349 | [key: string]: any; |
154 | 350 | ||
155 | // TODO: | 351 | // TODO: |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +export type WindowMessageType = 'widgetException'; | ||
18 | + | ||
19 | +export interface WindowMessage { | ||
20 | + type: WindowMessageType; | ||
21 | + data: any; | ||
22 | +} |