Showing
9 changed files
with
743 additions
and
22 deletions
... | ... | @@ -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 | ... | ... |