Browse Source

UI: Introduce device datasource type. Implement basic and advanced widget config modes.

pull/8708/head
Igor Kulikov 3 years ago
parent
commit
ee159be366
  1. 7
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 28
      ui-ngx/src/app/core/api/alias-controller.ts
  3. 4
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  4. 32
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  5. 30
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  6. 14
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  8. 20
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  9. 26
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  10. 44
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  11. 5
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  12. 51
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html
  13. 70
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts
  14. 4
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  15. 6
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  16. 14
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  17. 1
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html
  18. 1
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts
  19. 11
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  20. 6
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  21. 1
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts
  22. 39
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  23. 16
      ui-ngx/src/app/modules/home/components/widget/datasource.component.html
  24. 11
      ui-ngx/src/app/modules/home/components/widget/datasource.component.ts
  25. 2
      ui-ngx/src/app/modules/home/components/widget/datasources.component.html
  26. 5
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  27. 239
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  28. 87
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  29. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.scss
  30. 1
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  31. 12
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  32. 24
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  33. 5
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  34. 11
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  35. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  36. 6
      ui-ngx/src/app/shared/components/toggle-header.component.html
  37. 11
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  38. 7
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  39. 8
      ui-ngx/src/app/shared/models/query/query.models.ts
  40. 11
      ui-ngx/src/app/shared/models/widget.models.ts
  41. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

