diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json
index b19c5d40b9..8a80aa80f1 100644
--- a/application/src/main/data/json/system/widget_bundles/input_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json
@@ -455,6 +455,24 @@
"dataKeySettingsSchema": "{}\n",
"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\":{}}"
}
+ },
+ {
+ "alias": "update_json_attribute",
+ "name": "Update JSON attribute",
+ "image": "data:image/png;base64,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",
+ "description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "\n",
+ "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}",
+ "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}",
+ "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}",
+ "dataKeySettingsSchema": "{}",
+ "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}"
+ }
}
]
}
diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts
index b9c2697d0a..225a1398b0 100644
--- a/ui-ngx/src/app/core/utils.ts
+++ b/ui-ngx/src/app/core/utils.ts
@@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean {
return true;
}
+export function isLiteralObject(value: any) {
+ return (!!value) && (value.constructor === Object);
+}
+
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
new file mode 100644
index 0000000000..81c06fe0f7
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
@@ -0,0 +1,69 @@
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
new file mode 100644
index 0000000000..f097de5afb
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.tb-json-input {
+ width: 100%;
+ height: 100%;
+ padding: 5px;
+
+ &__form {
+ overflow: auto;
+ height: 100%;
+ }
+
+ &__error {
+ text-align: center;
+ font-size: 18px;
+ color: #a0a0a0;
+ }
+}
+
+.tb-toast {
+ font-size: 14px!important;
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
new file mode 100644
index 0000000000..77c0f907ce
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
@@ -0,0 +1,227 @@
+///
+/// Copyright © 2016-2021 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, Input, OnInit } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { WidgetContext } from '@home/models/widget-component.models';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { UtilsService } from '@core/services/utils.service';
+import { TranslateService } from '@ngx-translate/core';
+import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
+import { IWidgetSubscription } from '@core/api/widget-api.models';
+import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
+import { AttributeService } from '@core/http/attribute.service';
+import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
+import { EntityId } from '@shared/models/id/entity-id';
+import { EntityType } from '@shared/models/entity-type.models';
+import { createLabelFromDatasource } from '@core/utils';
+import { Observable } from 'rxjs';
+
+enum JsonInputWidgetMode {
+ ATTRIBUTE = 'ATTRIBUTE',
+ TIME_SERIES = 'TIME_SERIES',
+}
+
+interface JsonInputWidgetSettings {
+ widgetTitle: string;
+ widgetMode: JsonInputWidgetMode;
+ attributeScope?: AttributeScope;
+ showLabel: boolean;
+ labelValue?: string;
+ attributeRequired: boolean;
+ showResultMessage: boolean;
+}
+
+@Component({
+ selector: 'tb-json-input-widget ',
+ templateUrl: './json-input-widget.component.html',
+ styleUrls: ['./json-input-widget.component.scss']
+})
+export class JsonInputWidgetComponent extends PageComponent implements OnInit {
+
+ @Input()
+ ctx: WidgetContext;
+
+ public settings: JsonInputWidgetSettings;
+ private widgetConfig: WidgetConfig;
+ private subscription: IWidgetSubscription;
+ private datasource: Datasource;
+
+ labelValue: string;
+
+ entityDetected = false;
+ dataKeyDetected = false;
+ isValidParameter = false;
+ errorMessage: string;
+
+ isFocused: boolean;
+ originalValue: any;
+ attributeUpdateFormGroup: FormGroup;
+
+ toastTargetId = 'json-input-widget' + this.utils.guid();
+
+ constructor(protected store: Store,
+ private utils: UtilsService,
+ private fb: FormBuilder,
+ private attributeService: AttributeService,
+ private translate: TranslateService) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.jsonInputWidget = this;
+ this.settings = this.ctx.settings;
+ this.widgetConfig = this.ctx.widgetConfig;
+ this.subscription = this.ctx.defaultSubscription;
+ this.datasource = this.subscription.datasources[0];
+ this.initializeConfig();
+ this.validateDatasources();
+ this.buildForm();
+ this.ctx.updateWidgetParams();
+ }
+
+ private initializeConfig() {
+ if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
+ const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle);
+ this.ctx.widgetTitle = this.utils.customTranslation(title, title);
+ } else {
+ this.ctx.widgetTitle = this.ctx.widgetConfig.title;
+ }
+
+ if (this.settings.labelValue && this.settings.labelValue.length) {
+ const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
+ this.labelValue = this.utils.customTranslation(label, label);
+ } else {
+ this.labelValue = this.translate.instant('widgets.input-widgets.value');
+ }
+ }
+
+ private validateDatasources() {
+ if (this.datasource?.type === DatasourceType.entity) {
+ this.entityDetected = true;
+ if (this.datasource.dataKeys.length) {
+ this.dataKeyDetected = true;
+
+ if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
+ if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
+ if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) {
+ this.isValidParameter = true;
+ } else {
+ this.errorMessage = 'widgets.input-widgets.not-allowed-entity';
+ }
+ } else {
+ this.errorMessage = 'widgets.input-widgets.no-attribute-selected';
+ }
+ } else {
+ if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
+ this.isValidParameter = true;
+ } else {
+ this.errorMessage = 'widgets.input-widgets.no-timeseries-selected';
+ }
+ }
+
+ }
+ }
+ }
+
+ private buildForm() {
+ const validators: ValidatorFn[] = [];
+ if (this.settings.attributeRequired) {
+ validators.push(Validators.required);
+ }
+ this.attributeUpdateFormGroup = this.fb.group({
+ currentValue: [{}, validators]
+ });
+ this.attributeUpdateFormGroup.valueChanges.subscribe( () => {
+ this.ctx.detectChanges();
+ });
+ }
+
+ private updateWidgetData(data: Array) {
+ if (this.isValidParameter) {
+ let value = {};
+ if (data[0].data[0][1] !== '') {
+ try {
+ value = JSON.parse(data[0].data[0][1]);
+ } catch (e) {
+ value = data[0].data[0][1];
+ }
+ }
+ this.originalValue = value;
+ if (!this.isFocused) {
+ this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue);
+ this.ctx.detectChanges();
+ }
+ }
+ }
+
+ public onDataUpdated() {
+ this.updateWidgetData(this.subscription.data);
+ }
+
+ public save() {
+ this.isFocused = false;
+
+ const attributeToSave: AttributeData = {
+ key: this.datasource.dataKeys[0].name,
+ value: this.attributeUpdateFormGroup.get('currentValue').value
+ };
+
+ const entityId: EntityId = {
+ entityType: this.datasource.entityType,
+ id: this.datasource.entityId
+ };
+
+ let saveAttributeObservable: Observable;
+ if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
+ saveAttributeObservable = this.attributeService.saveEntityAttributes(
+ entityId,
+ this.settings.attributeScope,
+ [ attributeToSave ],
+ {}
+ );
+ } else {
+ saveAttributeObservable = this.attributeService.saveEntityTimeseries(
+ entityId,
+ LatestTelemetry.LATEST_TELEMETRY,
+ [ attributeToSave ],
+ {}
+ );
+ }
+ saveAttributeObservable.subscribe(
+ () => {
+ this.attributeUpdateFormGroup.markAsPristine();
+ this.ctx.detectChanges();
+ if (this.settings.showResultMessage) {
+ this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
+ 1000, 'bottom', 'left', this.toastTargetId);
+ }
+ },
+ () => {
+ if (this.settings.showResultMessage) {
+ this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
+ 'bottom', 'left', this.toastTargetId);
+ }
+ });
+ }
+
+ public discard() {
+ this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false});
+ this.attributeUpdateFormGroup.markAsPristine();
+ this.isFocused = false;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
index 2ad0fb1612..801ec12dc7 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
@@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
+import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
@NgModule({
declarations:
@@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent,
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent,
+ JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
@@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent,
RpcWidgetsModule,
DateRangeNavigatorWidgetComponent,
+ JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts
index a5d45d16c7..d82b66e7b6 100644
--- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts
+++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts
@@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
-import { guid, isUndefined } from '@core/utils';
+import { guid, isUndefined, isDefinedAndNotNull, isLiteralObject } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models';
@@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.contentValue = '';
this.objectValid = false;
try {
-
- if (this.modelValue) {
+ if (isDefinedAndNotNull(this.modelValue)) {
this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
(key, objectValue) => {
return this.sort(key, objectValue);
@@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
if (this.contentValue && this.contentValue.length > 0) {
try {
data = JSON.parse(this.contentValue);
+ if (!isLiteralObject(data)) {
+ throw new TypeError(`Value is not a valid JSON`);
+ }
this.objectValid = true;
this.validationError = '';
} catch (ex) {