Showing
7 changed files
with
362 additions
and
2 deletions
@@ -455,6 +455,24 @@ | @@ -455,6 +455,24 @@ | ||
455 | "dataKeySettingsSchema": "{}\n", | 455 | "dataKeySettingsSchema": "{}\n", |
456 | "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" | 456 | "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
457 | } | 457 | } |
458 | + }, | ||
459 | + { | ||
460 | + "alias": "update_json_attribute", | ||
461 | + "name": "Update JSON attribute", | ||
462 | + "image": "data:image/png;base64,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", | ||
463 | + "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.", | ||
464 | + "descriptor": { | ||
465 | + "type": "latest", | ||
466 | + "sizeX": 7.5, | ||
467 | + "sizeY": 3, | ||
468 | + "resources": [], | ||
469 | + "templateHtml": "<tb-json-input-widget \n [ctx]=\"ctx\">\n</tb-json-input-widget>", | ||
470 | + "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}", | ||
471 | + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}", | ||
472 | + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}", | ||
473 | + "dataKeySettingsSchema": "{}", | ||
474 | + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" | ||
475 | + } | ||
458 | } | 476 | } |
459 | ] | 477 | ] |
460 | } | 478 | } |
@@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { | @@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean { | ||
127 | return true; | 127 | return true; |
128 | } | 128 | } |
129 | 129 | ||
130 | +export function isLiteralObject(value: any) { | ||
131 | + return (!!value) && (value.constructor === Object); | ||
132 | +} | ||
133 | + | ||
130 | export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { | 134 | export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { |
131 | if (isDefinedAndNotNull(value) && isNumeric(value) && | 135 | if (isDefinedAndNotNull(value) && isNumeric(value) && |
132 | (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { | 136 | (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2021 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 | + | ||
19 | +<div class="tb-json-input" tb-toast toastTarget="{{ toastTargetId }}"> | ||
20 | + <form *ngIf="attributeUpdateFormGroup" | ||
21 | + fxLayout="column" | ||
22 | + class="tb-json-input__form" | ||
23 | + [formGroup]="attributeUpdateFormGroup" | ||
24 | + (ngSubmit)="save()"> | ||
25 | + <div fxLayout="column" fxLayoutGap="10px" fxFlex *ngIf="entityDetected && isValidParameter && dataKeyDetected"> | ||
26 | + <fieldset fxFlex> | ||
27 | + <tb-json-object-edit | ||
28 | + [editorStyle]="{minHeight: '100px'}" | ||
29 | + fillHeight="true" | ||
30 | + [required]="settings.attributeRequired" | ||
31 | + label="{{ settings.showLabel ? labelValue : '' }}" | ||
32 | + formControlName="currentValue" | ||
33 | + (focusin)="isFocused = true;" | ||
34 | + (focusout)="isFocused = false;" | ||
35 | + ></tb-json-object-edit> | ||
36 | + </fieldset> | ||
37 | + <div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px"> | ||
38 | + <button mat-button color="primary" | ||
39 | + type="button" | ||
40 | + [disabled]="!attributeUpdateFormGroup.dirty" | ||
41 | + (click)="discard()" | ||
42 | + matTooltip="{{ 'widgets.input-widgets.discard-changes' | translate }}" | ||
43 | + matTooltipPosition="above"> | ||
44 | + {{ "action.undo" | translate }} | ||
45 | + </button> | ||
46 | + <button mat-button mat-raised-button color="primary" | ||
47 | + type="submit" | ||
48 | + [disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty"> | ||
49 | + {{ "action.save" | translate }} | ||
50 | + </button> | ||
51 | + </div> | ||
52 | + </div> | ||
53 | + | ||
54 | + <div fxLayout="column" fxLayoutAlign="center center" fxFlex *ngIf="!entityDetected || !dataKeyDetected || !isValidParameter"> | ||
55 | + <div class="tb-json-input__error" | ||
56 | + *ngIf="!entityDetected"> | ||
57 | + {{ 'widgets.input-widgets.no-entity-selected' | translate }} | ||
58 | + </div> | ||
59 | + <div class="tb-json-input__error" | ||
60 | + *ngIf="entityDetected && !dataKeyDetected"> | ||
61 | + {{ 'widgets.input-widgets.no-datakey-selected' | translate }} | ||
62 | + </div> | ||
63 | + <div class="tb-json-input__error" | ||
64 | + *ngIf="dataKeyDetected && !isValidParameter"> | ||
65 | + {{ errorMessage | translate }} | ||
66 | + </div> | ||
67 | + </div> | ||
68 | + </form> | ||
69 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2021 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-json-input { | ||
18 | + width: 100%; | ||
19 | + height: 100%; | ||
20 | + padding: 5px; | ||
21 | + | ||
22 | + &__form { | ||
23 | + overflow: auto; | ||
24 | + height: 100%; | ||
25 | + } | ||
26 | + | ||
27 | + &__error { | ||
28 | + text-align: center; | ||
29 | + font-size: 18px; | ||
30 | + color: #a0a0a0; | ||
31 | + } | ||
32 | +} | ||
33 | + | ||
34 | +.tb-toast { | ||
35 | + font-size: 14px!important; | ||
36 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 { PageComponent } from '@shared/components/page.component'; | ||
19 | +import { WidgetContext } from '@home/models/widget-component.models'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { UtilsService } from '@core/services/utils.service'; | ||
23 | +import { TranslateService } from '@ngx-translate/core'; | ||
24 | +import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; | ||
25 | +import { IWidgetSubscription } from '@core/api/widget-api.models'; | ||
26 | +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; | ||
27 | +import { AttributeService } from '@core/http/attribute.service'; | ||
28 | +import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; | ||
29 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
30 | +import { EntityType } from '@shared/models/entity-type.models'; | ||
31 | +import { createLabelFromDatasource } from '@core/utils'; | ||
32 | +import { Observable } from 'rxjs'; | ||
33 | + | ||
34 | +enum JsonInputWidgetMode { | ||
35 | + ATTRIBUTE = 'ATTRIBUTE', | ||
36 | + TIME_SERIES = 'TIME_SERIES', | ||
37 | +} | ||
38 | + | ||
39 | +interface JsonInputWidgetSettings { | ||
40 | + widgetTitle: string; | ||
41 | + widgetMode: JsonInputWidgetMode; | ||
42 | + attributeScope?: AttributeScope; | ||
43 | + showLabel: boolean; | ||
44 | + labelValue?: string; | ||
45 | + attributeRequired: boolean; | ||
46 | + showResultMessage: boolean; | ||
47 | +} | ||
48 | + | ||
49 | +@Component({ | ||
50 | + selector: 'tb-json-input-widget ', | ||
51 | + templateUrl: './json-input-widget.component.html', | ||
52 | + styleUrls: ['./json-input-widget.component.scss'] | ||
53 | +}) | ||
54 | +export class JsonInputWidgetComponent extends PageComponent implements OnInit { | ||
55 | + | ||
56 | + @Input() | ||
57 | + ctx: WidgetContext; | ||
58 | + | ||
59 | + public settings: JsonInputWidgetSettings; | ||
60 | + private widgetConfig: WidgetConfig; | ||
61 | + private subscription: IWidgetSubscription; | ||
62 | + private datasource: Datasource; | ||
63 | + | ||
64 | + labelValue: string; | ||
65 | + | ||
66 | + entityDetected = false; | ||
67 | + dataKeyDetected = false; | ||
68 | + isValidParameter = false; | ||
69 | + errorMessage: string; | ||
70 | + | ||
71 | + isFocused: boolean; | ||
72 | + originalValue: any; | ||
73 | + attributeUpdateFormGroup: FormGroup; | ||
74 | + | ||
75 | + toastTargetId = 'json-input-widget' + this.utils.guid(); | ||
76 | + | ||
77 | + constructor(protected store: Store<AppState>, | ||
78 | + private utils: UtilsService, | ||
79 | + private fb: FormBuilder, | ||
80 | + private attributeService: AttributeService, | ||
81 | + private translate: TranslateService) { | ||
82 | + super(store); | ||
83 | + } | ||
84 | + | ||
85 | + ngOnInit(): void { | ||
86 | + this.ctx.$scope.jsonInputWidget = this; | ||
87 | + this.settings = this.ctx.settings; | ||
88 | + this.widgetConfig = this.ctx.widgetConfig; | ||
89 | + this.subscription = this.ctx.defaultSubscription; | ||
90 | + this.datasource = this.subscription.datasources[0]; | ||
91 | + this.initializeConfig(); | ||
92 | + this.validateDatasources(); | ||
93 | + this.buildForm(); | ||
94 | + this.ctx.updateWidgetParams(); | ||
95 | + } | ||
96 | + | ||
97 | + private initializeConfig() { | ||
98 | + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { | ||
99 | + const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); | ||
100 | + this.ctx.widgetTitle = this.utils.customTranslation(title, title); | ||
101 | + } else { | ||
102 | + this.ctx.widgetTitle = this.ctx.widgetConfig.title; | ||
103 | + } | ||
104 | + | ||
105 | + if (this.settings.labelValue && this.settings.labelValue.length) { | ||
106 | + const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); | ||
107 | + this.labelValue = this.utils.customTranslation(label, label); | ||
108 | + } else { | ||
109 | + this.labelValue = this.translate.instant('widgets.input-widgets.value'); | ||
110 | + } | ||
111 | + } | ||
112 | + | ||
113 | + private validateDatasources() { | ||
114 | + if (this.datasource?.type === DatasourceType.entity) { | ||
115 | + this.entityDetected = true; | ||
116 | + if (this.datasource.dataKeys.length) { | ||
117 | + this.dataKeyDetected = true; | ||
118 | + | ||
119 | + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { | ||
120 | + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { | ||
121 | + if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) { | ||
122 | + this.isValidParameter = true; | ||
123 | + } else { | ||
124 | + this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; | ||
125 | + } | ||
126 | + } else { | ||
127 | + this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; | ||
128 | + } | ||
129 | + } else { | ||
130 | + if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { | ||
131 | + this.isValidParameter = true; | ||
132 | + } else { | ||
133 | + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + } | ||
138 | + } | ||
139 | + } | ||
140 | + | ||
141 | + private buildForm() { | ||
142 | + const validators: ValidatorFn[] = []; | ||
143 | + if (this.settings.attributeRequired) { | ||
144 | + validators.push(Validators.required); | ||
145 | + } | ||
146 | + this.attributeUpdateFormGroup = this.fb.group({ | ||
147 | + currentValue: [{}, validators] | ||
148 | + }); | ||
149 | + this.attributeUpdateFormGroup.valueChanges.subscribe( () => { | ||
150 | + this.ctx.detectChanges(); | ||
151 | + }); | ||
152 | + } | ||
153 | + | ||
154 | + private updateWidgetData(data: Array<DatasourceData>) { | ||
155 | + if (this.isValidParameter) { | ||
156 | + let value = {}; | ||
157 | + if (data[0].data[0][1] !== '') { | ||
158 | + try { | ||
159 | + value = JSON.parse(data[0].data[0][1]); | ||
160 | + } catch (e) { | ||
161 | + value = data[0].data[0][1]; | ||
162 | + } | ||
163 | + } | ||
164 | + this.originalValue = value; | ||
165 | + if (!this.isFocused) { | ||
166 | + this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue); | ||
167 | + this.ctx.detectChanges(); | ||
168 | + } | ||
169 | + } | ||
170 | + } | ||
171 | + | ||
172 | + public onDataUpdated() { | ||
173 | + this.updateWidgetData(this.subscription.data); | ||
174 | + } | ||
175 | + | ||
176 | + public save() { | ||
177 | + this.isFocused = false; | ||
178 | + | ||
179 | + const attributeToSave: AttributeData = { | ||
180 | + key: this.datasource.dataKeys[0].name, | ||
181 | + value: this.attributeUpdateFormGroup.get('currentValue').value | ||
182 | + }; | ||
183 | + | ||
184 | + const entityId: EntityId = { | ||
185 | + entityType: this.datasource.entityType, | ||
186 | + id: this.datasource.entityId | ||
187 | + }; | ||
188 | + | ||
189 | + let saveAttributeObservable: Observable<any>; | ||
190 | + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { | ||
191 | + saveAttributeObservable = this.attributeService.saveEntityAttributes( | ||
192 | + entityId, | ||
193 | + this.settings.attributeScope, | ||
194 | + [ attributeToSave ], | ||
195 | + {} | ||
196 | + ); | ||
197 | + } else { | ||
198 | + saveAttributeObservable = this.attributeService.saveEntityTimeseries( | ||
199 | + entityId, | ||
200 | + LatestTelemetry.LATEST_TELEMETRY, | ||
201 | + [ attributeToSave ], | ||
202 | + {} | ||
203 | + ); | ||
204 | + } | ||
205 | + saveAttributeObservable.subscribe( | ||
206 | + () => { | ||
207 | + this.attributeUpdateFormGroup.markAsPristine(); | ||
208 | + this.ctx.detectChanges(); | ||
209 | + if (this.settings.showResultMessage) { | ||
210 | + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), | ||
211 | + 1000, 'bottom', 'left', this.toastTargetId); | ||
212 | + } | ||
213 | + }, | ||
214 | + () => { | ||
215 | + if (this.settings.showResultMessage) { | ||
216 | + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), | ||
217 | + 'bottom', 'left', this.toastTargetId); | ||
218 | + } | ||
219 | + }); | ||
220 | + } | ||
221 | + | ||
222 | + public discard() { | ||
223 | + this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false}); | ||
224 | + this.attributeUpdateFormGroup.markAsPristine(); | ||
225 | + this.isFocused = false; | ||
226 | + } | ||
227 | +} |
@@ -37,6 +37,7 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; | @@ -37,6 +37,7 @@ import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; | ||
37 | import { ImportExportService } from '@home/components/import-export/import-export.service'; | 37 | import { ImportExportService } from '@home/components/import-export/import-export.service'; |
38 | import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; | 38 | import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; |
39 | import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; | 39 | import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; |
40 | +import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; | ||
40 | 41 | ||
41 | @NgModule({ | 42 | @NgModule({ |
42 | declarations: | 43 | declarations: |
@@ -49,6 +50,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig | @@ -49,6 +50,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig | ||
49 | EntitiesHierarchyWidgetComponent, | 50 | EntitiesHierarchyWidgetComponent, |
50 | DateRangeNavigatorWidgetComponent, | 51 | DateRangeNavigatorWidgetComponent, |
51 | DateRangeNavigatorPanelComponent, | 52 | DateRangeNavigatorPanelComponent, |
53 | + JsonInputWidgetComponent, | ||
52 | MultipleInputWidgetComponent, | 54 | MultipleInputWidgetComponent, |
53 | TripAnimationComponent, | 55 | TripAnimationComponent, |
54 | PhotoCameraInputWidgetComponent, | 56 | PhotoCameraInputWidgetComponent, |
@@ -69,6 +71,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig | @@ -69,6 +71,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig | ||
69 | EntitiesHierarchyWidgetComponent, | 71 | EntitiesHierarchyWidgetComponent, |
70 | RpcWidgetsModule, | 72 | RpcWidgetsModule, |
71 | DateRangeNavigatorWidgetComponent, | 73 | DateRangeNavigatorWidgetComponent, |
74 | + JsonInputWidgetComponent, | ||
72 | MultipleInputWidgetComponent, | 75 | MultipleInputWidgetComponent, |
73 | TripAnimationComponent, | 76 | TripAnimationComponent, |
74 | PhotoCameraInputWidgetComponent, | 77 | PhotoCameraInputWidgetComponent, |
@@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati | @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati | ||
22 | import { Store } from '@ngrx/store'; | 22 | import { Store } from '@ngrx/store'; |
23 | import { AppState } from '@core/core.state'; | 23 | import { AppState } from '@core/core.state'; |
24 | import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | 24 | import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; |
25 | -import { guid } from '@core/utils'; | 25 | +import { guid, isDefinedAndNotNull, isLiteralObject } from '@core/utils'; |
26 | import { ResizeObserver } from '@juggle/resize-observer'; | 26 | import { ResizeObserver } from '@juggle/resize-observer'; |
27 | import { getAce } from '@shared/models/ace/ace.models'; | 27 | import { getAce } from '@shared/models/ace/ace.models'; |
28 | 28 | ||
@@ -224,7 +224,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | @@ -224,7 +224,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | ||
224 | this.contentValue = ''; | 224 | this.contentValue = ''; |
225 | this.objectValid = false; | 225 | this.objectValid = false; |
226 | try { | 226 | try { |
227 | - if (this.modelValue) { | 227 | + if (isDefinedAndNotNull(this.modelValue)) { |
228 | this.contentValue = JSON.stringify(this.modelValue, undefined, 2); | 228 | this.contentValue = JSON.stringify(this.modelValue, undefined, 2); |
229 | this.objectValid = true; | 229 | this.objectValid = true; |
230 | } else { | 230 | } else { |
@@ -250,6 +250,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | @@ -250,6 +250,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va | ||
250 | if (this.contentValue && this.contentValue.length > 0) { | 250 | if (this.contentValue && this.contentValue.length > 0) { |
251 | try { | 251 | try { |
252 | data = JSON.parse(this.contentValue); | 252 | data = JSON.parse(this.contentValue); |
253 | + if (!isLiteralObject(data)) { | ||
254 | + throw new TypeError(`Value is not a valid JSON`); | ||
255 | + } | ||
253 | this.objectValid = true; | 256 | this.objectValid = true; |
254 | this.validationError = ''; | 257 | this.validationError = ''; |
255 | } catch (ex) { | 258 | } catch (ex) { |