28
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<Datasource> {
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;

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

32
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -19,7 +19,21 @@
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="hasBasicMode" (valueChange)="widgetConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="[
{
name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -35,6 +49,8 @@
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
@ -42,6 +58,13 @@
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset>
</div>
@ -62,13 +85,6 @@
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
<button *ngIf="previewMode"
mat-button color="primary"
type="button"
(click)="previewMode = false"
cdkFocusInitial>
{{ 'action.back' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">

30
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -22,11 +22,12 @@ import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { isDefined, isString } from '@core/utils';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
export interface AddWidgetDialogData {
dashboard: Dashboard;
@ -45,6 +46,8 @@ export interface AddWidgetDialogData {
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {
widgetConfigModes = WidgetConfigMode;
widgetFormGroup: UntypedFormGroup;
dashboard: Dashboard;
@ -52,12 +55,28 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController;
widget: Widget;
widgetConfig: WidgetConfigComponentData;
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();
}
}
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
public translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
@ -96,7 +115,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
const widgetConfig: WidgetConfigComponentData = {
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
layout: {},
widgetType: this.widget.type,
@ -110,9 +130,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective
};
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup = this.fb.group({
widgetConfig: [widgetConfig, []]
widgetConfig: [this.widgetConfig, []]
}
);
}

14
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -359,7 +359,19 @@
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="tbEditWidget.hasBasicMode" (valueChange)="tbEditWidget.widgetConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="[
{
name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="tbEditWidget.widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<tb-edit-widget #tbEditWidget

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -332,7 +332,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private mobileService: MobileService,
private fb: UntypedFormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
public translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private ngZone: NgZone,

20
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -22,9 +22,19 @@
[functionsOnly]="widgetEditMode"
[dashboard]="dashboard"
[widget]="widget"
[hideToggleHeader]="previewMode"
[widgetConfigMode]="widgetConfigMode"
formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix">
<button mat-button
*ngIf="previewMode"
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
<div class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<button [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
<button *ngIf="!previewMode" [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
@ -44,17 +54,11 @@
</button>
</div>
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
<tb-widget-preview *ngIf="previewMode" class="widget-preview-section"
[aliasController]="aliasController"
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button color="primary"
(click)="previewMode = false">
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset>
</form>

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

44
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<AppState>,
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});
}
}

5
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,

51
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html

@ -0,0 +1,51 @@
<!--
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.
-->
<form [formGroup]="actionsSettings" style="width: 900px;">
<mat-toolbar color="primary">
<h2>{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content style="position: relative; height: 500px;">
<tb-manage-widget-actions
[callbacks]="data.callbacks"
[widgetType] = "data.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="save()"
[disabled]="(isLoading$ | async) || actionsSettings.invalid || !actionsSettings.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

70
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<ManageWidgetActionsDialogComponent,
WidgetActionsData> implements OnInit {
actionsSettings: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData,
private fb: UntypedFormBuilder,
public dialogRef: MatDialogRef<ManageWidgetActionsDialogComponent, WidgetActionsData>) {
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);
}
}

4
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -15,8 +15,8 @@
limitations under the License.
-->
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<div class="tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>widget-config.actions</span>

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

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

1
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html

@ -32,6 +32,7 @@
<tb-data-key-config #dataKeyConfig
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId"
[entityAliasId]="data.entityAliasId"
[dashboard]="data.dashboard"
[aliasController]="data.aliasController"

1
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts

@ -36,6 +36,7 @@ export interface DataKeyConfigDialogData {
aliasController: IAliasController;
widget: Widget;
widgetType: widgetType;
deviceId?: string;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;

11
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts

@ -90,6 +90,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
deviceId: string;
@Input()
entityAliasId: string;
@ -377,9 +380,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys);
} else {
if (this.entityAliasId) {
if (this.deviceId || this.entityAliasId) {
const dataKeyTypes = [this.modelValue.type];
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
if (this.deviceId) {
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes);
} else {
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
}
} else {
fetchObservable = of([]);
}

6
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -48,7 +48,7 @@
</div>
<div class="tb-chip-labels">
<div class="tb-chip-label">
<ng-container *ngIf="datasourceType === datasourceTypes.entity">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
@ -112,7 +112,7 @@
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<ng-container *ngIf="datasourceType === datasourceTypes.entity">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
@ -139,7 +139,7 @@
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="datasourceType !== datasourceTypes.entity; else createEntityKey">
<span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>

1
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<DataKeyType>) => Observable<Array<DataKey>>;
fetchEntityKeysForDevice: (deviceId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
}

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

16
ui-ngx/src/app/modules/home/components/widget/datasource.component.html

@ -32,15 +32,21 @@
formControlName="name">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.entityCount ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''">
<ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.device ||
datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.entityCount ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount"
propagatedFilter="false"
style="height: 56px; margin-bottom: 22px;"
formControlName="alarmFilterConfig"></tb-alarm-filter-config>
<tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device"
required
[entityType]="entityType.DEVICE"
formControlName="deviceId">
</tb-entity-autocomplete>
<tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
@ -76,6 +82,7 @@
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="dataKeys">
</tb-data-keys>
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
@ -89,6 +96,7 @@
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="latestDataKeys">
</tb-data-keys>
</section>

11
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<DatasourceType> = [];
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});
}

2
ui-ngx/src/app/modules/home/components/widget/datasources.component.html

@ -77,7 +77,7 @@
</mat-list>
</div>
</ng-template>
<div *ngIf="showAddDatasource" style="padding-left: 42px;">
<div *ngIf="showAddDatasource">
<button type="button"
mat-stroked-button color="primary"
(click)="addDatasource()"

