Commit e7d00f4b68e255192fcdf50f7304e85dbbb851ea

Authored by Igor Kulikov
1 parent f6e5b01c

Multiple input widget

... ... @@ -317,9 +317,9 @@
317 317 "sizeX": 7.5,
318 318 "sizeY": 3.5,
319 319 "resources": [],
320   - "templateHtml": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>",
321   - "templateCss": "",
322   - "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",
  320 + "templateHtml": "<tb-multiple-input-widget \n [ctx]=\"ctx\">\n</tb-multiple-input-widget>",
  321 + "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}",
  322 + "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n",
323 323 "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}",
324 324 "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",
325 325 "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\":{}}"
... ...
... ... @@ -75,31 +75,21 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
75 75 verticalPosition: NotificationVerticalPosition = 'bottom',
76 76 horizontalPosition: NotificationHorizontalPosition = 'left',
77 77 target?: string) {
78   - this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
  78 + this.ctx.showSuccessToast(message, duration, verticalPosition, horizontalPosition, target);
79 79 }
80 80
81 81 showErrorToast(message: string,
82 82 verticalPosition: NotificationVerticalPosition = 'bottom',
83 83 horizontalPosition: NotificationHorizontalPosition = 'left',
84 84 target?: string) {
85   - this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target);
  85 + this.ctx.showErrorToast(message, verticalPosition, horizontalPosition, target);
