diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 4930219741..20b77bf1ab 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -3,7 +3,9 @@ "alias": "cards", "title": "Cards", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAT5UlEQVR42u3d95MUVffHcf86/UEtqwxVapmzaH0VE2YR82PAgBkTKCYUAyrGR0mSxERwBXRV0EdFRcAc5vvaPnptZ5bdZWbDwJ5TW1s93T23b5/7vufe7t3PuXv8+OOP36YNZrzUqCzdNUR37fHdd9810gaz4qV01xDdlWAlWAlWgpVgJVgJVnoqwUqwEqwEK8FKsNISrAQrwdqlwfrtt9/+V7Off/55gIJ++OGH8m46bOvWrb/88sv4Aeubb76pu8vb5z/++MPvEaqPwocL8RUrVqhwfc8rr7zSYdsNBFZvb+/hhx/+n79t/fr1ree8+eabn376qY03KrPx2muvff311zaeffbZd955Z/yANWvWLF76v8psPPLII19++eVdd901QvX57LPP7rvvvqGfv3nz5ldffbXfQzfffPPy5cvre9zC999/P4JgnXfeefU9n3/+uX6pEkHMxo0bL7vsMh5UCU786quvfGXSpElz584VwByNkvUtX3nrrbdsRDmrVq1auHDhtm3bdr+h8LHHHpszZ05s//TTT9EbN2zYwG+LFy/mH+OACMGBf/75Z5zW09Pj0Pbt2wsxuuimTZvi46+//qr3vvvuu3H+hx9++MUXX/g6D3/88cdxzkcffbRo0aItW7bER+FHCa5VamW0ef75588555z4iootW7Zs5cqV0SLA0hyuUmJHAUutHOo3pnQElqpsq8xt2DNjxowzzjjjySefPP/8819++eUPPvjAxh133AGpJ5544plnnnHDp512mtNUa/r06QsWLOCOKVOmPPTQQ7rXDTfcoJCHH374+uuvd/KECROaRs/dDCwOvPjii21Mnjz58ssvF8JPPvnkK664wsYll1zCAw5xpvD21FNPcazRR5fj8xdeeIEbuff333/nYWXedttt/On8s88++8ILL5w3b56jV155ZQxbPKxMNJh+QEdRgVGJQ1pw5syZTnj77beRfdZZZ6nk7bffrtgAK/b4CowKWO5UUUoWPlxlOME69NBDL67MtQOsl156yYYa33rrrTbUbMmSJTYCLBvuVhe0EWDpYVwT0w5u1VduvPFG96AP6cfjByxBJToVkmysXr0aTwLGUUcdJbRzztSpU5cuXWq0uvba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", - "description": "Tables and cards to display latest and historical values for multiple entities simultaneously." + "description": "Tables and cards to display latest and historical values for multiple entities simultaneously.", + "externalId": null, + "name": "Cards" }, "widgetTypes": [ { @@ -100,7 +102,8 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-simple-card-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "hasBasicMode": true, + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" } }, { diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index af0e64ddb6..be08d50d69 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -27,7 +27,13 @@ import { AlarmFilter, AlarmFilterConfig, createDefaultEntityDataPageLink, - Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, + Filter, + FilterInfo, + filterInfoToKeyFilters, + Filters, + KeyFilter, + singleEntityDataPageLink, + singleEntityFilterFromDeviceId, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; import { TranslateService } from '@ngx-translate/core'; @@ -246,6 +252,9 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); + if (newDatasource.type === DatasourceType.device) { + newDatasource.type = DatasourceType.entity; + } if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount || newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.filterId) { @@ -254,7 +263,22 @@ export class AliasController implements IAliasController { if (newDatasource.type === DatasourceType.alarmCount) { newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); } - if (newDatasource.entityAliasId) { + if (newDatasource.deviceId) { + newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId); + if (forceFilter) { + return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter, + {ignoreLoading: true, ignoreErrors: true}).pipe( + map((entity) => { + if (entity) { + updateDatasourceFromEntityInfo(newDatasource, entity, true); + } + return newDatasource; + }) + ); + } else { + return of(newDatasource); + } + } else if (newDatasource.entityAliasId) { return this.getAliasInfo(newDatasource.entityAliasId).pipe( mergeMap((aliasInfo) => { newDatasource.aliasName = aliasInfo.alias; diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 3881e3e914..501fbd7189 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -242,10 +242,8 @@ export class DashboardUtilsService { widgetConfig.datasources = []; } widgetConfig.datasources.forEach((datasource) => { - if (datasource.type === 'device') { - datasource.type = DatasourceType.entity; - } if (datasource.deviceAliasId) { + datasource.type = DatasourceType.entity; datasource.entityAliasId = datasource.deviceAliasId; delete datasource.deviceAliasId; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 547cfcc1f5..fb5336de66 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -19,7 +19,21 @@