5
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -99,6 +99,8 @@ export class WidgetComponentService {
settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
);
@ -404,6 +406,9 @@ export class WidgetComponentService {
if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) {
directives.push(widgetInfo.latestDataKeySettingsDirective);
}
if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) {
directives.push(widgetInfo.basicModeDirective);
}
if (directives.length) {
factories.filter((factory) => directives.includes(factory.selector))
.forEach((foundFactory) => {

239
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -16,107 +16,19 @@
-->
<div class="tb-widget-config tb-absolute-fill">
<div class="tb-widget-config-header">
<tb-toggle-header (valueChange)="selectedOption = $event" ignoreMdLgSize="true"
<div class="tb-widget-config-header" *ngIf="!hideHeader">
<tb-toggle-header *ngIf="!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced" (valueChange)="selectedOption = $event" ignoreMdLgSize="true"
appearance="fill" [options]="headerOptions" [value]="selectedOption" name="widgetConfigHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<ng-content select=".tb-widget-config-header-prefix"></ng-content>
<ng-content select=".tb-widget-config-header-suffix"></ng-content>
</div>
<div class="tb-widget-config-content">
<div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" [formGroup]="dataSettings" class="mat-content">
<div *ngIf="displayTimewindowConfig" class="tb-widget-config-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div>
<tb-toggle-header (valueChange)="dataSettings.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true"
[options]="[
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true},
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false}
]"
[value]="dataSettings.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false">
</tb-toggle-header>
</div>
<div class="tb-widget-config-row">
<tb-timewindow asButton="true"
strokedButton
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow()"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<mat-slide-toggle class="mat-slide" formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }}
</mat-slide-toggle>
</div>
</div>
<div class="tb-widget-config-panel" *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config>
</div>
<tb-datasources
formControlName="datasources" *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
modelValue?.isDataEnabled">
</tb-datasources>
<div class="tb-widget-config-panel" *ngIf="widgetType === widgetTypes.rpc &&
modelValue?.isDataEnabled">
<div class="tb-widget-config-panel-title" translate>widget-config.target-device</div>
<div [formGroup]="targetDeviceSettings">
<tb-entity-alias-select fxFlex
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
</div>
<div class="tb-widget-config-panel" *ngIf="widgetType === widgetTypes.alarm &&
modelValue?.isDataEnabled">
<div class="tb-widget-config-panel-title" translate>widget-config.alarm-source</div>
<tb-datasource
formControlName="alarmSource">
</tb-datasource>
</div>
<div *ngIf="displayLimits" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.limits</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.data-page-size</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="pageSize" type="number" min="1" step="1">
</mat-form-field>
</div>
</div>
<div *ngIf="widgetConfigMode === widgetConfigModes.advanced; else basicMode" class="tb-widget-config-content">
<div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
</div>
<div [fxShow]="selectedOption === 'appearance'" class="mat-content">
<div *ngIf="displayAppearanceDataSettings" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.units</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput formControlName="units" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.decimals</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="displayNoDataDisplayMessageConfig" class="tb-widget-config-row">
<div translate>widget-config.no-data-display-message</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="noDataDisplayMessage" placeholder="{{ 'widget-config.set-message' | translate }}">
</mat-form-field>
</div>
</div>
<div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="advancedSettings">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
</div>
<div [fxShow]="selectedOption === 'card'" [formGroup]="widgetSettings" class="mat-content">
<div class="tb-widget-config-panel">
@ -231,11 +143,13 @@
</div>
</div>
<div [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" style="height: 100%;">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
<div class="tb-widget-config-panel" style="height: 100%;">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
</div>
</div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-widget-config-panel">
@ -264,5 +178,130 @@
</div>
</div>
</div>
<ng-template #basicMode>
<div class="tb-widget-config-content">
<div class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
<div class="tb-widget-config-panel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div class="tb-widget-config-panel-title" translate style="padding-right: 48px;">widget-config.actions</div>
<mat-chip-listbox fxFlex>
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds">
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)">
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon>
{{ widgetAction.name }}
</mat-chip>
</ng-container>
</mat-chip-listbox>
<button mat-stroked-button color="primary" *ngIf="!hasWidgetActions">
{{ 'widget-config.add-action' | translate }}
</button>
<button mat-icon-button color="primary" *ngIf="hasWidgetActions">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template #data>
<div *ngIf="displayTimewindowConfig" [formGroup]="dataSettings" class="tb-widget-config-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div>
<tb-toggle-header (valueChange)="dataSettings.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true"
[options]="[
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true},
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false}
]"
[value]="dataSettings.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false">
</tb-toggle-header>
</div>
<div class="tb-widget-config-row">
<tb-timewindow asButton="true"
strokedButton
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow()"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<mat-slide-toggle class="mat-slide" formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }}
</mat-slide-toggle>
</div>
</div>
<div *ngIf="widgetType === widgetTypes.alarm" [formGroup]="dataSettings" class="tb-widget-config-panel" fxLayout="column" fxLayoutAlign="center">
<tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config>
</div>
<div *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
modelValue?.isDataEnabled"
[formGroup]="dataSettings">
<tb-datasources
formControlName="datasources">
</tb-datasources>
</div>
<div class="tb-widget-config-panel" *ngIf="widgetType === widgetTypes.rpc &&
modelValue?.isDataEnabled">
<div class="tb-widget-config-panel-title" translate>widget-config.target-device</div>
<div [formGroup]="targetDeviceSettings">
<tb-entity-alias-select fxFlex
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
</div>
<div *ngIf="widgetType === widgetTypes.alarm && modelValue?.isDataEnabled"
[formGroup]="dataSettings" class="tb-widget-config-panel" >
<div class="tb-widget-config-panel-title" translate>widget-config.alarm-source</div>
<tb-datasource
formControlName="alarmSource">
</tb-datasource>
</div>
<div *ngIf="displayLimits" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.limits</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.data-page-size</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="pageSize" type="number" min="1" step="1">
</mat-form-field>
</div>
</div>
</ng-template>
<ng-template #appearance>
<div *ngIf="displayAppearanceDataSettings" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.units</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput formControlName="units" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.decimals</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="displayNoDataDisplayMessageConfig" class="tb-widget-config-row">
<div translate>widget-config.no-data-display-message</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="noDataDisplayMessage" placeholder="{{ 'widget-config.set-message' | translate }}">
</mat-form-field>
</div>
</div>
<div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="advancedSettings">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
</ng-template>
</div>

