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 @@
+
+
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;
}
}