86 86 }
87 87
88 88 showToast(type: NotificationType, message: string, duration: number,
89 89 verticalPosition: NotificationVerticalPosition = 'bottom',
90 90 horizontalPosition: NotificationHorizontalPosition = 'left',
91 91 target?: string) {
92   - this.store.dispatch(new ActionNotificationShow(
93   - {
94   - message,
95   - type,
96   - duration,
97   - verticalPosition,
98   - horizontalPosition,
99   - target,
100   - panelClass: this.ctx.widgetNamespace,
101   - forceDismiss: true
102   - }));
  92 + this.ctx.showToast(type, message, duration, verticalPosition, horizontalPosition, target);
103 93 }
104 94
105 95 }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 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 +<form #formContainer class="tb-multiple-input"
  19 + #multipleInputForm="ngForm"
  20 + [formGroup]="multipleInputFormGroup"
  21 + tb-toast toastTarget="{{ toastTargetId }}"
  22 + (ngSubmit)="save()" novalidate autocomplete="off">
  23 + <div style="padding: 0 8px;" *ngIf="entityDetected && isAllParametersValid">
  24 + <fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}">
  25 + <legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }}
  26 + </legend>
  27 + <div fxLayout="row" class="layout-wrap"
  28 + [ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}">
  29 + <div *ngFor="let key of visibleKeys(source)"
  30 + [ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}">
  31 + <div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'">
  32 + <mat-form-field class="mat-block">
  33 + <mat-label>{{key.label}}</mat-label>
  34 + <input matInput
  35 + formControlName="{{key.formId}}"
  36 + [required]="key.settings.required"
  37 + [readonly]="key.settings.isEditable === 'readonly'"
  38 + type="text"
  39 + (focus)="key.isFocused = true; focusInputElement($event)"
  40 + (blur)="key.isFocused = false; inputChanged(source, key)">
  41 + <mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
  42 + <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
  43 + {{key.settings.requiredErrorMessage}}
  44 + </mat-error>
  45 + </mat-form-field>
  46 + </div>
  47 + <div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' ||
  48 + key.settings.dataKeyValueType === 'integer'">
  49 + <mat-form-field class="mat-block">
  50 + <mat-label>{{key.label}}</mat-label>
  51 + <input matInput
  52 + formControlName="{{key.formId}}"
  53 + [required]="key.settings.required"
  54 + [readonly]="key.settings.isEditable === 'readonly'"
  55 + type="number"
  56 + step="{{key.settings.step}}"
  57 + (focus)="key.isFocused = true; focusInputElement($event)"
  58 + (blur)="key.isFocused = false; inputChanged(source, key)">
  59 + <mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
  60 + <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
  61 + {{key.settings.requiredErrorMessage}}
  62 + </mat-error>
  63 + </mat-form-field>
  64 + </div>
  65 + <div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'">
  66 + <mat-checkbox formControlName="{{key.formId}}"
  67 + (change)="inputChanged(source, key)">
  68 + {{key.label}}
  69 + </mat-checkbox>
  70 + </div>
  71 + <div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'">
  72 + <mat-slide-toggle formControlName="{{key.formId}}"
  73 + (change)="inputChanged(source, key)">
  74 + {{key.label}}
  75 + </mat-slide-toggle>
  76 + </div>
  77 + <div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') ||
  78 + (key.settings.dataKeyValueType === 'date') ||
  79 + (key.settings.dataKeyValueType === 'time')" fxLayout="column">
  80 + <div fxLayout="row" [ngClass]="{'vertically-aligned': smallWidthContainer}" fxLayoutGap="16px">
  81 + <mat-form-field>
  82 + <mat-placeholder>{{key.label}}</mat-placeholder>
  83 + <mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
  84 + <mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
  85 + openOnFocus="true"></mat-datetimepicker>
  86 + <input matInput formControlName="{{key.formId}}"
  87 + [required]="key.settings.required"
  88 + [readonly]="key.settings.isEditable === 'readonly'"
  89 + [matDatetimepicker]="datePicker"
  90 + (focus)="key.isFocused = true;"
  91 + (blur)="key.isFocused = false;"
  92 + (dateChange)="inputChanged(source, key)">
  93 + <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
  94 + {{key.settings.requiredErrorMessage}}
  95 + </mat-error>
  96 + </mat-form-field>
  97 + </div>
  98 + </div>
  99 + </div>
  100 + </div>
  101 + </fieldset>
  102 + <div class="mat-padding" fxLayout="row" fxLayoutAlign="end center"
  103 + *ngIf="entityDetected && settings.showActionButtons">
  104 + <button mat-button color="primary" type="button"
  105 + (click)="discardAll()" style="max-height: 50px; margin-right:20px;"
  106 + [disabled]="!multipleInputForm.dirty">
  107 + {{ 'action.undo' | translate }}
  108 + </button>
  109 + <button mat-button mat-raised-button color="primary" type="submit"
  110 + style="max-height: 50px; margin-right:20px;"
  111 + [disabled]="!multipleInputForm.dirty || multipleInputForm.invalid">
  112 + {{ 'action.save' | translate }}
  113 + </button>
  114 + </div>
  115 + </div>
  116 + <div class="tb-multiple-input__errors" fxLayout="column" fxLayoutAlign="center center" style="height: 100%;"
  117 + *ngIf="!entityDetected || !isAllParametersValid">
  118 + <div style="text-align: center; font-size: 18px; color: #a0a0a0;" [fxHide]="entityDetected">
  119 + {{ 'widgets.input-widgets.no-entity-selected' | translate }}
  120 + </div>
  121 + <div style="text-align: center; font-size: 18px; color: #a0a0a0;"
  122 + [fxShow]="entityDetected && !isAllParametersValid">
  123 + {{ 'widgets.input-widgets.not-allowed-entity' | translate }}
  124 + </div>
  125 + </div>
  126 +</form>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 +
  19 + .tb-multiple-input {
  20 + height: 100%;
  21 + overflow-x: hidden;
  22 + overflow-y: auto;
  23 +
  24 + .fields-group {
  25 + padding: 0 8px;
  26 + margin: 10px 0;
  27 + border: 1px groove rgba(0, 0, 0, .25);
  28 +
  29 + legend {
  30 + color: rgba(0, 0, 0, .7);
  31 + }
  32 + }
  33 +
  34 + .input-field {
  35 + padding-right: 10px;
  36 +
  37 + mat-form-field {
  38 + margin-bottom: 5px;
  39 + }
  40 + }
  41 +
  42 + mat-checkbox,
  43 + mat-slide-toggle {
  44 + display: block;
  45 + margin-top: 20px;
  46 + margin-bottom: 16px;
  47 + white-space: normal;
  48 + }
  49 +
  50 + .date-time-input {
  51 + mat-form-field {
  52 + width: 100%;
  53 + margin: 2px 0;
  54 + }
  55 + }
  56 +
  57 + .vertical-alignment {
  58 + flex-direction: column;
  59 +
  60 + mat-checkbox,
  61 + mat-slide-toggle {
  62 + margin-top: 18px;
  63 + }
  64 +
  65 + mat-slide-toggle {
  66 + display: flex;
  67 + justify-content: space-between;
  68 + }
  69 + }
  70 +
  71 + .vertically-aligned {
  72 + flex-direction: column;
  73 + }
  74 + }
  75 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 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, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } 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 { Overlay } from '@angular/cdk/overlay';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +import { TranslateService } from '@ngx-translate/core';
  25 +import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
  26 +import { IWidgetSubscription } from '@core/api/widget-api.models';
  27 +import { isDefined, isEqual, isUndefined } from '@core/utils';
  28 +import { EntityType } from '@shared/models/entity-type.models';
  29 +import * as _moment from 'moment';
  30 +import { FormBuilder, FormGroup, NgForm, ValidatorFn, Validators } from '@angular/forms';
  31 +import { RequestConfig } from '@core/http/http-utils';
  32 +import { AttributeService } from '@core/http/attribute.service';
  33 +import { AttributeData, AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
  34 +import { forkJoin, Observable } from 'rxjs';
  35 +import { EntityId } from '@shared/models/id/entity-id';
  36 +
  37 +type FieldAlignment = 'row' | 'column';
  38 +
  39 +type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries';
  40 +type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' |
  41 + 'booleanCheckbox' | 'booleanSwitch' |
  42 + 'dateTime' | 'date' | 'time';
  43 +type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
  44 +
  45 +interface MultipleInputWidgetSettings {
  46 + widgetTitle: string;
  47 + showActionButtons: boolean;
  48 + updateAllValues: boolean;
  49 + showResultMessage: boolean;
  50 + showGroupTitle: boolean;
  51 + groupTitle: string;
  52 + fieldsAlignment: FieldAlignment;
  53 + fieldsInRow: number;
  54 + attributesShared?: boolean;
  55 +}
  56 +
  57 +interface MultipleInputWidgetDataKeySettings {
  58 + dataKeyType: MultipleInputWidgetDataKeyType;
  59 + dataKeyValueType: MultipleInputWidgetDataKeyValueType;
  60 + required: boolean;
  61 + isEditable: MultipleInputWidgetDataKeyEditableType;
  62 + disabledOnDataKey: string;
  63 + dataKeyHidden: boolean;
  64 + step: number;
  65 + requiredErrorMessage: string;
  66 + icon: string;
  67 + inputTypeNumber?: boolean;
  68 + readOnly?: boolean;
  69 + disabledOnCondition?: boolean;
  70 +}
  71 +
  72 +interface MultipleInputWidgetDataKey extends DataKey {
  73 + formId?: string;
  74 + settings: MultipleInputWidgetDataKeySettings;
  75 + isFocused: boolean;
  76 + value?: any;
  77 +}
  78 +
  79 +interface MultipleInputWidgetSource {
  80 + datasource: Datasource;
  81 + keys: MultipleInputWidgetDataKey[];
  82 +}
  83 +
  84 +@Component({
  85 + selector: 'tb-multiple-input-widget ',
  86 + templateUrl: './multiple-input-widget.component.html',
  87 + styleUrls: ['./multiple-input-widget.component.scss']
  88 +})
  89 +export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
  90 +
  91 + @ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>;
  92 + @ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm;
  93 +
  94 + @Input()
  95 + ctx: WidgetContext;
  96 +
  97 + private formResizeListener: any;
  98 + private settings: MultipleInputWidgetSettings;
  99 + private widgetConfig: WidgetConfig;
  100 + private subscription: IWidgetSubscription;
  101 + private datasources: Array<Datasource>;
  102 + private sources: Array<MultipleInputWidgetSource> = [];
  103 +
  104 + isVerticalAlignment: boolean;
  105 + inputWidthSettings: string;
  106 + changeAlignment: boolean;
  107 + smallWidthContainer: boolean;
  108 +
  109 + entityDetected = false;
  110 + isAllParametersValid = true;
  111 +
  112 + multipleInputFormGroup: FormGroup;
  113 +
  114 + toastTargetId = 'multiple-input-widget' + this.utils.guid();
  115 +
  116 + constructor(protected store: Store<AppState>,
  117 + private elementRef: ElementRef,
  118 + private ngZone: NgZone,
  119 + private overlay: Overlay,
  120 + private viewContainerRef: ViewContainerRef,
  121 + private utils: UtilsService,
  122 + private fb: FormBuilder,
  123 + private attributeService: AttributeService,
  124 + private translate: TranslateService) {
  125 + super(store);
  126 + }
  127 +
  128 + ngOnInit(): void {
  129 + this.ctx.$scope.multipleInputWidget = this;
  130 + this.settings = this.ctx.settings;
  131 + this.widgetConfig = this.ctx.widgetConfig;
  132 + this.subscription = this.ctx.defaultSubscription;
  133 + this.datasources = this.subscription.datasources;
  134 + this.initializeConfig();
  135 + this.updateDatasources();
  136 + this.buildForm();
  137 + this.ctx.updateWidgetParams();
  138 + this.formResizeListener = this.resize.bind(this);
  139 + // @ts-ignore
  140 + addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
  141 + }
  142 +
  143 + ngOnDestroy(): void {
  144 + if (this.formResizeListener) {
  145 + // @ts-ignore
  146 + removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
  147 + }
  148 + }
  149 +
  150 + private initializeConfig() {
  151 +
  152 + if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
  153 + this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
  154 + } else {
  155 + this.ctx.widgetTitle = this.ctx.widgetConfig.title;
  156 + }
  157 +
  158 + this.settings.groupTitle = this.settings.groupTitle || '${entityName}';
  159 +
  160 + // For backward compatibility
  161 + if (isUndefined(this.settings.showActionButtons)) {
  162 + this.settings.showActionButtons = true;
  163 + }
  164 + if (isUndefined(this.settings.fieldsAlignment)) {
  165 + this.settings.fieldsAlignment = 'row';
  166 + }
  167 + if (isUndefined(this.settings.fieldsInRow)) {
  168 + this.settings.fieldsInRow = 2;
  169 + }
  170 + // For backward compatibility
  171 +
  172 + this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row');
  173 +
  174 + if (!this.isVerticalAlignment && this.settings.fieldsInRow) {
  175 + this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%';
  176 + }
  177 +
  178 + this.updateWidgetDisplaying();
  179 + }
  180 +
  181 + private updateDatasources() {
  182 + if (this.datasources && this.datasources.length) {
  183 + this.entityDetected = true;
  184 + let keyIndex = 0;
  185 + this.datasources.forEach((datasource) => {
  186 + const source: MultipleInputWidgetSource = {
  187 + datasource,
  188 + keys: []
  189 + };
  190 + if (datasource.type === DatasourceType.entity) {
  191 + datasource.dataKeys.forEach((dataKey: MultipleInputWidgetDataKey) => {
  192 + if ((datasource.entityType !== EntityType.DEVICE) && (dataKey.settings.dataKeyType === 'shared')) {
  193 + this.isAllParametersValid = false;
  194 + }
  195 + if (dataKey.units) {
  196 + dataKey.label += ' (' + dataKey.units + ')';
  197 + }
  198 + dataKey.formId = (++keyIndex)+'';
  199 + dataKey.isFocused = false;
  200 +
  201 + // For backward compatibility
  202 + if (isUndefined(dataKey.settings.dataKeyType)) {
  203 + if (this.settings.attributesShared) {
  204 + dataKey.settings.dataKeyType = 'shared';
  205 + } else {
  206 + dataKey.settings.dataKeyType = 'server';
  207 + }
  208 + }
  209 +
  210 + if (isUndefined(dataKey.settings.dataKeyValueType)) {
  211 + if (dataKey.settings.inputTypeNumber) {
  212 + dataKey.settings.dataKeyValueType = 'double';
  213 + } else {
  214 + dataKey.settings.dataKeyValueType = 'string';
  215 + }
  216 + }
  217 +
  218 + if (isUndefined(dataKey.settings.isEditable)) {
  219 + if (dataKey.settings.readOnly) {
  220 + dataKey.settings.isEditable = 'readonly';
  221 + } else {
  222 + dataKey.settings.isEditable = 'editable';
  223 + }
  224 + }
  225 + // For backward compatibility
  226 +
  227 + source.keys.push(dataKey);
  228 + });
  229 + } else {
  230 + this.entityDetected = false;
  231 + }
  232 + this.sources.push(source);
  233 + });
  234 + }
  235 + }
  236 +
  237 + private buildForm() {
  238 + this.multipleInputFormGroup = this.fb.group({});
  239 + this.sources.forEach((source) => {
  240 + for (const key of this.visibleKeys(source)) {
  241 + const validators: ValidatorFn[] = [];
  242 + if (key.settings.required) {
  243 + validators.push(Validators.required);
  244 + }
  245 + if (key.settings.dataKeyValueType === 'integer') {
  246 + validators.push(Validators.pattern(/^-?[0-9]+$/));
  247 + }
  248 + const formControl = this.fb.control(
  249 + { value: key.value,
  250 + disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition},
  251 + validators
  252 + );
  253 + this.multipleInputFormGroup.addControl(key.formId, formControl);
  254 + }
  255 + });
  256 + }
  257 +
  258 + private updateWidgetData(data: Array<DatasourceData>) {
  259 + let dataIndex = 0;
  260 + this.sources.forEach((source) => {
  261 + source.keys.forEach((key) => {
  262 + const keyData = data[dataIndex].data;
  263 + if (keyData && keyData.length) {
  264 + let value;
  265 + switch (key.settings.dataKeyValueType) {
  266 + case 'dateTime':
  267 + case 'date':
  268 + value = _moment(keyData[0][1]).toDate();
  269 + break;
  270 + case 'time':
  271 + value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate();
  272 + break;
  273 + case 'booleanCheckbox':
  274 + case 'booleanSwitch':
  275 + value = (keyData[0][1] === 'true');
  276 + break;
  277 + default:
  278 + value = keyData[0][1];
  279 + }
  280 + key.value = value;
  281 + }
  282 +
  283 + if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) {
  284 + const conditions = data.filter((item) => {
  285 + return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey;
  286 + });
  287 + if (conditions && conditions.length) {
  288 + if (conditions[0].data.length) {
  289 + if (conditions[0].data[0][1] === 'false') {
  290 + key.settings.disabledOnCondition = true;
  291 + } else {
  292 + key.settings.disabledOnCondition = !conditions[0].data[0][1];
  293 + }
  294 + }
  295 + }
  296 + }
  297 +
  298 + if (!key.settings.dataKeyHidden) {
  299 + if (key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition) {
  300 + this.multipleInputFormGroup.get(key.formId).disable({emitEvent: false});
  301 + } else {
  302 + this.multipleInputFormGroup.get(key.formId).enable({emitEvent: false});
  303 + }
  304 + const dirty = this.multipleInputFormGroup.get(key.formId).dirty;
  305 + if (!key.isFocused && !dirty) {
  306 + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false});
  307 + }
  308 + }
  309 + dataIndex++;
  310 + });
  311 + });
  312 + }
  313 +
  314 + private updateWidgetDisplaying() {
  315 + this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
  316 + this.smallWidthContainer = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 420);
  317 + }
  318 +
  319 + public onDataUpdated() {
  320 + this.ngZone.run(() => {
  321 + this.updateWidgetData(this.subscription.data);
  322 + this.ctx.detectChanges();
  323 + });
  324 + }
  325 +
  326 + private resize() {
  327 + this.ngZone.run(() => {
  328 + this.updateWidgetDisplaying();
  329 + this.ctx.detectChanges();
  330 + });
  331 + }
  332 +
  333 + public getGroupTitle(datasource: Datasource): string {
  334 + return this.utils.createLabelFromDatasource(datasource, this.settings.groupTitle);
  335 + }
  336 +
  337 + public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] {
  338 + return source.keys.filter(key => !key.settings.dataKeyHidden);
  339 + }
  340 +
  341 + public datePickerType(keyType: MultipleInputWidgetDataKeyValueType): string {
  342 + switch (keyType) {
  343 + case 'dateTime':
  344 + return 'datetime';
  345 + case 'date':
  346 + return 'date';
  347 + case 'time':
  348 + return 'time';
  349 + }
  350 + }
  351 +
  352 + public focusInputElement($event: Event) {
  353 + ($event.target as HTMLInputElement).select();
  354 + }
  355 +
  356 + public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) {
  357 + if (!this.settings.showActionButtons) {
  358 + const currentValue = this.multipleInputFormGroup.get(key.formId).value;
  359 + if (!key.settings.required || (key.settings.required && isDefined(currentValue))) {
  360 + const dataToSave: MultipleInputWidgetSource = {
  361 + datasource: source.datasource,
  362 + keys: [key]
  363 + };
  364 + this.save(dataToSave);
  365 + }
  366 + }
  367 + }
  368 +
  369 + public save(dataToSave?: MultipleInputWidgetSource) {
  370 + const config: RequestConfig = {
  371 + ignoreLoading: !this.settings.showActionButtons
  372 + };
  373 + let data: Array<MultipleInputWidgetSource>;
  374 + if (dataToSave) {
  375 + data = [dataToSave];
  376 + } else {
  377 + data = this.sources;
  378 + }
  379 + const tasks: Observable<any>[] = [];
  380 + data.forEach((toSave) => {
  381 + const serverAttributes: AttributeData[] = [];
  382 + const sharedAttributes: AttributeData[] = [];
  383 + const telemetry: AttributeData[] = [];
  384 + for (const key of this.visibleKeys(toSave)) {
  385 + const currentValue = this.multipleInputFormGroup.get(key.formId).value;
  386 + if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) {
  387 + const attribute: AttributeData = {
  388 + key: key.name,
  389 + value: null
  390 + };
  391 + if (currentValue) {
  392 + switch (key.settings.dataKeyValueType) {
  393 + case 'dateTime':
  394 + case 'date':
  395 + attribute.value = currentValue.getTime();
  396 + break;
  397 + case 'time':
  398 + attribute.value = currentValue.getTime() - _moment().startOf('day').valueOf();
  399 + break;
  400 + default:
  401 + attribute.value = currentValue;
  402 + }
  403 + } else {
  404 + if (currentValue === '') {
  405 + attribute.value = null;
  406 + } else {
  407 + attribute.value = currentValue;
  408 + }
  409 + }
  410 +
  411 + switch (key.settings.dataKeyType) {
  412 + case 'shared':
  413 + sharedAttributes.push(attribute);
  414 + break;
  415 + case 'timeseries':
  416 + telemetry.push(attribute);
  417 + break;
  418 + default:
  419 + serverAttributes.push(attribute);
  420 + }
  421 + }
  422 + }
  423 + const entityId: EntityId = {
  424 + entityType: toSave.datasource.entityType,
  425 + id: toSave.datasource.entityId
  426 + };
  427 + if (serverAttributes.length) {
  428 + tasks.push(this.attributeService.saveEntityAttributes(
  429 + entityId,
  430 + AttributeScope.SERVER_SCOPE,
  431 + serverAttributes,
  432 + config
  433 + ));
  434 + }
  435 + if (sharedAttributes.length) {
  436 + tasks.push(this.attributeService.saveEntityAttributes(
  437 + entityId,
  438 + AttributeScope.SHARED_SCOPE,
  439 + sharedAttributes,
  440 + config
  441 + ));
  442 + }
  443 + if (telemetry.length) {
  444 + tasks.push(this.attributeService.saveEntityTimeseries(
  445 + entityId,
  446 + LatestTelemetry.LATEST_TELEMETRY,
  447 + telemetry,
  448 + config
  449 + ));
  450 + }
  451 + });
  452 + if (tasks.length) {
  453 + forkJoin(tasks).subscribe(
  454 + () => {
  455 + this.multipleInputForm.resetForm();
  456 + this.multipleInputFormGroup.markAsPristine();
  457 + if (this.settings.showResultMessage) {
  458 + this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
  459 + 1000, 'bottom', 'left', this.toastTargetId);
  460 + }
  461 + },
  462 + () => {
  463 + if (this.settings.showResultMessage) {
  464 + this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
  465 + 'bottom', 'left', this.toastTargetId);
  466 + }
  467 + });
  468 + } else {
  469 + this.multipleInputForm.resetForm();
  470 + this.multipleInputFormGroup.markAsPristine();
  471 + }
  472 + }
  473 +
  474 + public discardAll() {
  475 + this.multipleInputForm.resetForm();
  476 + this.sources.forEach((source) => {
  477 + for (const key of this.visibleKeys(source)) {
  478 + this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false});
  479 + }
  480 + });
  481 + this.multipleInputFormGroup.markAsPristine();
  482 + }
  483 +}