87
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<string> {
const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value;
return actionsData?.actionsMap ? Object.keys(actionsData.actionsMap) : [];
}
public widgetActionsByActionSourceId(actionSourceId: string): Array<WidgetActionDescriptor> {
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, ManageWidgetActionsDialogData,
WidgetActionsData>(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<DataKeyType>): Observable<Array<DataKey>> {
const entityFilter = singleEntityFilterFromDeviceId(deviceId);
return this.entityService.getEntityKeysByEntityFilter(
entityFilter,
dataKeyTypes,
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
);
}
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(

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

1
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html

@ -26,7 +26,6 @@
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">
</tb-dashboard>
<span class="tb-preview-title" translate>widget-config.preview</span>
<div class="tb-preview-panel">
<ng-content select=".tb-preview-panel-content"></ng-content>
</div>

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

24
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));

5
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 {

11
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -270,6 +270,17 @@
[(ngModel)]="widget.latestDataKeySettingsDirective"
(ngModelChange)="isDirty = true"/>
</mat-form-field>
<mat-slide-toggle class="mat-block" style="padding-bottom: 16px;"
[(ngModel)]="widget.hasBasicMode"
(ngModelChange)="isDirty = true">
{{ 'widget.has-basic-mode' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="widget.hasBasicMode" class="mat-block">
<mat-label translate>widget.basic-mode-form-selector</mat-label>
<input matInput
[(ngModel)]="widget.basicModeDirective"
(ngModelChange)="isDirty = true"/>
</mat-form-field>
</div>
</div>
</mat-tab>

4
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<void> {
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') {

6
ui-ngx/src/app/shared/components/toggle-header.component.html

@ -16,9 +16,11 @@
-->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header"
[ngClass]="{'tb-fill': appearance === 'fill', 'tb-ignore-md-lg': ignoreMdLgSize}" [name]="name" [(ngModel)]="value"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize}" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle>
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">

11
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} {

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

8
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<KeyFilter>;

11
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, string>(
[
[ 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;

4
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}}'?",

Loading…
Cancel
Save