Browse Source

Multiple input widget

pull/2470/head
Igor Kulikov 6 years ago
parent
commit
e7d00f4b68
  1. 6
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 16
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  3. 126
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  4. 75
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
  5. 483
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  6. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  7. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  8. 45
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  9. 6
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

6
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -317,9 +317,9 @@
"sizeX": 7.5,
"sizeY": 3.5,
"resources": [],
"templateHtml": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>",
"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": "<tb-multiple-input-widget \n [ctx]=\"ctx\">\n</tb-multiple-input-widget>",
"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\":{}}"

16
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);
}
}

126
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -0,0 +1,126 @@
<!--
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.
-->
<form #formContainer class="tb-multiple-input"
#multipleInputForm="ngForm"
[formGroup]="multipleInputFormGroup"
tb-toast toastTarget="{{ toastTargetId }}"
(ngSubmit)="save()" novalidate autocomplete="off">
<div style="padding: 0 8px;" *ngIf="entityDetected && isAllParametersValid">
<fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}">
<legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }}
</legend>
<div fxLayout="row" class="layout-wrap"
[ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}">
<div *ngFor="let key of visibleKeys(source)"
[ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}">
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'">
<mat-form-field class="mat-block">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="text"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' ||
key.settings.dataKeyValueType === 'integer'">
<mat-form-field class="mat-block">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="number"
step="{{key.settings.step}}"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'">
<mat-checkbox formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
{{key.label}}
</mat-checkbox>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'">
<mat-slide-toggle formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
{{key.label}}
</mat-slide-toggle>
</div>
<div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') ||
(key.settings.dataKeyValueType === 'date') ||
(key.settings.dataKeyValueType === 'time')" fxLayout="column">
<div fxLayout="row" [ngClass]="{'vertically-aligned': smallWidthContainer}" fxLayoutGap="16px">
<mat-form-field>
<mat-placeholder>{{key.label}}</mat-placeholder>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
openOnFocus="true"></mat-datetimepicker>
<input matInput formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
[matDatetimepicker]="datePicker"
(focus)="key.isFocused = true;"
(blur)="key.isFocused = false;"
(dateChange)="inputChanged(source, key)">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{key.settings.requiredErrorMessage}}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div>
</fieldset>
<div class="mat-padding" fxLayout="row" fxLayoutAlign="end center"
*ngIf="entityDetected && settings.showActionButtons">
<button mat-button color="primary" type="button"
(click)="discardAll()" style="max-height: 50px; margin-right:20px;"
[disabled]="!multipleInputForm.dirty">
{{ 'action.undo' | translate }}
</button>
<button mat-button mat-raised-button color="primary" type="submit"
style="max-height: 50px; margin-right:20px;"
[disabled]="!multipleInputForm.dirty || multipleInputForm.invalid">
{{ 'action.save' | translate }}
</button>
</div>
</div>
<div class="tb-multiple-input__errors" fxLayout="column" fxLayoutAlign="center center" style="height: 100%;"
*ngIf="!entityDetected || !isAllParametersValid">
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" [fxHide]="entityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div style="text-align: center; font-size: 18px; color: #a0a0a0;"
[fxShow]="entityDetected && !isAllParametersValid">
{{ 'widgets.input-widgets.not-allowed-entity' | translate }}
</div>
</div>
</form>

75
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;
}
}
}

483
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<HTMLElement>;
@ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm;
@Input()
ctx: WidgetContext;
private formResizeListener: any;
private settings: MultipleInputWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasources: Array<Datasource>;
private sources: Array<MultipleInputWidgetSource> = [];
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<AppState>,
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<DatasourceData>) {
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<MultipleInputWidgetSource>;
if (dataToSave) {
data = [dataToSave];
} else {
data = this.sources;
}
const tasks: Observable<any>[] = [];
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();
}
}

7
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

1
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;

45
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<any>;
$containerParent: JQuery<any>;
$container: JQuery<HTMLElement>;
$containerParent: JQuery<HTMLElement>;
width: number;
height: number;
$scope: IDynamicWidgetComponent;
@ -184,11 +192,44 @@ export class WidgetContext {
ngZone?: NgZone;
store?: Store<AppState>;
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) {

6
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;
}
}

Loading…
Cancel
Save