... ...
... ... @@ -30,6 +30,7 @@ import {
30 30 DateRangeNavigatorPanelComponent,
31 31 DateRangeNavigatorWidgetComponent
32 32 } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
  33 +import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
33 34
34 35 @NgModule({
35 36 declarations:
... ... @@ -41,7 +42,8 @@ import {
41 42 TimeseriesTableWidgetComponent,
42 43 EntitiesHierarchyWidgetComponent,
43 44 DateRangeNavigatorWidgetComponent,
44   - DateRangeNavigatorPanelComponent
  45 + DateRangeNavigatorPanelComponent,
  46 + MultipleInputWidgetComponent
45 47 ],
46 48 imports: [
47 49 CommonModule,
... ... @@ -55,7 +57,8 @@ import {
55 57 TimeseriesTableWidgetComponent,
56 58 EntitiesHierarchyWidgetComponent,
57 59 RpcWidgetsModule,
58   - DateRangeNavigatorWidgetComponent
  60 + DateRangeNavigatorWidgetComponent,
  61 + MultipleInputWidgetComponent
59 62 ],
60 63 providers: [
61 64 CustomDialogService
... ...
... ... @@ -260,6 +260,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
260 260 this.widgetContext = this.dashboardWidget.widgetContext;
261 261 this.widgetContext.changeDetector = this.cd;
262 262 this.widgetContext.ngZone = this.ngZone;
  263 + this.widgetContext.store = this.store;
263 264 this.widgetContext.servicesMap = ServicesMap;
264 265 this.widgetContext.isEdit = this.isEdit;
265 266 this.widgetContext.isMobile = this.isMobile;
... ...
... ... @@ -52,6 +52,14 @@ import { CustomDialogService } from '@home/components/widget/dialog/custom-dialo
52 52 import { isDefined, formatValue } from '@core/utils';
53 53 import { forkJoin, Observable, of, ReplaySubject } from 'rxjs';
54 54 import { WidgetSubscription } from '@core/api/widget-subscription';
  55 +import { Store } from '@ngrx/store';
  56 +import { AppState } from '@core/core.state';
  57 +import {
  58 + NotificationHorizontalPosition,
  59 + NotificationType,
  60 + NotificationVerticalPosition
  61 +} from '@core/notification/notification.models';
  62 +import { ActionNotificationShow } from '@core/notification/notification.actions';
55 63
56 64 export interface IWidgetAction {
57 65 name: string;
... ... @@ -152,8 +160,8 @@ export class WidgetContext {
152 160 formatValue
153 161 };
154 162
155   - $container: JQuery<any>;
156   - $containerParent: JQuery<any>;
  163 + $container: JQuery<HTMLElement>;
  164 + $containerParent: JQuery<HTMLElement>;
157 165 width: number;
158 166 height: number;
159 167 $scope: IDynamicWidgetComponent;
... ... @@ -184,11 +192,44 @@ export class WidgetContext {
184 192
185 193 ngZone?: NgZone;
186 194
  195 + store?: Store<AppState>;
  196 +
187 197 rxjs = {
188 198 forkJoin,
189 199 of
190 200 };
191 201
  202 + showSuccessToast(message: string, duration: number = 1000,
  203 + verticalPosition: NotificationVerticalPosition = 'bottom',
  204 + horizontalPosition: NotificationHorizontalPosition = 'left',
  205 + target?: string) {
  206 + this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
  207 + }
  208 +
  209 + showErrorToast(message: string,
  210 + verticalPosition: NotificationVerticalPosition = 'bottom',
  211 + horizontalPosition: NotificationHorizontalPosition = 'left',
  212 + target?: string) {
  213 + this.showToast('error', message, undefined, verticalPosition, horizontalPosition, target);
  214 + }
  215 +
  216 + showToast(type: NotificationType, message: string, duration: number,
  217 + verticalPosition: NotificationVerticalPosition = 'bottom',
  218 + horizontalPosition: NotificationHorizontalPosition = 'left',
  219 + target?: string) {
  220 + this.store.dispatch(new ActionNotificationShow(
  221 + {
  222 + message,
  223 + type,
  224 + duration,
  225 + verticalPosition,
  226 + horizontalPosition,
  227 + target,
  228 + panelClass: this.widgetNamespace,
  229 + forceDismiss: true
  230 + }));
  231 + }
  232 +
192 233 detectChanges(updateWidgetParams: boolean = false) {
193 234 if (!this.destroyed) {
194 235 if (updateWidgetParams) {
... ...
... ... @@ -27,7 +27,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-
27 27 import {
28 28 Aggregation,
29 29 aggregationTranslations,
30   - AggregationType,
  30 + AggregationType, DAY,
31 31 HistoryWindow,
32 32 HistoryWindowType,
33 33 IntervalWindow,
... ... @@ -205,9 +205,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
205 205 const timewindowFormValue = this.timewindowForm.getRawValue();
206 206 if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
207 207 return timewindowFormValue.history.timewindowMs;
208   - } else {
  208 + } else if (timewindowFormValue.history.fixedTimewindow) {
209 209 return timewindowFormValue.history.fixedTimewindow.endTimeMs -
210 210 timewindowFormValue.history.fixedTimewindow.startTimeMs;
  211 + } else {
  212 + return DAY;
211 213 }
212 214 }
213 215
... ...