Browse Source

UI: Widget config improvements

pull/8708/head
Igor Kulikov 3 years ago
parent
commit
600d6cf27c
  1. 22
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 30
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  3. 8
      ui-ngx/src/app/modules/common/modules-map.ts
  4. 37
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  5. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  6. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  7. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  8. 32
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  9. 35
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  10. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  11. 32
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html
  12. 151
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts
  13. 17
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  14. 4
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  15. 3
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
  16. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html
  17. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss
  18. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts
  19. 21
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  20. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts
  21. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html
  22. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts
  23. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  24. 10
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  25. 337
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  26. 23
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  27. 79
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  28. 32
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  29. 49
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  30. 64
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  31. 16
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  32. 87
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  33. 1
      ui-ngx/src/app/shared/components/public-api.ts
  34. 3
      ui-ngx/src/app/shared/components/toggle-header.component.html
  35. 14
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  36. 9
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  37. 36
      ui-ngx/src/app/shared/models/widget.models.ts
  38. 7
      ui-ngx/src/app/shared/shared.module.ts
  39. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

22
application/src/main/data/json/system/widget_bundles/charts.json

File diff suppressed because one or more lines are too long

30
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -29,8 +29,15 @@ import {
GridSettings,
WidgetLayout
} from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils';
import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models';
import { isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils';
import {
Datasource,
DatasourceType,
defaultLegendConfig,
Widget,
WidgetConfig,
widgetType
} from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id';
@ -204,9 +211,26 @@ export class DashboardUtilsService {
public validateAndUpdateWidget(widget: Widget): Widget {
widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
// Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_timeseries';
}
if (widget.isSystemType && widget.bundleAlias === 'charts' &&
['state_chart', 'basic_timeseries', 'timeseries_bars_flot'].includes(widget.typeAlias)) {
const widgetConfig = widget.config;
const widgetSettings = widget.config.settings;
if (isDefinedAndNotNull(widgetConfig.showLegend)) {
widgetSettings.showLegend = widgetConfig.showLegend;
delete widgetConfig.showLegend;
} else if (isUndefined(widgetSettings.showLegend)) {
widgetSettings.showLegend = true;
}
if (isDefinedAndNotNull(widgetConfig.legendConfig)) {
widgetSettings.legendConfig = widgetConfig.legendConfig;
delete widgetConfig.legendConfig;
} else if (isUndefined(widgetSettings.legendConfig)) {
widgetSettings.legendConfig = defaultLegendConfig(widget.type);
}
}
return widget;
}

8
ui-ngx/src/app/modules/common/modules-map.ts

@ -196,7 +196,7 @@ import * as AddAttributeDialogComponent from '@home/components/attribute/add-att
import * as EditAttributeValuePanelComponent from '@home/components/attribute/edit-attribute-value-panel.component';
import * as DashboardComponent from '@home/components/dashboard/dashboard.component';
import * as WidgetComponent from '@home/components/widget/widget.component';
import * as LegendComponent from '@home/components/widget/legend.component';
import * as LegendComponent from '@home/components/widget/lib/legend.component';
import * as AliasesEntitySelectPanelComponent from '@home/components/alias/aliases-entity-select-panel.component';
import * as AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.component';
import * as WidgetConfigComponent from '@home/components/widget/widget-config.component';
@ -209,7 +209,7 @@ import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias
import * as DataKeysComponent from '@home/components/widget/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/data-key-config.component';
import * as LegendConfigComponent from '@home/components/widget/legend-config.component';
import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component';
import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
@ -491,7 +491,7 @@ class ModulesMap implements IModulesMap {
'@home/components/attribute/edit-attribute-value-panel.component': EditAttributeValuePanelComponent,
'@home/components/dashboard/dashboard.component': DashboardComponent,
'@home/components/widget/widget.component': WidgetComponent,
'@home/components/widget/legend.component': LegendComponent,
'@home/components/widget/lib/legend.component': LegendComponent,
'@home/components/alias/aliases-entity-select-panel.component': AliasesEntitySelectPanelComponent,
'@home/components/alias/aliases-entity-select.component': AliasesEntitySelectComponent,
'@home/components/widget/widget-config.component': WidgetConfigComponent,
@ -504,7 +504,7 @@ class ModulesMap implements IModulesMap {
'@home/components/widget/data-keys.component': DataKeysComponent,
'@home/components/widget/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/data-key-config.component': DataKeyConfigComponent,
'@home/components/widget/legend-config.component': LegendConfigComponent,
'@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 1200px;">
<form [formGroup]="widgetFormGroup" style="width: 1200px;">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
@ -37,9 +37,15 @@
[widget]="widget"
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
</tb-widget-preview>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -47,10 +53,27 @@
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
<div fxLayout="row" fxLayoutGap="8px">
<button *ngIf="!previewMode"
mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
(click)="previewMode = true"
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">
{{ 'action.add' | translate }}
</button>
</div>
</div>
</form>

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

@ -24,13 +24,14 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController } 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 { isDefined, isString } from '@core/utils';
export interface AddWidgetDialogData {
dashboard: Dashboard;
aliasController: IAliasController;
stateController: IStateController;
widget: Widget;
widgetInfo: WidgetInfo;
}
@ -48,8 +49,11 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
dashboard: Dashboard;
aliasController: IAliasController;
stateController: IStateController;
widget: Widget;
previewMode = false;
submitted = false;
constructor(protected store: Store<AppState>,
@ -62,6 +66,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.dashboard = this.data.dashboard;
this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController;
this.widget = this.data.widget;
const widgetInfo = this.data.widgetInfo;

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

@ -357,21 +357,20 @@
<tb-details-panel *ngIf="!isEditingWidgetClosed" fxFlex
headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditWidgetClosed()"
(toggleDetailsEditMode)="onRevertWidgetEdit()"
(applyDetails)="saveWidget()"
[theForm]="tbEditWidget.widgetFormGroup">
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons">
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<tb-edit-widget #tbEditWidget
[dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout">
[widgetLayout]="editingWidgetLayout"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</tb-edit-widget>
</tb-details-panel>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex

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

@ -1176,6 +1176,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: {
dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController,
stateController: this.dashboardCtx.stateController,
widget: newWidget,
widgetInfo: widgetTypeInfo
}

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

@ -23,6 +23,38 @@
[dashboard]="dashboard"
[widget]="widget"
formControlName="widgetConfig">
<div class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<button [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || !widgetFormGroup.dirty"
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || widgetFormGroup.invalid || !widgetFormGroup.dirty"
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon>
{{ 'action.apply' | translate }}
</button>
</div>
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[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>

35
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -14,13 +14,22 @@
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ChangeDetectionStrategy,
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 } from '@core/api/widget-api.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models';
@ -40,6 +49,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
aliasController: IAliasController;
@Input()
stateController: IStateController;
@Input()
widgetEditMode: boolean;
@ -49,10 +61,18 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
widgetLayout: WidgetLayout;
@Output()
applyWidgetConfig = new EventEmitter<void>();
@Output()
revertWidgetConfig = new EventEmitter<void>();
widgetFormGroup: UntypedFormGroup;
widgetConfig: WidgetConfigComponentData;
previewMode = false;
constructor(protected store: Store<AppState>,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
@ -78,10 +98,21 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
}
}
if (reloadConfig) {
this.previewMode = false;
this.loadWidgetConfig();
}
}
onApplyWidgetConfig() {
if (this.widgetFormGroup.valid) {
this.applyWidgetConfig.emit();
}
}
onRevertWidgetConfig() {
this.revertWidgetConfig.emit();
}
private loadWidgetConfig() {
if (!this.widget) {
return;

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -36,7 +36,6 @@ import { EditAttributeValuePanelComponent } from '@home/components/attribute/edi
import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { LegendComponent } from '@home/components/widget/legend.component';
import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@ -49,7 +48,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-
import { DataKeysComponent } from '@home/components/widget/data-keys.component';
import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { LegendConfigComponent } from '@home/components/widget/legend-config.component';
import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
@ -182,6 +180,7 @@ import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component';
@NgModule({
declarations:
@ -220,16 +219,15 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent,
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent,
EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,
@ -371,16 +369,15 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent,
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent,
EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,

32
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html

@ -0,0 +1,32 @@
<!--
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.
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex #flotElement></div>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div>

151
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts

@ -0,0 +1,151 @@
///
/// 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 { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { ChartType, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
widgetType
} from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-flot-widget',
templateUrl: './flot-widget.component.html',
styleUrls: []
})
export class FlotWidgetComponent implements OnInit {
@ViewChild('flotElement', {static: true}) flotElement: ElementRef;
@Input()
ctx: WidgetContext;
@Input()
chartType: ChartType;
displayLegend: boolean;
legendConfig: LegendConfig;
legendData: LegendData;
isLegendFirst: boolean;
legendContainerLayoutType: string;
legendStyle: {[klass: string]: any};
public settings: TbFlotSettings;
private flot: TbFlot;
constructor() {
}
ngOnInit(): void {
this.ctx.$scope.flotWidget = this;
this.settings = this.ctx.settings;
this.chartType = this.chartType || 'line';
this.configureLegend();
this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement));
}
private configureLegend(): void {
this.displayLegend = isDefinedAndNotNull(this.settings.showLegend) ? this.settings.showLegend
: false;
this.legendContainerLayoutType = 'column';
if (this.displayLegend) {
this.legendConfig = this.settings.legendConfig || defaultLegendConfig(widgetType.timeseries);
if (this.ctx.defaultSubscription) {
this.legendData = this.ctx.defaultSubscription.legendData;
} else {
this.legendData = {
keys: [],
data: []
};
}
if (this.legendConfig.position === LegendPosition.top ||
this.legendConfig.position === LegendPosition.bottom) {
this.legendContainerLayoutType = 'column';
this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
} else {
this.legendContainerLayoutType = 'row';
this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
}
switch (this.legendConfig.position) {
case LegendPosition.top:
this.legendStyle = {
paddingBottom: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.bottom:
this.legendStyle = {
paddingTop: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.left:
this.legendStyle = {
paddingRight: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.right:
this.legendStyle = {
paddingLeft: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
}
}
}
public onLegendKeyHiddenChange(index: number) {
for (const id of Object.keys(this.ctx.subscriptions)) {
const subscription = this.ctx.subscriptions[id];
subscription.updateDataVisibility(index);
}
}
public onDataUpdated() {
this.flot.update();
}
public onLatestDataUpdated() {
this.flot.latestDataUpdate();
}
public onResize() {
this.flot.resize();
}
public onEditModeChanged() {
this.flot.checkMouseEvents();
}
public onDestroy() {
this.flot.destroy();
}
}

17
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -17,13 +17,21 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
import { DataKey, Datasource, DatasourceData, FormattedData, JsonSettingsSchema } from '@shared/models/widget.models';
import {
DataKey,
Datasource,
DatasourceData,
FormattedData,
JsonSettingsSchema,
LegendConfig
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { ComparisonDuration } from '@shared/models/time/time.models';
export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph';
export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings;
export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotLegendSettings &
TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings;
export declare type TooltipValueFormatFunction = (value: any, latestData: FormattedData) => string;
@ -140,6 +148,11 @@ export interface TbFlotBaseSettings {
yaxis: TbFlotYAxisSettings;
}
export interface TbFlotLegendSettings {
showLegend?: boolean;
legendConfig?: LegendConfig;
}
export interface TbFlotComparisonSettings {
comparisonEnabled: boolean;
timeForComparison: ComparisonDuration;

4
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -127,7 +127,7 @@ export class TbFlot {
private pieAnimationLastTime: number;
private pieAnimationCaf: CancelAnimationFrame;
constructor(private ctx: WidgetContext, private readonly chartType: ChartType) {
constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>) {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
this.utils = this.ctx.$injector.get(UtilsService);
@ -334,7 +334,7 @@ export class TbFlot {
}
if (this.ctx.defaultSubscription) {
this.init(this.ctx.$container, this.ctx.defaultSubscription);
this.init(this.$flotElement || this.ctx.$container, this.ctx.defaultSubscription);
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts

@ -28,7 +28,6 @@ import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-
import {
GettingStartedCompletedDialogComponent
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component';
import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component';
import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component';
import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component';
@ -49,7 +48,6 @@ import {
EditLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
QuickLinkComponent,
@ -70,7 +68,6 @@ import {
EditLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
QuickLinkComponent,

0
ui-ngx/src/app/modules/home/components/widget/legend.component.html → ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html

0
ui-ngx/src/app/modules/home/components/widget/legend.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss

0
ui-ngx/src/app/modules/home/components/widget/legend.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts

21
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html

@ -68,6 +68,25 @@
<input matInput type="number" min="0" formControlName="thresholdsLineWidth">
</mat-form-field>
</fieldset>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.legend</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row" style="min-height: 48px;">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">
{{ 'widget-config.display-legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxFlex="40" style="white-space: nowrap;" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showTooltip').value">
@ -290,7 +309,7 @@
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.custom-legend-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('customLegendEnabled').value">
<mat-expansion-panel-header fxLayout="row" style="height: 100%; min-height: 48px;">
<mat-expansion-panel-header fxLayout="row" style="min-height: 48px;">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="customLegendEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts

@ -40,8 +40,9 @@ import {
labelDataKeyValidator
} from '@home/components/widget/lib/settings/chart/label-data-key.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { defaultLegendConfig, widgetType } from '@shared/models/widget.models';
export function flotDefaultSettings(chartType: ChartType): Partial<TbFlotSettings> {
export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSettings> => {
const settings: Partial<TbFlotSettings> = {
stack: false,
fontColor: '#545454',
@ -95,9 +96,11 @@ export function flotDefaultSettings(chartType: ChartType): Partial<TbFlotSetting
};
settings.customLegendEnabled = false;
settings.dataKeysListForLabels = [];
settings.showLegend = true;
settings.legendConfig = defaultLegendConfig(widgetType.timeseries);
}
return settings;
}
};
@Component({
selector: 'tb-flot-widget-settings',
@ -221,6 +224,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
showLabels: [true, []]
}));
// Legend settings
this.flotSettingsFormGroup.addControl('showLegend', this.fb.control(false, []));
this.flotSettingsFormGroup.addControl('legendConfig', this.fb.control(null, []));
// Custom legend settings
this.flotSettingsFormGroup.addControl('customLegendEnabled', this.fb.control(false, []));
@ -240,6 +248,9 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
});
if (this.chartType === 'graph' || this.chartType === 'bar') {
this.flotSettingsFormGroup.get('showLegend').valueChanges.subscribe(() => {
this.updateValidators(true);
});
this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => {
this.updateValidators(true);
});
@ -350,9 +361,15 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false});
if (this.chartType === 'graph' || this.chartType === 'bar') {
const showLegend: boolean = this.flotSettingsFormGroup.get('showLegend').value;
const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value;
const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').value;
const customLegendEnabled: boolean = this.flotSettingsFormGroup.get('customLegendEnabled').value;
if (showLegend) {
this.flotSettingsFormGroup.get('legendConfig').enable({emitEvent});
} else {
this.flotSettingsFormGroup.get('legendConfig').disable({emitEvent});
}
if (comparisonEnabled) {
this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false});
if (timeForComparison === 'customInterval') {
@ -370,6 +387,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent});
}
this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false});

0
ui-ngx/src/app/modules/home/components/widget/legend-config.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html

0
ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -265,6 +265,7 @@ import {
import {
QuickLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component';
import { LegendConfigComponent } from '@home/components/widget/lib/settings/common/legend-config.component';
@NgModule({
declarations: [
@ -290,6 +291,7 @@ import {
AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent,
TickValueComponent,
FlotWidgetSettingsComponent,
@ -395,6 +397,7 @@ import {
AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent,
TickValueComponent,
FlotWidgetSettingsComponent,

10
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -41,6 +41,8 @@ import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-wi
import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module';
import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
@NgModule({
declarations:
@ -62,7 +64,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent
],
imports: [
CommonModule,
@ -88,7 +92,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

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

@ -15,9 +15,15 @@
limitations under the License.
-->
<mat-tab-group mat-stretch-tabs="false" class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div class="tb-widget-config tb-absolute-fill">
<div class="tb-widget-config-header">
<tb-toggle-header (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-suffix"></ng-content>
</div>
<div class="tb-widget-config-content">
<div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" [formGroup]="dataSettings" class="mat-content" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxFlex.gt-xs>
@ -55,7 +61,7 @@
<div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
</div>
<mat-icon *ngIf = '(datasourceError.length > 0 || !datasourcesFormArray().valid) && !openExtensionPanel'
<mat-icon *ngIf = '(datasourceError.length > 0 || !datasourcesFormArray()?.valid) && !openExtensionPanel'
style = "color: red; font-size: 18px; margin-left: 5px;">
report_gmailerrorred
</mat-icon>
@ -64,7 +70,7 @@
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</mat-panel-description>
</mat-expansion-panel-header>
<div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate">
<div *ngIf="datasourcesFormArray()?.length === 0; else datasourcesTemplate">
<span translate fxLayoutAlign="center center"
class="tb-prompt">datasource.add-datasource-prompt</span>
</div>
@ -86,7 +92,7 @@
(cdkDropListDropped)="onDatasourceDrop($event)"
[cdkDropListDisabled]="disabled"
formArrayName="datasources">
<mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
<mat-list-item *ngFor="let datasourceControl of datasourcesFormArray()?.controls; let $index = index;"
class="tb-datasource-list-item tb-draggable" cdkDrag
[cdkDragDisabled]="disabled">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
@ -205,7 +211,7 @@
type="button"
mat-raised-button color="primary"
[fxShow]="modelValue?.typeParameters &&
(modelValue?.typeParameters.maxDatasources === -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)"
(modelValue?.typeParameters.maxDatasources === -1 || datasourcesFormArray()?.controls?.length < modelValue?.typeParameters.maxDatasources)"
(click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above">
@ -223,11 +229,11 @@
</mat-expansion-panel-header>
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
<tb-entity-alias-select fxFlex
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
</mat-expansion-panel>
@ -312,183 +318,148 @@
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}">
<div class="mat-content mat-padding" fxLayout="column">
<div [formGroup]="widgetSettings" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.title</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.display-title' | translate }}
<div *ngIf="displayAdvanced()" [fxShow]="selectedOption === 'appearance'" [formGroup]="advancedSettings" class="mat-content tb-advanced-widget-config"
fxLayout="column">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
<div [fxShow]="selectedOption === 'card'" [formGroup]="widgetSettings" class="mat-content" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.title</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.display-title' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.title-tooltip</mat-label>
<input matInput formControlName="titleTooltip">
</mat-form-field>
</div>
<fieldset class="fields-group" fxLayoutGap.gt-xs="8px" style="margin: 0">
<legend class="group-title" translate>widget-config.title-icon</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<tb-material-icon-select fxFlex
formControlName="titleIcon">
</tb-material-icon-select>
<tb-color-input fxFlex
label="{{'widget-config.icon-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="iconColor">
</tb-color-input>
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.title-tooltip</mat-label>
<input matInput formControlName="titleTooltip">
<mat-label translate>widget-config.icon-size</mat-label>
<input matInput formControlName="iconSize">
</mat-form-field>
</div>
<fieldset class="fields-group" fxLayoutGap.gt-xs="8px" style="margin: 0">
<legend class="group-title" translate>widget-config.title-icon</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<tb-material-icon-select fxFlex
formControlName="titleIcon">
</tb-material-icon-select>
<tb-color-input fxFlex
label="{{'widget-config.icon-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="iconColor">
</tb-color-input>
<mat-form-field fxFlex>
<mat-label translate>widget-config.icon-size</mat-label>
<input matInput formControlName="iconSize">
</mat-form-field>
</div>
</fieldset>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.widget-style</legend>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap.gt-xs="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<tb-color-input fxFlex
label="{{'widget-config.background-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="backgroundColor">
</tb-color-input>
<tb-color-input fxFlex
label="{{'widget-config.text-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="color">
</tb-color-input>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.padding</mat-label>
<input matInput formControlName="padding">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.margin</mat-label>
<input matInput formControlName="margin">
</mat-form-field>
</div>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.widget-style</legend>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap.gt-xs="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<tb-color-input fxFlex
label="{{'widget-config.background-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="backgroundColor">
</tb-color-input>
<tb-color-input fxFlex
label="{{'widget-config.text-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="color">
</tb-color-input>
</div>
<mat-slide-toggle class="slide-block" formControlName="dropShadow">
{{ 'widget-config.drop-shadow' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="slide-block" formControlName="enableFullscreen">
{{ 'widget-config.enable-fullscreen' | translate }}
</mat-slide-toggle>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.widget-style' | translate }}"
formControlName="widgetStyle"
></tb-json-object-edit>
<tb-css
label="{{ 'widget-config.widget-css' | translate }}"
formControlName="widgetCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider" *ngIf="showLegendFieldset">
<legend class="group-title" translate>widget-config.legend</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.display-legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header style="height: fit-content;">
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex="70">
<mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="desktopHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate fxFlex="30">
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" step="1">
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.padding</mat-label>
<input matInput formControlName="padding">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.margin</mat-label>
<input matInput formControlName="margin">
</mat-form-field>
</div>
</div>
<mat-slide-toggle class="slide-block" formControlName="dropShadow">
{{ 'widget-config.drop-shadow' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="slide-block" formControlName="enableFullscreen">
{{ 'widget-config.enable-fullscreen' | translate }}
</mat-slide-toggle>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.widget-style' | translate }}"
formControlName="widgetStyle"
></tb-json-object-edit>
<tb-css
label="{{ 'widget-config.widget-css' | translate }}"
formControlName="widgetCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div>
</mat-tab>
<mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
<div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config"
fxLayout="column">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
<div [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" fxLayout="column" style="height: 100%;">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
</div>
</mat-tab>
<mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
</mat-tab>
</mat-tab-group>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content" fxLayout="column" fxLayoutGap="16px">
<mat-slide-toggle formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" step="1">
</mat-form-field>
</div>
</div>
</div>
</div>

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

@ -16,6 +16,24 @@
@import '../../../../../scss/constants';
.tb-widget-config {
display: flex;
flex-direction: column;
gap: 16px;
.tb-widget-config-header {
padding: 24px 24px 8px;
height: 56px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.tb-widget-config-content {
flex: 1;
overflow: auto;
& > div {
padding: 16px;
}
}
.tb-panel-hint {
font-size: 12px;
color: #808080;
@ -120,6 +138,11 @@
:host ::ng-deep {
.tb-widget-config {
.tb-widget-config-header {
.mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 0 20px;
}
}
tb-alarm-filter-config {
.mdc-button {
width: 100%;

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { 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';
@ -25,7 +25,6 @@ import {
datasourcesHasOnlyComparisonAggregation,
DatasourceType,
datasourceTypeTranslationMap,
defaultLegendConfig,
GroupInfo,
JsonSchema,
JsonSettingsSchema,
@ -68,6 +67,7 @@ import { entityFields } from '@shared/models/entity.models';
import { Filter } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -95,7 +95,7 @@ const defaultSettingsForm = [
}
]
})
export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
widgetTypes = widgetType;
@ -134,14 +134,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetEditMode = this.utils.widgetEditMode;
selectedTab: number;
modelValue: WidgetConfigComponentData;
showLegendFieldset = true;
private propagateChange = null;
headerOptions: ToggleHeaderOption[] = [];
selectedOption: string;
public dataSettings: UntypedFormGroup;
public targetDeviceSettings: UntypedFormGroup;
public alarmSourceSettings: UntypedFormGroup;
@ -196,9 +195,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
pageSize: [1024, [Validators.min(1), Validators.pattern(/^\d*$/)]],
units: [null, []],
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
noDataDisplayMessage: [null, []],
showLegend: [null, []],
legendConfig: [null, []]
noDataDisplayMessage: [null, []]
});
this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => {
if (value) {
@ -224,13 +221,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.widgetSettings.get('iconSize').disable({emitEvent: false});
}
});
this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
if (value) {
this.widgetSettings.get('legendConfig').enable({emitEvent: false});
} else {
this.widgetSettings.get('legendConfig').disable({emitEvent: false});
}
});
this.layoutSettings = this.fb.group({
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
@ -370,15 +360,49 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.modelValue = value;
this.removeChangeSubscriptions();
if (this.modelValue) {
this.headerOptions.length = 0;
if (this.modelValue.widgetType !== widgetType.static) {
this.headerOptions.push(
{
name: this.translate.instant('widget-config.data'),
value: 'data'
}
);
}
if (this.displayAdvanced()) {
this.headerOptions.push(
{
name: this.translate.instant('widget-config.appearance'),
value: 'appearance'
}
);
}
this.headerOptions.push(
{
name: this.translate.instant('widget-config.widget-card'),
value: 'card'
}
);
this.headerOptions.push(
{
name: this.translate.instant('widget-config.actions'),
value: 'actions'
}
);
this.headerOptions.push(
{
name: this.translate.instant('widget-config.mobile'),
value: 'mobile'
}
);
this.selectedOption = this.headerOptions[0].value;
if (this.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType;
this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest);
this.buildForms();
}
const config = this.modelValue.config;
const layout = this.modelValue.layout;
if (config) {
this.selectedTab = 0;
const displayWidgetTitle = isDefined(config.showTitle) ? config.showTitle : false;
this.widgetSettings.patchValue({
title: config.title,
@ -403,10 +427,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
pageSize: isDefined(config.pageSize) ? config.pageSize : 1024,
units: config.units,
decimals: config.decimals,
noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '',
showLegend: isDefined(config.showLegend) ? config.showLegend :
this.widgetType === widgetType.timeseries,
legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType)
noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : ''
},
{emitEvent: false}
);
@ -430,12 +451,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.widgetSettings.get('iconColor').disable({emitEvent: false});
this.widgetSettings.get('iconSize').disable({emitEvent: false});
}
const showLegend: boolean = this.widgetSettings.get('showLegend').value;
if (showLegend) {
this.widgetSettings.get('legendConfig').enable({emitEvent: false});
} else {
this.widgetSettings.get('legendConfig').disable({emitEvent: false});
}
const actionsData: WidgetActionsData = {
actionsMap: config.actions || {},
actionSources: this.modelValue.actionSources || {}
@ -812,15 +827,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
);
}),
)),
catchError(() => of([] as Array<DataKey>))
);
}

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

@ -0,0 +1,32 @@
<!--
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.
-->
<tb-dashboard class="tb-preview-dashboard"
[aliasController]="aliasController"
[stateController]="stateController"
[widgets]="widgets"
[autofillHeight]="true"
[columns]="20"
[isEdit]="false"
[isMobileDisabled]="true"
[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>

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

@ -0,0 +1,49 @@
/**
* 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 {
z-index: 10;
background: #F3F6FA;
.tb-preview-dashboard {
position: absolute;
top: 15%;
bottom: 15%;
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;
}
}
:host ::ng-deep {
tb-dashboard.tb-preview-dashboard {
.tb-dashboard-content {
background-color: transparent !important;
}
}
}

64
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts

@ -0,0 +1,64 @@
///
/// 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 { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget, WidgetConfig } from '@shared/models/widget.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-widget-preview',
templateUrl: './widget-preview.component.html',
styleUrls: ['./widget-preview.component.scss']
})
export class WidgetPreviewComponent extends PageComponent implements OnInit {
@Input()
aliasController: IAliasController;
@Input()
stateController: IStateController;
@Input()
widget: Widget;
@Input()
widgetConfig: WidgetConfig;
widgets: Widget[];
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
const sizeX = this.widget.sizeX * 2;
const sizeY = this.widget.sizeY * 2;
const col = Math.floor(Math.max(0, (20 - sizeX) / 2));
const widget = deepClone(this.widget);
widget.sizeX = sizeX;
widget.sizeY = sizeY;
widget.row = 0;
widget.col = col;
widget.config = this.widgetConfig;
this.widgets = [widget];
}
}

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

@ -15,23 +15,11 @@
limitations under the License.
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType" tb-toast
<div class="tb-absolute-fill" fxLayout="column" tb-toast
toastTarget="{{ toastTargetId }}">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex id="widget-container">
<ng-container #widgetContent></ng-container>
</div>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>
@ -42,5 +30,5 @@
class="tb-absolute-fill">{{ noDataDisplayMessageText }}</span>
</div>
<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>
<mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
</div>

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

@ -39,10 +39,6 @@ import {
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
Widget,
WidgetActionDescriptor,
widgetActionSources,
@ -152,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
displayNoData = false;
noDataDisplayMessageText: string;
displayLegend: boolean;
legendConfig: LegendConfig;
legendData: LegendData;
isLegendFirst: boolean;
legendContainerLayoutType: string;
legendStyle: {[klass: string]: any};
dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>;
dynamicWidgetComponent: IDynamicWidgetComponent;
@ -220,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widget = this.dashboardWidget.widget;
this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
: this.widget.type === widgetType.timeseries;
this.legendContainerLayoutType = 'column';
if (this.displayLegend) {
this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type);
this.legendData = {
keys: [],
data: []
};
if (this.legendConfig.position === LegendPosition.top ||
this.legendConfig.position === LegendPosition.bottom) {
this.legendContainerLayoutType = 'column';
this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
} else {
this.legendContainerLayoutType = 'row';
this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
}
switch (this.legendConfig.position) {
case LegendPosition.top:
this.legendStyle = {
paddingBottom: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.bottom:
this.legendStyle = {
paddingTop: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.left:
this.legendStyle = {
paddingRight: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.right:
this.legendStyle = {
paddingLeft: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
}
}
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
if (this.widget.config.actions) {
for (const actionSourceId of Object.keys(this.widget.config.actions)) {
@ -463,13 +401,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
public onLegendKeyHiddenChange(index: number) {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.updateDataVisibility(index);
}
}
private loadFromWidgetInfo() {
this.widgetContext.widgetNamespace =
`widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
@ -872,9 +803,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
this.createSubscription(options, subscribe).subscribe(
(subscription) => {
if (useDefaultComponents) {
this.defaultSubscriptionOptions(subscription, options);
}
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
@ -892,8 +820,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null;
if (this.displayLegend) {
options.legendConfig = this.legendConfig;
if (this.widget.config.settings.showLegend === true) {
options.legendConfig = this.widget.config.settings.legendConfig;
}
options.decimals = this.widgetContext.decimals;
options.units = this.widgetContext.units;
@ -962,13 +890,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
});
}
};
}
private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) {
if (this.displayLegend) {
this.legendData = subscription.legendData;
}
}
private createDefaultSubscription(): Observable<any> {
@ -999,7 +920,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.createSubscription(options).subscribe(
(subscription) => {
this.defaultSubscriptionOptions(subscription, options);
// backward compatibility
this.widgetContext.datasources = subscription.datasources;
@ -1479,7 +1399,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
}
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>, actionDescriptor: WidgetActionDescriptor): Observable<any> {
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>,
actionDescriptor: WidgetActionDescriptor): Observable<any> {
const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<ModulesWithFactories | string>[] = [];

1
ui-ngx/src/app/shared/components/public-api.ts

@ -22,3 +22,4 @@ export * from './js-func.component';
export * from './script-lang.component';
export * from './slack-conversation-autocomplete.component';
export * from './notification/template-autocomplete.component';
export * from './toggle-header.component';

3
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html → ui-ngx/src/app/shared/components/toggle-header.component.html

@ -15,7 +15,8 @@
limitations under the License.
-->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" [name]="name" [(ngModel)]="value"
<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"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>

14
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss → ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -14,7 +14,7 @@
* limitations under the License.
*/
@import "../../../../../../../scss/constants";
@import "../../../scss/constants";
:host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
@ -58,9 +58,19 @@
}
}
}
&.tb-fill {
.mat-button-toggle.mat-button-toggle-appearance-standard {
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: #305680;
color: #FFFFFF;
}
}
}
}
}
@media #{$mat-md-lg} {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-md-lg) {
height: 24px;
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button {

9
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts → ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -44,6 +44,8 @@ export interface ToggleHeaderOption {
value: any;
}
export type ToggleHeaderAppearance = 'fill' | 'stroked';
@Component({
selector: 'tb-toggle-header',
templateUrl: './toggle-header.component.html',
@ -67,6 +69,13 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit {
@coerceBoolean()
useSelectOnMdLg = true;
@Input()
@coerceBoolean()
ignoreMdLgSize = false;
@Input()
appearance: ToggleHeaderAppearance = 'stroked';
isMdLg: boolean;
private observeBreakpointSubscription: Subscription;

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

@ -252,18 +252,16 @@ export interface LegendConfig {
showLatest: boolean;
}
export function defaultLegendConfig(wType: widgetType): LegendConfig {
return {
direction: LegendDirection.column,
position: LegendPosition.bottom,
sortDataKeys: false,
showMin: false,
showMax: false,
showAvg: wType === widgetType.timeseries,
showTotal: false,
showLatest: false
};
}
export const defaultLegendConfig = (wType: widgetType): LegendConfig => ({
direction: LegendDirection.column,
position: LegendPosition.bottom,
sortDataKeys: false,
showMin: false,
showMax: false,
showAvg: wType === widgetType.timeseries,
showTotal: false,
showLatest: false
});
export enum ComparisonResultType {
PREVIOUS_VALUE = 'PREVIOUS_VALUE',
@ -363,7 +361,7 @@ export interface Datasource {
[key: string]: any;
}
export function datasourcesHasAggregation(datasources?: Array<Datasource>): boolean {
export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
@ -375,9 +373,9 @@ export function datasourcesHasAggregation(datasources?: Array<Datasource>): bool
}
}
return false;
}
};
export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Datasource>): boolean {
export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array<Datasource>): boolean => {
if (!datasourcesHasAggregation(datasources)) {
return false;
}
@ -392,7 +390,7 @@ export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Data
}
}
return true;
}
};
export interface FormattedData {
$datasource: Datasource;
@ -614,8 +612,6 @@ export interface WidgetConfig {
enableFullscreen?: boolean;
useDashboardTimewindow?: boolean;
displayTimewindow?: boolean;
showLegend?: boolean;
legendConfig?: LegendConfig;
timewindow?: Timewindow;
desktopHide?: boolean;
mobileHide?: boolean;
@ -700,7 +696,7 @@ export interface IWidgetSettingsComponent {
[key: string]: any;
}
function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings {
const removeEmptyWidgetSettings = (settings: WidgetSettings): WidgetSettings => {
if (settings) {
const keys = Object.keys(settings);
for (const key of keys) {
@ -711,7 +707,7 @@ function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings {
}
}
return settings;
}
};
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix

7
ui-ngx/src/app/shared/shared.module.ts

@ -189,6 +189,7 @@ import {
} from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -356,7 +357,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent
ColorPickerComponent,
ToggleHeaderComponent
],
imports: [
CommonModule,
@ -580,7 +582,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent
ColorPickerComponent,
ToggleHeaderComponent
]
})
export class SharedModule { }

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

@ -35,6 +35,7 @@
"edit-mode": "Edit mode",
"enter-edit-mode": "Enter edit mode",
"decline-changes": "Decline changes",
"decline": "Decline",
"close": "Close",
"back": "Back",
"run": "Run",
@ -4091,6 +4092,9 @@
"data": "Data",
"settings": "Settings",
"advanced": "Advanced",
"appearance": "Appearance",
"widget-card": "Widget card",
"mobile": "Mobile",
"title": "Title",
"title-tooltip": "Title Tooltip",
"general-settings": "General settings",
@ -4151,7 +4155,8 @@
"data-settings": "Data settings",
"no-data-display-message": "\"No data to display\" alternative message",
"data-page-size": "Maximum entities per datasource",
"settings-component-not-found": "Settings form component not found for selector '{{selector}}'"
"settings-component-not-found": "Settings form component not found for selector '{{selector}}'",
"preview": "Preview"
},
"widget-type": {
"import": "Import widget type",

Loading…
Cancel
Save