From e7d00f4b68e255192fcdf50f7304e85dbbb851ea Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 28 Feb 2020 19:49:14 +0200 Subject: [PATCH] Multiple input widget --- .../system/widget_bundles/input_widgets.json | 6 +- .../widget/dynamic-widget.component.ts | 16 +- .../lib/multiple-input-widget.component.html | 126 +++++ .../lib/multiple-input-widget.component.scss | 75 +++ .../lib/multiple-input-widget.component.ts | 483 ++++++++++++++++++ .../widget/widget-components.module.ts | 7 +- .../components/widget/widget.component.ts | 1 + .../home/models/widget-component.models.ts | 45 +- .../time/timewindow-panel.component.ts | 6 +- 9 files changed, 743 insertions(+), 22 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts 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 301f1ae3eb..3285ba7903 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 @@ -317,9 +317,9 @@ "sizeX": 7.5, "sizeY": 3.5, "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n\r\nself.onResize = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-resize', self.ctx.$scope.formId);\r\n}\r\n", + "templateHtml": "\n", + "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", + "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index 6911695fe0..855f4314b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -75,31 +75,21 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', target?: string) { - this.showToast('success', message, duration, verticalPosition, horizontalPosition, target); + this.ctx.showSuccessToast(message, duration, verticalPosition, horizontalPosition, target); } showErrorToast(message: string, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', target?: string) { - this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target); + this.ctx.showErrorToast(message, verticalPosition, horizontalPosition, target); } showToast(type: NotificationType, message: string, duration: number, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', target?: string) { - this.store.dispatch(new ActionNotificationShow( - { - message, - type, - duration, - verticalPosition, - horizontalPosition, - target, - panelClass: this.ctx.widgetNamespace, - forceDismiss: true - })); + this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html new file mode 100644 index 0000000000..10bc9881a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -0,0 +1,126 @@ + +
+
+
+ {{ getGroupTitle(source.datasource) }} + +
+
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{key.settings.requiredErrorMessage}} + + +
+
+ + {{key.label}} + + {{key.settings.icon}} + + {{key.settings.requiredErrorMessage}} + + +
+
+ + {{key.label}} + +
+
+ + {{key.label}} + +
+
+
+ + {{key.label}} + + + + + {{key.settings.requiredErrorMessage}} + + +
+
+
+
+
+
+ + +
+
+
+
+ {{ 'widgets.input-widgets.no-entity-selected' | translate }} +
+
+ {{ 'widgets.input-widgets.not-allowed-entity' | translate }} +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss new file mode 100644 index 0000000000..de1a672535 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + + .tb-multiple-input { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + + .fields-group { + padding: 0 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + + legend { + color: rgba(0, 0, 0, .7); + } + } + + .input-field { + padding-right: 10px; + + mat-form-field { + margin-bottom: 5px; + } + } + + mat-checkbox, + mat-slide-toggle { + display: block; + margin-top: 20px; + margin-bottom: 16px; + white-space: normal; + } + + .date-time-input { + mat-form-field { + width: 100%; + margin: 2px 0; + } + } + + .vertical-alignment { + flex-direction: column; + + mat-checkbox, + mat-slide-toggle { + margin-top: 18px; + } + + mat-slide-toggle { + display: flex; + justify-content: space-between; + } + } + + .vertically-aligned { + flex-direction: column; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts new file mode 100644 index 0000000000..ca7a2c5526 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -0,0 +1,483 @@ +/// +/// Copyright © 2016-2020 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, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } 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 { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { isDefined, isEqual, isUndefined } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import * as _moment from 'moment'; +import { FormBuilder, FormGroup, NgForm, ValidatorFn, Validators } from '@angular/forms'; +import { RequestConfig } from '@core/http/http-utils'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeData, AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; +import { forkJoin, Observable } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; + +type FieldAlignment = 'row' | 'column'; + +type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; +type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | + 'booleanCheckbox' | 'booleanSwitch' | + 'dateTime' | 'date' | 'time'; +type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; + +interface MultipleInputWidgetSettings { + widgetTitle: string; + showActionButtons: boolean; + updateAllValues: boolean; + showResultMessage: boolean; + showGroupTitle: boolean; + groupTitle: string; + fieldsAlignment: FieldAlignment; + fieldsInRow: number; + attributesShared?: boolean; +} + +interface MultipleInputWidgetDataKeySettings { + dataKeyType: MultipleInputWidgetDataKeyType; + dataKeyValueType: MultipleInputWidgetDataKeyValueType; + required: boolean; + isEditable: MultipleInputWidgetDataKeyEditableType; + disabledOnDataKey: string; + dataKeyHidden: boolean; + step: number; + requiredErrorMessage: string; + icon: string; + inputTypeNumber?: boolean; + readOnly?: boolean; + disabledOnCondition?: boolean; +} + +interface MultipleInputWidgetDataKey extends DataKey { + formId?: string; + settings: MultipleInputWidgetDataKeySettings; + isFocused: boolean; + value?: any; +} + +interface MultipleInputWidgetSource { + datasource: Datasource; + keys: MultipleInputWidgetDataKey[]; +} + +@Component({ + selector: 'tb-multiple-input-widget ', + templateUrl: './multiple-input-widget.component.html', + styleUrls: ['./multiple-input-widget.component.scss'] +}) +export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { + + @ViewChild('formContainer', {static: true}) formContainerRef: ElementRef; + @ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm; + + @Input() + ctx: WidgetContext; + + private formResizeListener: any; + private settings: MultipleInputWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private datasources: Array; + private sources: Array = []; + + isVerticalAlignment: boolean; + inputWidthSettings: string; + changeAlignment: boolean; + smallWidthContainer: boolean; + + entityDetected = false; + isAllParametersValid = true; + + multipleInputFormGroup: FormGroup; + + toastTargetId = 'multiple-input-widget' + this.utils.guid(); + + constructor(protected store: Store, + private elementRef: ElementRef, + private ngZone: NgZone, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private fb: FormBuilder, + private attributeService: AttributeService, + private translate: TranslateService) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.multipleInputWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.datasources = this.subscription.datasources; + this.initializeConfig(); + this.updateDatasources(); + this.buildForm(); + this.ctx.updateWidgetParams(); + this.formResizeListener = this.resize.bind(this); + // @ts-ignore + addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); + } + + ngOnDestroy(): void { + if (this.formResizeListener) { + // @ts-ignore + removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); + } + } + + private initializeConfig() { + + if (this.settings.widgetTitle && this.settings.widgetTitle.length) { + this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); + } else { + this.ctx.widgetTitle = this.ctx.widgetConfig.title; + } + + this.settings.groupTitle = this.settings.groupTitle || '${entityName}'; + + // For backward compatibility + if (isUndefined(this.settings.showActionButtons)) { + this.settings.showActionButtons = true; + } + if (isUndefined(this.settings.fieldsAlignment)) { + this.settings.fieldsAlignment = 'row'; + } + if (isUndefined(this.settings.fieldsInRow)) { + this.settings.fieldsInRow = 2; + } + // For backward compatibility + + this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row'); + + if (!this.isVerticalAlignment && this.settings.fieldsInRow) { + this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%'; + } + + this.updateWidgetDisplaying(); + } + + private updateDatasources() { + if (this.datasources && this.datasources.length) { + this.entityDetected = true; + let keyIndex = 0; + this.datasources.forEach((datasource) => { + const source: MultipleInputWidgetSource = { + datasource, + keys: [] + }; + if (datasource.type === DatasourceType.entity) { + datasource.dataKeys.forEach((dataKey: MultipleInputWidgetDataKey) => { + if ((datasource.entityType !== EntityType.DEVICE) && (dataKey.settings.dataKeyType === 'shared')) { + this.isAllParametersValid = false; + } + if (dataKey.units) { + dataKey.label += ' (' + dataKey.units + ')'; + } + dataKey.formId = (++keyIndex)+''; + dataKey.isFocused = false; + + // For backward compatibility + if (isUndefined(dataKey.settings.dataKeyType)) { + if (this.settings.attributesShared) { + dataKey.settings.dataKeyType = 'shared'; + } else { + dataKey.settings.dataKeyType = 'server'; + } + } + + if (isUndefined(dataKey.settings.dataKeyValueType)) { + if (dataKey.settings.inputTypeNumber) { + dataKey.settings.dataKeyValueType = 'double'; + } else { + dataKey.settings.dataKeyValueType = 'string'; + } + } + + if (isUndefined(dataKey.settings.isEditable)) { + if (dataKey.settings.readOnly) { + dataKey.settings.isEditable = 'readonly'; + } else { + dataKey.settings.isEditable = 'editable'; + } + } + // For backward compatibility + + source.keys.push(dataKey); + }); + } else { + this.entityDetected = false; + } + this.sources.push(source); + }); + } + } + + private buildForm() { + this.multipleInputFormGroup = this.fb.group({}); + this.sources.forEach((source) => { + for (const key of this.visibleKeys(source)) { + const validators: ValidatorFn[] = []; + if (key.settings.required) { + validators.push(Validators.required); + } + if (key.settings.dataKeyValueType === 'integer') { + validators.push(Validators.pattern(/^-?[0-9]+$/)); + } + const formControl = this.fb.control( + { value: key.value, + disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition}, + validators + ); + this.multipleInputFormGroup.addControl(key.formId, formControl); + } + }); + } + + private updateWidgetData(data: Array) { + let dataIndex = 0; + this.sources.forEach((source) => { + source.keys.forEach((key) => { + const keyData = data[dataIndex].data; + if (keyData && keyData.length) { + let value; + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + value = _moment(keyData[0][1]).toDate(); + break; + case 'time': + value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate(); + break; + case 'booleanCheckbox': + case 'booleanSwitch': + value = (keyData[0][1] === 'true'); + break; + default: + value = keyData[0][1]; + } + key.value = value; + } + + if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { + const conditions = data.filter((item) => { + return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey; + }); + if (conditions && conditions.length) { + if (conditions[0].data.length) { + if (conditions[0].data[0][1] === 'false') { + key.settings.disabledOnCondition = true; + } else { + key.settings.disabledOnCondition = !conditions[0].data[0][1]; + } + } + } + } + + if (!key.settings.dataKeyHidden) { + if (key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition) { + this.multipleInputFormGroup.get(key.formId).disable({emitEvent: false}); + } else { + this.multipleInputFormGroup.get(key.formId).enable({emitEvent: false}); + } + const dirty = this.multipleInputFormGroup.get(key.formId).dirty; + if (!key.isFocused && !dirty) { + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); + } + } + dataIndex++; + }); + }); + } + + private updateWidgetDisplaying() { + this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); + this.smallWidthContainer = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 420); + } + + public onDataUpdated() { + this.ngZone.run(() => { + this.updateWidgetData(this.subscription.data); + this.ctx.detectChanges(); + }); + } + + private resize() { + this.ngZone.run(() => { + this.updateWidgetDisplaying(); + this.ctx.detectChanges(); + }); + } + + public getGroupTitle(datasource: Datasource): string { + return this.utils.createLabelFromDatasource(datasource, this.settings.groupTitle); + } + + public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] { + return source.keys.filter(key => !key.settings.dataKeyHidden); + } + + public datePickerType(keyType: MultipleInputWidgetDataKeyValueType): string { + switch (keyType) { + case 'dateTime': + return 'datetime'; + case 'date': + return 'date'; + case 'time': + return 'time'; + } + } + + public focusInputElement($event: Event) { + ($event.target as HTMLInputElement).select(); + } + + public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) { + if (!this.settings.showActionButtons) { + const currentValue = this.multipleInputFormGroup.get(key.formId).value; + if (!key.settings.required || (key.settings.required && isDefined(currentValue))) { + const dataToSave: MultipleInputWidgetSource = { + datasource: source.datasource, + keys: [key] + }; + this.save(dataToSave); + } + } + } + + public save(dataToSave?: MultipleInputWidgetSource) { + const config: RequestConfig = { + ignoreLoading: !this.settings.showActionButtons + }; + let data: Array; + if (dataToSave) { + data = [dataToSave]; + } else { + data = this.sources; + } + const tasks: Observable[] = []; + data.forEach((toSave) => { + const serverAttributes: AttributeData[] = []; + const sharedAttributes: AttributeData[] = []; + const telemetry: AttributeData[] = []; + for (const key of this.visibleKeys(toSave)) { + const currentValue = this.multipleInputFormGroup.get(key.formId).value; + if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) { + const attribute: AttributeData = { + key: key.name, + value: null + }; + if (currentValue) { + switch (key.settings.dataKeyValueType) { + case 'dateTime': + case 'date': + attribute.value = currentValue.getTime(); + break; + case 'time': + attribute.value = currentValue.getTime() - _moment().startOf('day').valueOf(); + break; + default: + attribute.value = currentValue; + } + } else { + if (currentValue === '') { + attribute.value = null; + } else { + attribute.value = currentValue; + } + } + + switch (key.settings.dataKeyType) { + case 'shared': + sharedAttributes.push(attribute); + break; + case 'timeseries': + telemetry.push(attribute); + break; + default: + serverAttributes.push(attribute); + } + } + } + const entityId: EntityId = { + entityType: toSave.datasource.entityType, + id: toSave.datasource.entityId + }; + if (serverAttributes.length) { + tasks.push(this.attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + serverAttributes, + config + )); + } + if (sharedAttributes.length) { + tasks.push(this.attributeService.saveEntityAttributes( + entityId, + AttributeScope.SHARED_SCOPE, + sharedAttributes, + config + )); + } + if (telemetry.length) { + tasks.push(this.attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + telemetry, + config + )); + } + }); + if (tasks.length) { + forkJoin(tasks).subscribe( + () => { + this.multipleInputForm.resetForm(); + this.multipleInputFormGroup.markAsPristine(); + 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); + } + }); + } else { + this.multipleInputForm.resetForm(); + this.multipleInputFormGroup.markAsPristine(); + } + } + + public discardAll() { + this.multipleInputForm.resetForm(); + this.sources.forEach((source) => { + for (const key of this.visibleKeys(source)) { + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); + } + }); + this.multipleInputFormGroup.markAsPristine(); + } +} 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 e3cc12b853..9754019025 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 @@ -30,6 +30,7 @@ import { DateRangeNavigatorPanelComponent, DateRangeNavigatorWidgetComponent } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; +import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component'; @NgModule({ declarations: @@ -41,7 +42,8 @@ import { TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, DateRangeNavigatorWidgetComponent, - DateRangeNavigatorPanelComponent + DateRangeNavigatorPanelComponent, + MultipleInputWidgetComponent ], imports: [ CommonModule, @@ -55,7 +57,8 @@ import { TimeseriesTableWidgetComponent, EntitiesHierarchyWidgetComponent, RpcWidgetsModule, - DateRangeNavigatorWidgetComponent + DateRangeNavigatorWidgetComponent, + MultipleInputWidgetComponent ], providers: [ CustomDialogService diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 18497e342b..5dd493a4f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -260,6 +260,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext = this.dashboardWidget.widgetContext; this.widgetContext.changeDetector = this.cd; this.widgetContext.ngZone = this.ngZone; + this.widgetContext.store = this.store; this.widgetContext.servicesMap = ServicesMap; this.widgetContext.isEdit = this.isEdit; this.widgetContext.isMobile = this.isMobile; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index e9bec966a4..185074a8ad 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -52,6 +52,14 @@ import { CustomDialogService } from '@home/components/widget/dialog/custom-dialo import { isDefined, formatValue } from '@core/utils'; import { forkJoin, Observable, of, ReplaySubject } from 'rxjs'; import { WidgetSubscription } from '@core/api/widget-subscription'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + NotificationHorizontalPosition, + NotificationType, + NotificationVerticalPosition +} from '@core/notification/notification.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; export interface IWidgetAction { name: string; @@ -152,8 +160,8 @@ export class WidgetContext { formatValue }; - $container: JQuery; - $containerParent: JQuery; + $container: JQuery; + $containerParent: JQuery; width: number; height: number; $scope: IDynamicWidgetComponent; @@ -184,11 +192,44 @@ export class WidgetContext { ngZone?: NgZone; + store?: Store; + rxjs = { forkJoin, of }; + showSuccessToast(message: string, duration: number = 1000, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('success', message, duration, verticalPosition, horizontalPosition, target); + } + + showErrorToast(message: string, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target); + } + + showToast(type: NotificationType, message: string, duration: number, + verticalPosition: NotificationVerticalPosition = 'bottom', + horizontalPosition: NotificationHorizontalPosition = 'left', + target?: string) { + this.store.dispatch(new ActionNotificationShow( + { + message, + type, + duration, + verticalPosition, + horizontalPosition, + target, + panelClass: this.widgetNamespace, + forceDismiss: true + })); + } + detectChanges(updateWidgetParams: boolean = false) { if (!this.destroyed) { if (updateWidgetParams) { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index ac0b22815e..dea27b8f4f 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -27,7 +27,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time- import { Aggregation, aggregationTranslations, - AggregationType, + AggregationType, DAY, HistoryWindow, HistoryWindowType, IntervalWindow, @@ -205,9 +205,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { const timewindowFormValue = this.timewindowForm.getRawValue(); if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { return timewindowFormValue.history.timewindowMs; - } else { + } else if (timewindowFormValue.history.fixedTimewindow) { return timewindowFormValue.history.fixedTimewindow.endTimeMs - timewindowFormValue.history.fixedTimewindow.startTimeMs; + } else { + return DAY; } }