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, AlarmFilter,
AlarmFilterConfig, AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, Filter,
FilterInfo,
filterInfoToKeyFilters,
Filters,
KeyFilter,
singleEntityDataPageLink,
singleEntityFilterFromDeviceId,
updateDatasourceFromEntityInfo updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -246,6 +252,9 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.device) {
newDatasource.type = DatasourceType.entity;
}
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) { || newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
@ -254,7 +263,22 @@ export class AliasController implements IAliasController {
if (newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.type === DatasourceType.alarmCount) {
newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); 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( return this.getAliasInfo(newDatasource.entityAliasId).pipe(
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
newDatasource.aliasName = aliasInfo.alias; 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 = [];
} }
widgetConfig.datasources.forEach((datasource) => { widgetConfig.datasources.forEach((datasource) => {
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
}
if (datasource.deviceAliasId) { if (datasource.deviceAliasId) {
datasource.type = DatasourceType.entity;
datasource.entityAliasId = datasource.deviceAliasId; datasource.entityAliasId = datasource.deviceAliasId;
delete 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"> <mat-toolbar color="primary">
<h2 translate>widget.add</h2> <h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span> <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 <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -35,6 +49,8 @@
[functionsOnly]="false" [functionsOnly]="false"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" <tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
@ -42,6 +58,13 @@
[stateController]="stateController" [stateController]="stateController"
[widget]="widget" [widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> [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> </tb-widget-preview>
</fieldset> </fieldset>
</div> </div>
@ -62,13 +85,6 @@
cdkFocusInitial> cdkFocusInitial>
{{ 'widget-config.preview' | translate }} {{ 'widget-config.preview' | translate }}
</button> </button>
<button *ngIf="previewMode"
mat-button color="primary"
type="button"
(click)="previewMode = false"
cdkFocusInitial>
{{ 'action.back' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="add()" (click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"> [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 { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; 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 { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.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 { export interface AddWidgetDialogData {
dashboard: Dashboard; dashboard: Dashboard;
@ -45,6 +46,8 @@ export interface AddWidgetDialogData {
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget> export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {
widgetConfigModes = WidgetConfigMode;
widgetFormGroup: UntypedFormGroup; widgetFormGroup: UntypedFormGroup;
dashboard: Dashboard; dashboard: Dashboard;
@ -52,12 +55,28 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController; stateController: IStateController;
widget: Widget; widget: Widget;
widgetConfig: WidgetConfigComponentData;
previewMode = false; 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; submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
public translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>, public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
@ -96,7 +115,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ? latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
} }
const widgetConfig: WidgetConfigComponentData = { this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config, config: this.widget.config,
layout: {}, layout: {},
widgetType: this.widget.type, widgetType: this.widget.type,
@ -110,9 +130,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup = this.fb.group({ 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 }}" headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="true" [isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()"> (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-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
<tb-edit-widget #tbEditWidget <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 mobileService: MobileService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private dialog: MatDialog, private dialog: MatDialog,
private translate: TranslateService, public translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private ngZone: NgZone, private ngZone: NgZone,

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

@ -22,9 +22,19 @@
[functionsOnly]="widgetEditMode" [functionsOnly]="widgetEditMode"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideToggleHeader]="previewMode"
[widgetConfigMode]="widgetConfigMode"
formControlName="widgetConfig"> 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"> <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" mat-button color="primary"
(click)="previewMode = true"> (click)="previewMode = true">
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
@ -44,17 +54,11 @@
</button> </button>
</div> </div>
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" <tb-widget-preview *ngIf="previewMode" class="widget-preview-section"
[aliasController]="aliasController" [aliasController]="aliasController"
[stateController]="stateController" [stateController]="stateController"
[widget]="widget" [widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> [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> </tb-widget-preview>
</fieldset> </fieldset>
</form> </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. /// limitations under the License.
/// ///
import { import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models'; import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models';
import { IAliasController, IStateController } from '@core/api/widget-api.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 { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models'; 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'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@Component({ @Component({
selector: 'tb-edit-widget', selector: 'tb-edit-widget',
templateUrl: './edit-widget.component.html', templateUrl: './edit-widget.component.html',
styleUrls: [] styleUrls: ['./edit-widget.component.scss']
}) })
export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges {
@ -73,6 +64,21 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
previewMode = false; 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>, constructor(protected store: Store<AppState>,
private dialog: MatDialog, private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@ -98,18 +104,24 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
} }
} }
if (reloadConfig) { if (reloadConfig) {
this.previewMode = false; if (this.currentWidgetConfigChanged) {
this.currentWidgetConfigChanged = false;
} else {
this.previewMode = false;
}
this.loadWidgetConfig(); this.loadWidgetConfig();
} }
} }
onApplyWidgetConfig() { onApplyWidgetConfig() {
if (this.widgetFormGroup.valid) { if (this.widgetFormGroup.valid) {
this.currentWidgetConfigChanged = true;
this.applyWidgetConfig.emit(); this.applyWidgetConfig.emit();
} }
} }
onRevertWidgetConfig() { onRevertWidgetConfig() {
this.currentWidgetConfigChanged = true;
this.revertWidgetConfig.emit(); this.revertWidgetConfig.emit();
} }
@ -144,6 +156,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
} }
this.widgetConfig = { this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config, config: this.widget.config,
layout: this.widgetLayout, layout: this.widgetLayout,
widgetType: this.widget.type, widgetType: this.widget.type,
@ -155,8 +168,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
latestDataKeySettingsSchema, latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); 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 { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component';
import { DatasourceComponent } from '@home/components/widget/datasource.component'; import { DatasourceComponent } from '@home/components/widget/datasource.component';
import { DatasourcesComponent } from '@home/components/widget/datasources.component'; import { DatasourcesComponent } from '@home/components/widget/datasources.component';
import {
ManageWidgetActionsDialogComponent
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -234,6 +237,7 @@ import { DatasourcesComponent } from '@home/components/widget/datasources.compon
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent, CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent, CustomActionPrettyEditorComponent,
MobileActionEditorComponent, MobileActionEditorComponent,
@ -386,6 +390,7 @@ import { DatasourcesComponent } from '@home/components/widget/datasources.compon
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent, CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent, CustomActionPrettyEditorComponent,
MobileActionEditorComponent, 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. limitations under the License.
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border"> <div fxFlex fxLayout="column" class="tb-entity-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>widget-config.actions</span> <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; background: #fff;
overflow: hidden; 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 { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; 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; dragDisabled = true;
private widgetResize$: ResizeObserver; private widgetResize$: ResizeObserver;
private destroyed = false;
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
@ -123,6 +124,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
if (this.widgetResize$) { if (this.widgetResize$) {
this.widgetResize$.disconnect(); this.widgetResize$.disconnect();
} }
@ -340,11 +342,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
this.innerValue = obj; this.innerValue = obj;
if (this.innerValue) { if (this.innerValue) {
setTimeout(() => { setTimeout(() => {
this.dataSource.setActions(this.innerValue); if (!this.destroyed) {
if (this.viewsInited) { this.dataSource.setActions(this.innerValue);
this.resetSortAndFilter(true); if (this.viewsInited) {
} else { this.resetSortAndFilter(true);
this.dirtyValue = true; } else {
this.dirtyValue = true;
}
} }
}, 0); }, 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 <tb-data-key-config #dataKeyConfig
[dataKeySettingsSchema]="data.dataKeySettingsSchema" [dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective" [dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId"
[entityAliasId]="data.entityAliasId" [entityAliasId]="data.entityAliasId"
[dashboard]="data.dashboard" [dashboard]="data.dashboard"
[aliasController]="data.aliasController" [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; aliasController: IAliasController;
widget: Widget; widget: Widget;
widgetType: widgetType; widgetType: widgetType;
deviceId?: string;
entityAliasId?: string; entityAliasId?: string;
showPostProcessing?: boolean; showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks; 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; comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
deviceId: string;
@Input() @Input()
entityAliasId: string; entityAliasId: string;
@ -377,9 +380,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.type === DataKeyType.alarm) { if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys); fetchObservable = of(this.alarmKeys);
} else { } else {
if (this.entityAliasId) { if (this.deviceId || this.entityAliasId) {
const dataKeyTypes = [this.modelValue.type]; 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 { } else {
fetchObservable = of([]); fetchObservable = of([]);
} }

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

@ -48,7 +48,7 @@
</div> </div>
<div class="tb-chip-labels"> <div class="tb-chip-labels">
<div class="tb-chip-label"> <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" <mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon> matTooltipPosition="above">notifications</mat-icon>
@ -112,7 +112,7 @@
[displayWith]="displayKeyFn"> [displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> <mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;"> <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" <mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon> matTooltipPosition="above">notifications</mat-icon>
@ -139,7 +139,7 @@
{{ translate.get('entity.no-key-matching', {{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }} {key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span *ngIf="datasourceType !== datasourceTypes.entity; else createEntityKey"> <span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a> <a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span> </span>
<ng-template #createEntityKey> <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 { export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey;
fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<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, NgForm,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, ValidationErrors, UntypedFormGroup,
Validators ValidationErrors
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
@ -140,6 +140,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input() @Input()
entityAliasId: string; entityAliasId: string;
@Input()
deviceId: string;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue || !this.optDataKeys || this.isCountDatasource; 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)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) { if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'entityAliasId') { if (['deviceId', 'entityAliasId'].includes(propName)) {
this.clearSearchCache(); this.clearSearchCache();
this.dirty = true; this.dirty = true;
} else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) { } else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) {
this.clearSearchCache(); if (propName === 'datasourceType' &&
this.updateParams(); [DatasourceType.device, DatasourceType.entity].includes(change.previousValue) &&
setTimeout(() => { [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) {
this.reset(); this.clearSearchCache();
}, 1); } else {
this.clearSearchCache();
this.updateParams();
setTimeout(() => {
this.reset();
}, 1);
}
} else if (['required', 'optDataKeys'].includes(propName)) { } else if (['required', 'optDataKeys'].includes(propName)) {
this.updateValidators(); this.updateValidators();
} }
@ -468,6 +477,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
aliasController: this.aliasController, aliasController: this.aliasController,
widget: this.widget, widget: this.widget,
widgetType: this.widgetType, widgetType: this.widgetType,
deviceId: this.deviceId,
entityAliasId: this.entityAliasId, entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm, showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks callbacks: this.callbacks
@ -517,7 +527,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList); 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]; const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute); dataKeyTypes.push(DataKeyType.attribute);
@ -526,7 +537,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
dataKeyTypes.push(DataKeyType.alarm); 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 { } else {
fetchObservable = of([]); fetchObservable = of([]);
} }
@ -563,6 +578,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType);
} }
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
get inputDisabled(): boolean { get inputDisabled(): boolean {
return this.isCountDatasource || (this.maxDataKeysSet && this.keys.length >= this.maxDataKeys); 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"> formControlName="name">
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.entity || <ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.device ||
datasourceFormGroup.get('type').value === datasourceType.entityCount || datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''"> 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" <tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount"
propagatedFilter="false" propagatedFilter="false"
style="height: 56px; margin-bottom: 22px;" style="height: 56px; margin-bottom: 22px;"
formControlName="alarmFilterConfig"></tb-alarm-filter-config> 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 <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" [showLabel]="true"
[tbRequired]="true" [tbRequired]="true"
[aliasController]="aliasController" [aliasController]="aliasController"
@ -76,6 +82,7 @@
[widget]="widget" [widget]="widget"
[callbacks]="dataKeysCallbacks" [callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="dataKeys"> formControlName="dataKeys">
</tb-data-keys> </tb-data-keys>
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex <tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
@ -89,6 +96,7 @@
[widget]="widget" [widget]="widget"
[callbacks]="dataKeysCallbacks" [callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" [entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="latestDataKeys"> formControlName="latestDataKeys">
</tb-data-keys> </tb-data-keys>
</section> </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 { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';
@Component({ @Component({
selector: 'tb-datasource', selector: 'tb-datasource',
@ -122,6 +123,8 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
widgetTypes = widgetType; widgetTypes = widgetType;
entityType = EntityType;
datasourceType = DatasourceType; datasourceType = DatasourceType;
datasourceTypes: Array<DatasourceType> = []; datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap; datasourceTypesTranslations = datasourceTypeTranslationMap;
@ -160,7 +163,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
if (this.widgetConfigComponent.functionsOnly) { if (this.widgetConfigComponent.functionsOnly) {
this.datasourceTypes = [DatasourceType.function]; this.datasourceTypes = [DatasourceType.function];
} else { } else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
if (this.widgetConfigComponent.widgetType === widgetType.latest) { if (this.widgetConfigComponent.widgetType === widgetType.latest) {
this.datasourceTypes.push(DatasourceType.entityCount); this.datasourceTypes.push(DatasourceType.entityCount);
this.datasourceTypes.push(DatasourceType.alarmCount); this.datasourceTypes.push(DatasourceType.alarmCount);
@ -171,6 +174,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
{ {
type: [null, [Validators.required]], type: [null, [Validators.required]],
name: [null, []], name: [null, []],
deviceId: [null, []],
entityAliasId: [null, []], entityAliasId: [null, []],
filterId: [null, []], filterId: [null, []],
dataKeys: [null, []], dataKeys: [null, []],
@ -194,6 +198,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
this.datasourceFormGroup.patchValue({ this.datasourceFormGroup.patchValue({
type: datasource?.type, type: datasource?.type,
name: datasource?.name, name: datasource?.name,
deviceId: datasource?.deviceId,
entityAliasId: datasource?.entityAliasId, entityAliasId: datasource?.entityAliasId,
filterId: datasource?.filterId, filterId: datasource?.filterId,
dataKeys: datasource?.dataKeys, dataKeys: datasource?.dataKeys,
@ -231,11 +236,15 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
private updateValidators() { private updateValidators() {
const type: DatasourceType = this.datasourceFormGroup.get('type').value; const type: DatasourceType = this.datasourceFormGroup.get('type').value;
this.datasourceFormGroup.get('deviceId').setValidators(
type === DatasourceType.device ? [Validators.required] : []
);
this.datasourceFormGroup.get('entityAliasId').setValidators( this.datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
); );
const newDataKeysRequired = !this.isDataKeysOptional(type); const newDataKeysRequired = !this.isDataKeysOptional(type);
this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); 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('entityAliasId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('dataKeys').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> </mat-list>
</div> </div>
</ng-template> </ng-template>
<div *ngIf="showAddDatasource" style="padding-left: 42px;"> <div *ngIf="showAddDatasource">
<button type="button" <button type="button"
mat-stroked-button color="primary" mat-stroked-button color="primary"
(click)="addDatasource()" (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, settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective, latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
); );
@ -404,6 +406,9 @@ export class WidgetComponentService {
if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) { if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) {
directives.push(widgetInfo.latestDataKeySettingsDirective); directives.push(widgetInfo.latestDataKeySettingsDirective);
} }
if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) {
directives.push(widgetInfo.basicModeDirective);
}
if (directives.length) { if (directives.length) {
factories.filter((factory) => directives.includes(factory.selector)) factories.filter((factory) => directives.includes(factory.selector))
.forEach((foundFactory) => { .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 tb-absolute-fill">
<div class="tb-widget-config-header"> <div class="tb-widget-config-header" *ngIf="!hideHeader">
<tb-toggle-header (valueChange)="selectedOption = $event" ignoreMdLgSize="true" <tb-toggle-header *ngIf="!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced" (valueChange)="selectedOption = $event" ignoreMdLgSize="true"
appearance="fill" [options]="headerOptions" [value]="selectedOption" name="widgetConfigHeader" useSelectOnMdLg="false"> appearance="fill" [options]="headerOptions" [value]="selectedOption" name="widgetConfigHeader" useSelectOnMdLg="false">
</tb-toggle-header> </tb-toggle-header>
<ng-content select=".tb-widget-config-header-prefix"></ng-content>
<ng-content select=".tb-widget-config-header-suffix"></ng-content> <ng-content select=".tb-widget-config-header-suffix"></ng-content>
</div> </div>
<div class="tb-widget-config-content"> <div *ngIf="widgetConfigMode === widgetConfigModes.advanced; else basicMode" class="tb-widget-config-content">
<div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" [formGroup]="dataSettings" class="mat-content"> <div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" class="mat-content">
<div *ngIf="displayTimewindowConfig" class="tb-widget-config-panel"> <ng-container *ngTemplateOutlet="data"></ng-container>
<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> </div>
<div [fxShow]="selectedOption === 'appearance'" class="mat-content"> <div [fxShow]="selectedOption === 'appearance'" class="mat-content">
<div *ngIf="displayAppearanceDataSettings" class="tb-widget-config-panel" [formGroup]="widgetSettings"> <ng-container *ngTemplateOutlet="appearance"></ng-container>
<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>
</div> </div>
<div [fxShow]="selectedOption === 'card'" [formGroup]="widgetSettings" class="mat-content"> <div [fxShow]="selectedOption === 'card'" [formGroup]="widgetSettings" class="mat-content">
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
@ -231,11 +143,13 @@
</div> </div>
</div> </div>
<div [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" style="height: 100%;"> <div [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" style="height: 100%;">
<tb-manage-widget-actions <div class="tb-widget-config-panel" style="height: 100%;">
[callbacks]="widgetConfigCallbacks" <tb-manage-widget-actions
[widgetType] = "modelValue.widgetType" [callbacks]="widgetConfigCallbacks"
formControlName="actionsData"> [widgetType] = "modelValue.widgetType"
</tb-manage-widget-actions> formControlName="actionsData">
</tb-manage-widget-actions>
</div>
</div> </div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content"> <div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
@ -264,5 +178,130 @@
</div> </div>
</div> </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> </div>

87
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -26,6 +26,8 @@ import {
JsonSchema, JsonSchema,
JsonSettingsSchema, JsonSettingsSchema,
Widget, Widget,
WidgetActionDescriptor,
WidgetConfigMode,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { 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 { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.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 { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { ToggleHeaderOption } from '@shared/components/toggle-header.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 = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -94,6 +101,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
widgetTypes = widgetType; widgetTypes = widgetType;
widgetConfigModes = WidgetConfigMode;
entityTypes = EntityType; entityTypes = EntityType;
@Input() @Input()
@ -111,14 +120,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@Input() @Input()
functionsOnly: boolean; functionsOnly: boolean;
@Input()
@coerceBoolean()
hideHeader = false;
@Input()
@coerceBoolean()
hideToggleHeader = false;
@Input() disabled: boolean; @Input() disabled: boolean;
@Input()
widgetConfigMode = WidgetConfigMode.advanced;
widgetType: widgetType; widgetType: widgetType;
widgetConfigCallbacks: WidgetConfigCallbacks = { widgetConfigCallbacks: WidgetConfigCallbacks = {
createEntityAlias: this.createEntityAlias.bind(this), createEntityAlias: this.createEntityAlias.bind(this),
createFilter: this.createFilter.bind(this), createFilter: this.createFilter.bind(this),
generateDataKey: this.generateDataKey.bind(this), generateDataKey: this.generateDataKey.bind(this),
fetchEntityKeysForDevice: this.fetchEntityKeysForDevice.bind(this),
fetchEntityKeys: this.fetchEntityKeys.bind(this), fetchEntityKeys: this.fetchEntityKeys.bind(this),
fetchDashboardStates: this.fetchDashboardStates.bind(this) fetchDashboardStates: this.fetchDashboardStates.bind(this)
}; };
@ -151,7 +172,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
private entityService: EntityService, private entityService: EntityService,
private dialog: MatDialog, private dialog: MatDialog,
public translate: TranslateService, public translate: TranslateService,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store); super(store);
} }
@ -288,7 +310,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'mobile' 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() { private buildForms() {
@ -607,6 +631,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget; 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 { public onlyHistoryTimewindow(): boolean {
if (this.widgetType === widgetType.latest) { if (this.widgetType === widgetType.latest) {
const datasources = this.dataSettings.get('datasources').value; 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 { public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey {
if (isObject(chip)) { if (isObject(chip)) {
(chip as DataKey)._hash = Math.random(); (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>> { private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe( return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(

1
ui-ngx/src/app/modules/home/components/widget/widget-config.scss

@ -22,6 +22,7 @@
flex-direction: column; flex-direction: column;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.15px; letter-spacing: 0.15px;
position: relative;
&.no-padding-bottom { &.no-padding-bottom {
padding-bottom: 0; padding-bottom: 0;
} }

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

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

12
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss

@ -23,20 +23,10 @@
left: 0; left: 0;
right: 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 { .tb-preview-panel {
position: absolute; position: absolute;
top: 16px; 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. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget, WidgetConfig } from '@shared/models/widget.models'; import { Widget, WidgetConfig } from '@shared/models/widget.models';
@ -28,7 +27,7 @@ import { deepClone } from '@core/utils';
templateUrl: './widget-preview.component.html', templateUrl: './widget-preview.component.html',
styleUrls: ['./widget-preview.component.scss'] styleUrls: ['./widget-preview.component.scss']
}) })
export class WidgetPreviewComponent extends PageComponent implements OnInit { export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges {
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@ -49,6 +48,25 @@ export class WidgetPreviewComponent extends PageComponent implements OnInit {
} }
ngOnInit(): void { 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 sizeX = this.widget.sizeX * 2;
const sizeY = this.widget.sizeY * 2; const sizeY = this.widget.sizeY * 2;
const col = Math.floor(Math.max(0, (20 - sizeX) / 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 { export interface WidgetConfigComponentData {
widgetName: string;
config: WidgetConfig; config: WidgetConfig;
layout: WidgetLayout; layout: WidgetLayout;
widgetType: widgetType; widgetType: widgetType;
@ -551,6 +552,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
settingsDirective: widgetTypeEntity.descriptor.settingsDirective, settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode,
basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig defaultConfig: widgetTypeEntity.descriptor.defaultConfig
}; };
} }
@ -581,6 +584,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
hasBasicMode: widgetInfo.hasBasicMode,
basicModeDirective: widgetInfo.basicModeDirective,
defaultConfig: widgetInfo.defaultConfig defaultConfig: widgetInfo.defaultConfig
}; };
return { return {

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

@ -270,6 +270,17 @@
[(ngModel)]="widget.latestDataKeySettingsDirective" [(ngModel)]="widget.latestDataKeySettingsDirective"
(ngModelChange)="isDirty = true"/> (ngModelChange)="isDirty = true"/>
</mat-form-field> </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>
</div> </div>
</mat-tab> </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 { EntityService } from '@core/http/entity.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { isEqual } from '@core/utils'; import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
@ -291,7 +291,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
async writeValue(value: string | EntityId | null): Promise<void> { async writeValue(value: string | EntityId | null): Promise<void> {
this.searchText = ''; 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 targetEntityType: EntityType;
let id: string; let id: string;
if (typeof value === '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" <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)"> (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> </mat-button-toggle-group>
<ng-template #select> <ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic"> <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} { @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 { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { BreadCrumb } from '@shared/components/breadcrumb';
export interface ToggleHeaderOption { export interface ToggleHeaderOption {
name: string; name: string;
value: any; value: any;
} }
export type ToggleHeaderAppearance = 'fill' | 'stroked'; export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
@Component({ @Component({
selector: 'tb-toggle-header', 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 singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1);
export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({
type: AliasFilterType.singleEntity,
singleEntity: {
entityType: EntityType.DEVICE,
id: deviceId
}
});
export interface EntityCountQuery { export interface EntityCountQuery {
entityFilter: EntityFilter; entityFilter: EntityFilter;
keyFilters?: Array<KeyFilter>; keyFilters?: Array<KeyFilter>;

11
ui-ngx/src/app/shared/models/widget.models.ts

@ -162,6 +162,8 @@ export interface WidgetTypeDescriptor {
settingsDirective?: string; settingsDirective?: string;
dataKeySettingsDirective?: string; dataKeySettingsDirective?: string;
latestDataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string;
hasBasicMode?: boolean;
basicModeDirective?: string;
defaultConfig: string; defaultConfig: string;
sizeX: number; sizeX: number;
sizeY: number; sizeY: number;
@ -318,6 +320,7 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType { export enum DatasourceType {
function = 'function', function = 'function',
device = 'device',
entity = 'entity', entity = 'entity',
entityCount = 'entityCount', entityCount = 'entityCount',
alarmCount = 'alarmCount' alarmCount = 'alarmCount'
@ -326,6 +329,7 @@ export enum DatasourceType {
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>( export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[ [
[ DatasourceType.function, 'function.function' ], [ DatasourceType.function, 'function.function' ],
[ DatasourceType.device, 'device.device' ],
[ DatasourceType.entity, 'entity.entity' ], [ DatasourceType.entity, 'entity.entity' ],
[ DatasourceType.entityCount, 'entity.entities-count' ], [ DatasourceType.entityCount, 'entity.entities-count' ],
[ DatasourceType.alarmCount, 'entity.alarms-count' ] [ DatasourceType.alarmCount, 'entity.alarms-count' ]
@ -341,6 +345,7 @@ export interface Datasource {
entityType?: EntityType; entityType?: EntityType;
entityId?: string; entityId?: string;
entityName?: string; entityName?: string;
deviceId?: string;
entityAliasId?: string; entityAliasId?: string;
filterId?: string; filterId?: string;
unresolvedStateEntity?: boolean; unresolvedStateEntity?: boolean;
@ -600,7 +605,13 @@ export interface WidgetSettings {
[key: string]: any; [key: string]: any;
} }
export enum WidgetConfigMode {
basic = 'basic',
advanced = 'advanced'
}
export interface WidgetConfig { export interface WidgetConfig {
configMode?: WidgetConfigMode;
title?: string; title?: string;
titleIcon?: string; titleIcon?: string;
showTitle?: boolean; showTitle?: boolean;

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3997,6 +3997,10 @@
"settings-form-selector": "Settings form selector", "settings-form-selector": "Settings form selector",
"data-key-settings-form-selector": "Data key 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", "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", "javascript": "Javascript",
"js": "JS", "js": "JS",
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",

Loading…
Cancel
Save