widget.add

: {{data.widgetInfo.widgetName}} -
+
+ + +
+
+ @@ -62,13 +85,6 @@ cdkFocusInitial> {{ 'widget-config.preview' | translate }} - +
-
- -
- -
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss new file mode 100644 index 0000000000..9777decdb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2023 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 { + .widget-preview-section { + position: absolute; + top: 72px; + left: 16px; + right: 16px; + bottom: 16px; + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: 4px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index 756709966f..f910f79b01 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts @@ -14,32 +14,23 @@ /// limitations under the License. /// -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges -} from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { MatDialog } from '@angular/material/dialog'; import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models'; -import { Widget } from '@shared/models/widget.models'; +import { Widget, WidgetConfigMode } from '@shared/models/widget.models'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetConfigComponentData } from '../../models/widget-component.models'; -import { isDefined, isString } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, isString } from '@core/utils'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; @Component({ selector: 'tb-edit-widget', templateUrl: './edit-widget.component.html', - styleUrls: [] + styleUrls: ['./edit-widget.component.scss'] }) export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { @@ -73,6 +64,21 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan previewMode = false; + hasBasicMode = false; + + get widgetConfigMode(): WidgetConfigMode { + return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced; + } + + set widgetConfigMode(widgetConfigMode: WidgetConfigMode) { + if (this.hasBasicMode) { + this.widgetConfig.config.configMode = widgetConfigMode; + this.widgetFormGroup.markAsDirty(); + } + } + + private currentWidgetConfigChanged = false; + constructor(protected store: Store, private dialog: MatDialog, private fb: UntypedFormBuilder, @@ -98,18 +104,24 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan } } if (reloadConfig) { - this.previewMode = false; + if (this.currentWidgetConfigChanged) { + this.currentWidgetConfigChanged = false; + } else { + this.previewMode = false; + } this.loadWidgetConfig(); } } onApplyWidgetConfig() { if (this.widgetFormGroup.valid) { + this.currentWidgetConfigChanged = true; this.applyWidgetConfig.emit(); } } onRevertWidgetConfig() { + this.currentWidgetConfigChanged = true; this.revertWidgetConfig.emit(); } @@ -144,6 +156,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; } this.widgetConfig = { + widgetName: widgetInfo.widgetName, config: this.widget.config, layout: this.widgetLayout, widgetType: this.widget.type, @@ -155,8 +168,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan latestDataKeySettingsSchema, settingsDirective: widgetInfo.settingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, - latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective + latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, }; + this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 5789c6ff5c..a8ad2fd4c4 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -183,6 +183,9 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component'; import { DatasourceComponent } from '@home/components/widget/datasource.component'; import { DatasourcesComponent } from '@home/components/widget/datasources.component'; +import { + ManageWidgetActionsDialogComponent +} from '@home/components/widget/action/manage-widget-actions-dialog.component'; @NgModule({ declarations: @@ -234,6 +237,7 @@ import { DatasourcesComponent } from '@home/components/widget/datasources.compon DataKeyConfigDialogComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, + ManageWidgetActionsDialogComponent, CustomActionPrettyResourcesTabsComponent, CustomActionPrettyEditorComponent, MobileActionEditorComponent, @@ -386,6 +390,7 @@ import { DatasourcesComponent } from '@home/components/widget/datasources.compon DataKeyConfigDialogComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, + ManageWidgetActionsDialogComponent, CustomActionPrettyResourcesTabsComponent, CustomActionPrettyEditorComponent, MobileActionEditorComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html new file mode 100644 index 0000000000..37db477dd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -0,0 +1,51 @@ + +
+ +

{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts new file mode 100644 index 0000000000..f458243c10 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2023 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 { widgetType } from '@shared/models/widget.models'; +import { + WidgetActionCallbacks, + WidgetActionsData +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { Component, Inject, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; + +export interface ManageWidgetActionsDialogData { + widgetTitle: string; + actionsData: WidgetActionsData; + callbacks: WidgetActionCallbacks; + widgetType: widgetType; +} + +@Component({ + selector: 'tb-manage-widget-actions-dialog', + templateUrl: './manage-widget-actions-dialog.component.html', + providers: [], + styleUrls: [] +}) +export class ManageWidgetActionsDialogComponent extends DialogComponent implements OnInit { + + actionsSettings: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData, + private fb: UntypedFormBuilder, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + } + + ngOnInit() { + this.actionsSettings = this.fb.group({ + actionsData: [this.data.actionsData, []] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.actionsSettings.get('actionsData').value); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index 7a85c37973..46475911fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -
-
+
+
widget-config.actions diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss index a42feda512..2e2e1687fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -24,12 +24,6 @@ background: #fff; overflow: hidden; - &.tb-outlined-border { - box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); - border: solid 1px #e0e0e0; - border-radius: 4px; - } - .tb-entity-table-title { padding-right: 20px; white-space: nowrap; diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index d18c1319f4..925435885f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -89,6 +89,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni dragDisabled = true; private widgetResize$: ResizeObserver; + private destroyed = false; @ViewChild('searchInput') searchInputField: ElementRef; @@ -123,6 +124,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } ngOnDestroy(): void { + this.destroyed = true; if (this.widgetResize$) { this.widgetResize$.disconnect(); } @@ -340,11 +342,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.innerValue = obj; if (this.innerValue) { setTimeout(() => { - this.dataSource.setActions(this.innerValue); - if (this.viewsInited) { - this.resetSortAndFilter(true); - } else { - this.dirtyValue = true; + if (!this.destroyed) { + this.dataSource.setActions(this.innerValue); + if (this.viewsInited) { + this.resetSortAndFilter(true); + } else { + this.dirtyValue = true; + } } }, 0); } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index 01b928e354..6cd6679934 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -32,6 +32,7 @@
- + notifications @@ -112,7 +112,7 @@ [displayWith]="displayKeyFn"> - + notifications @@ -139,7 +139,7 @@ {{ translate.get('entity.no-key-matching', {key: truncate.transform(searchText, true, 6, '...')}) | async }} - + entity.create-new-key diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts index 9407145376..a5519be953 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts @@ -21,4 +21,5 @@ import { Observable } from 'rxjs'; export interface DataKeysCallbacks { generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; fetchEntityKeys: (entityAliasId: string, types: Array) => Observable>; + fetchEntityKeysForDevice: (deviceId: string, types: Array) => Observable>; } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index 71b4121c9b..5e4dafe6cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -37,8 +37,8 @@ import { NgForm, UntypedFormBuilder, UntypedFormControl, - UntypedFormGroup, ValidationErrors, - Validators + UntypedFormGroup, + ValidationErrors } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; @@ -140,6 +140,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange @Input() entityAliasId: string; + @Input() + deviceId: string; + private requiredValue: boolean; get required(): boolean { return this.requiredValue || !this.optDataKeys || this.isCountDatasource; @@ -326,15 +329,21 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'entityAliasId') { + if (['deviceId', 'entityAliasId'].includes(propName)) { this.clearSearchCache(); this.dirty = true; } else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) { - this.clearSearchCache(); - this.updateParams(); - setTimeout(() => { - this.reset(); - }, 1); + if (propName === 'datasourceType' && + [DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearSearchCache(); + } else { + this.clearSearchCache(); + this.updateParams(); + setTimeout(() => { + this.reset(); + }, 1); + } } else if (['required', 'optDataKeys'].includes(propName)) { this.updateValidators(); } @@ -468,6 +477,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange aliasController: this.aliasController, widget: this.widget, widgetType: this.widgetType, + deviceId: this.deviceId, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, callbacks: this.callbacks @@ -517,7 +527,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange if (this.datasourceType === DatasourceType.function) { const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; fetchObservable = of(targetKeysList); - } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId) { + } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || + this.datasourceType === DatasourceType.device && this.deviceId) { const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); @@ -526,7 +537,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange dataKeyTypes.push(DataKeyType.alarm); } } - fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } } else { fetchObservable = of([]); } @@ -563,6 +578,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); } + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + get inputDisabled(): boolean { return this.isCountDatasource || (this.maxDataKeysSet && this.keys.length >= this.maxDataKeys); } diff --git a/ui-ngx/src/app/modules/home/components/widget/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/datasource.component.html index 797415e687..40c63e6764 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/datasource.component.html @@ -32,15 +32,21 @@ formControlName="name"> - + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts index 1d6053e024..3f521b1d91 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts @@ -40,6 +40,7 @@ import { IAliasController } from '@core/api/widget-api.models'; import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-datasource', @@ -122,6 +123,8 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida widgetTypes = widgetType; + entityType = EntityType; + datasourceType = DatasourceType; datasourceTypes: Array = []; datasourceTypesTranslations = datasourceTypeTranslationMap; @@ -160,7 +163,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida if (this.widgetConfigComponent.functionsOnly) { this.datasourceTypes = [DatasourceType.function]; } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; + this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; if (this.widgetConfigComponent.widgetType === widgetType.latest) { this.datasourceTypes.push(DatasourceType.entityCount); this.datasourceTypes.push(DatasourceType.alarmCount); @@ -171,6 +174,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida { type: [null, [Validators.required]], name: [null, []], + deviceId: [null, []], entityAliasId: [null, []], filterId: [null, []], dataKeys: [null, []], @@ -194,6 +198,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida this.datasourceFormGroup.patchValue({ type: datasource?.type, name: datasource?.name, + deviceId: datasource?.deviceId, entityAliasId: datasource?.entityAliasId, filterId: datasource?.filterId, dataKeys: datasource?.dataKeys, @@ -231,11 +236,15 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida private updateValidators() { const type: DatasourceType = this.datasourceFormGroup.get('type').value; + this.datasourceFormGroup.get('deviceId').setValidators( + type === DatasourceType.device ? [Validators.required] : [] + ); this.datasourceFormGroup.get('entityAliasId').setValidators( (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] ); const newDataKeysRequired = !this.isDataKeysOptional(type); this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); + this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false}); this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false}); this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/widget/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/datasources.component.html index ef109765b0..98bbe96600 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasources.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/datasources.component.html @@ -77,7 +77,7 @@
-
+
+ +
+
+
+
+ + +
+
+
timewindow.timewindow
+ + +
+
+ + + {{ 'widget-config.display-timewindow' | translate }} + +
+
+
+ +
+
+ + +
+
+
widget-config.target-device
+
+ + +
+
+
+
widget-config.alarm-source
+ + +
+
+
widget-config.limits
+
+
widget-config.data-page-size
+ + + +
+
+
+ +
+
widget-config.data-settings
+
+
widget-config.units
+ + + +
+
+
widget-config.decimals
+ + + +
+
+
widget-config.no-data-display-message
+ + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index cf01244a0c..8eafe8855e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -26,6 +26,8 @@ import { JsonSchema, JsonSettingsSchema, Widget, + WidgetActionDescriptor, + WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import { @@ -60,9 +62,14 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co import { WidgetActionsData } from './action/manage-widget-actions.component.models'; import { Dashboard } from '@shared/models/dashboard.models'; import { entityFields } from '@shared/models/entity.models'; -import { Filter } from '@shared/models/query/query.models'; +import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + ManageWidgetActionsDialogComponent, + ManageWidgetActionsDialogData +} from '@home/components/widget/action/manage-widget-actions-dialog.component'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -94,6 +101,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe widgetTypes = widgetType; + widgetConfigModes = WidgetConfigMode; + entityTypes = EntityType; @Input() @@ -111,14 +120,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe @Input() functionsOnly: boolean; + @Input() + @coerceBoolean() + hideHeader = false; + + @Input() + @coerceBoolean() + hideToggleHeader = false; + @Input() disabled: boolean; + @Input() + widgetConfigMode = WidgetConfigMode.advanced; + widgetType: widgetType; widgetConfigCallbacks: WidgetConfigCallbacks = { createEntityAlias: this.createEntityAlias.bind(this), createFilter: this.createFilter.bind(this), generateDataKey: this.generateDataKey.bind(this), + fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this), fetchEntityKeys: this.fetchEntityKeys.bind(this), fetchDashboardStates: this.fetchDashboardStates.bind(this) }; @@ -151,7 +172,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe private entityService: EntityService, private dialog: MatDialog, public translate: TranslateService, - private fb: UntypedFormBuilder) { + private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef) { super(store); } @@ -288,7 +310,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe value: 'mobile' } ); - this.selectedOption = this.headerOptions[0].value; + if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) { + this.selectedOption = this.headerOptions[0].value; + } } private buildForms() { @@ -607,6 +631,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget; } + public get widgetActionSourceIds(): Array { + const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value; + return actionsData?.actionsMap ? Object.keys(actionsData.actionsMap) : []; + } + + public widgetActionsByActionSourceId(actionSourceId: string): Array { + const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value; + return actionsData?.actionsMap[actionSourceId] || []; + } + + public get hasWidgetActions(): boolean { + const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value; + if (actionsData?.actionsMap) { + for (const actionSourceId of Object.keys(actionsData.actionsMap)) { + if (actionsData.actionsMap[actionSourceId] && actionsData.actionsMap[actionSourceId].length) { + return true; + } + } + } + return false; + } + public onlyHistoryTimewindow(): boolean { if (this.widgetType === widgetType.latest) { const datasources = this.dataSettings.get('datasources').value; @@ -616,6 +662,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } } + public manageWidgetActions() { + const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value; + this.dialog.open(ManageWidgetActionsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + widgetTitle: this.modelValue.widgetName, + callbacks: this.widgetConfigCallbacks, + actionsData: deepClone(actionsData), + widgetType: this.widgetType + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.actionsSettings.get('actionsData').patchValue(res); + this.cd.markForCheck(); + } + } + ); + } + public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey { if (isObject(chip)) { (chip as DataKey)._hash = Math.random(); @@ -711,6 +779,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe ); } + private fetchEntityKeysForDevice(deviceId: string, dataKeyTypes: Array): Observable> { + const entityFilter = singleEntityFilterFromDeviceId(deviceId); + return this.entityService.getEntityKeysByEntityFilter( + entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + } + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { return this.aliasController.getAliasInfo(entityAliasId).pipe( mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.scss index 25e91e55be..bc269e5a67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.scss @@ -22,6 +22,7 @@ flex-direction: column; color: rgba(0, 0, 0, 0.87); letter-spacing: 0.15px; + position: relative; &.no-padding-bottom { padding-bottom: 0; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html index 7948cb19d4..7d64036838 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html @@ -26,7 +26,6 @@ [isEditActionEnabled]="false" [isRemoveActionEnabled]="false"> -widget-config.preview
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss index 73636f346b..1782251f61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss @@ -23,20 +23,10 @@ left: 0; right: 0; } - .tb-preview-title { - position: absolute; - top: 16px; - left: 16px; - font-weight: 500; - font-size: 13px; - line-height: 16px; - letter-spacing: normal; - color: rgba(0, 0, 0, 0.38); - } .tb-preview-panel { position: absolute; top: 16px; - right: 24px; + left: 24px; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts index 3d82caa9a7..4b62bba653 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts @@ -14,8 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { Widget, WidgetConfig } from '@shared/models/widget.models'; @@ -28,7 +27,7 @@ import { deepClone } from '@core/utils'; templateUrl: './widget-preview.component.html', styleUrls: ['./widget-preview.component.scss'] }) -export class WidgetPreviewComponent extends PageComponent implements OnInit { +export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges { @Input() aliasController: IAliasController; @@ -49,6 +48,25 @@ export class WidgetPreviewComponent extends PageComponent implements OnInit { } ngOnInit(): void { + this.loadPreviewWidget(); + } + + ngOnChanges(changes: SimpleChanges): void { + let reloadPreviewWidget = false; + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['widget', 'widgetConfig'].includes(propName)) { + reloadPreviewWidget = true; + } + } + } + if (reloadPreviewWidget) { + this.loadPreviewWidget(); + } + } + + private loadPreviewWidget() { const sizeX = this.widget.sizeX * 2; const sizeY = this.widget.sizeY * 2; const col = Math.floor(Math.max(0, (20 - sizeX) / 2)); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 8ddb3d6218..090d637c99 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -447,6 +447,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri } export interface WidgetConfigComponentData { + widgetName: string; config: WidgetConfig; layout: WidgetLayout; widgetType: widgetType; @@ -551,6 +552,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { settingsDirective: widgetTypeEntity.descriptor.settingsDirective, dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, + hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode, + basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective, defaultConfig: widgetTypeEntity.descriptor.defaultConfig }; } @@ -581,6 +584,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: settingsDirective: widgetInfo.settingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + hasBasicMode: widgetInfo.hasBasicMode, + basicModeDirective: widgetInfo.basicModeDirective, defaultConfig: widgetInfo.defaultConfig }; return { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index c4b859038e..046ad5acb2 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -270,6 +270,17 @@ [(ngModel)]="widget.latestDataKeySettingsDirective" (ngModelChange)="isDirty = true"/> + + {{ 'widget.has-basic-mode' | translate }} + + + widget.basic-mode-form-selector + +
diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index a8cd40f9b8..af0373b7a9 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -38,7 +38,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { isEqual } from '@core/utils'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ @@ -291,7 +291,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit async writeValue(value: string | EntityId | null): Promise { this.searchText = ''; - if (value !== null && (typeof value === 'string' || (value.entityType && value.id))) { + if (isDefinedAndNotNull(value) && (typeof value === 'string' || (value.entityType && value.id))) { let targetEntityType: EntityType; let id: string; if (typeof value === 'string') { diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index 3cc0f8beaf..e36cdbd592 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -16,9 +16,11 @@ --> - {{ option.name }} + {{ option.name }} diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss index e135030bed..a9542012d4 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -68,6 +68,17 @@ } } } + &.tb-invert { + .mat-button-toggle.mat-button-toggle-appearance-standard { + color: rgba(255, 255, 255, 0.8); + &.mat-button-toggle-checked { + .mat-button-toggle-button { + background: #FFFFFF; + color: $tb-primary-color; + } + } + } + } } } @media #{$mat-md-lg} { diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index 7a66ac6c16..a7f37f53a4 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -38,13 +38,14 @@ import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button- import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { BreadCrumb } from '@shared/components/breadcrumb'; export interface ToggleHeaderOption { name: string; value: any; } -export type ToggleHeaderAppearance = 'fill' | 'stroked'; +export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; @Component({ selector: 'tb-toggle-header', @@ -96,4 +97,8 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit { } ); } + + trackByHeaderOption(index: number, option: ToggleHeaderOption){ + return option.value; + } } diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 4ee89fc89f..7c978fb4c9 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -749,6 +749,14 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); +export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({ + type: AliasFilterType.singleEntity, + singleEntity: { + entityType: EntityType.DEVICE, + id: deviceId + } +}); + export interface EntityCountQuery { entityFilter: EntityFilter; keyFilters?: Array; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 77d2acf3e2..bfe9021926 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -162,6 +162,8 @@ export interface WidgetTypeDescriptor { settingsDirective?: string; dataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string; + hasBasicMode?: boolean; + basicModeDirective?: string; defaultConfig: string; sizeX: number; sizeY: number; @@ -318,6 +320,7 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', + device = 'device', entity = 'entity', entityCount = 'entityCount', alarmCount = 'alarmCount' @@ -326,6 +329,7 @@ export enum DatasourceType { export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], + [ DatasourceType.device, 'device.device' ], [ DatasourceType.entity, 'entity.entity' ], [ DatasourceType.entityCount, 'entity.entities-count' ], [ DatasourceType.alarmCount, 'entity.alarms-count' ] @@ -341,6 +345,7 @@ export interface Datasource { entityType?: EntityType; entityId?: string; entityName?: string; + deviceId?: string; entityAliasId?: string; filterId?: string; unresolvedStateEntity?: boolean; @@ -600,7 +605,13 @@ export interface WidgetSettings { [key: string]: any; } +export enum WidgetConfigMode { + basic = 'basic', + advanced = 'advanced' +} + export interface WidgetConfig { + configMode?: WidgetConfigMode; title?: string; titleIcon?: string; showTitle?: boolean; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3496b5e65b..7f0a8ffdcb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3997,6 +3997,10 @@ "settings-form-selector": "Settings form selector", "data-key-settings-form-selector": "Data key settings form selector", "latest-data-key-settings-form-selector": "Latest data key settings form selector", + "has-basic-mode": "Has basic mode", + "basic-mode-form-selector": "Basic mode form selector", + "basic-mode": "Basic", + "advanced-mode": "Advanced", "javascript": "Javascript", "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",