UI: Implement time series comparison widget settings.

This commit is contained in:
Igor Kulikov 2024-04-12 17:34:43 +03:00
parent 95ac620977
commit 82ab52889b
31 changed files with 890 additions and 65 deletions

View File

@ -110,6 +110,10 @@ import {
import { import {
TimeSeriesChartBasicConfigComponent TimeSeriesChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component'; } from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component';
import { ComparisonKeyRowComponent } from '@home/components/widget/config/basic/chart/comparison-key-row.component';
import {
ComparisonKeysTableComponent
} from '@home/components/widget/config/basic/chart/comparison-keys-table.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -145,7 +149,9 @@ import {
PowerButtonBasicConfigComponent, PowerButtonBasicConfigComponent,
SliderBasicConfigComponent, SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent, ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -0,0 +1,37 @@
<!--
Copyright © 2016-2024 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 [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-comparison-key-row">
<mat-checkbox class="tb-show-field" formControlName="showValuesForComparison"></mat-checkbox>
<tb-data-key-input
[editable]="false"
[removable]="false"
[datasourceType]="datasourceType"
[formControl]="keyFormControl">
</tb-data-key-input>
<div class="tb-label-field">
<mat-form-field class="tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widgets.time-series-chart.comparison.comparison-values-label-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-color-field">
<tb-color-input asBoxInput
colorClearButton
formControlName="color">
</tb-color-input>
</div>
</div>

View File

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2024 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 '../../../../../../../../scss/constants';
.tb-comparison-key-row {
.tb-show-field {
width: 40px;
min-width: 40px;
}
.tb-data-key-input {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 100px;
flex: 1 1 40%;
}
}
.tb-label-field, .tb-color-field {
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
.tb-inline-field {
flex: 1;
}
}
.tb-label-field {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 150px;
flex: 1 1 60%;
}
}
.tb-color-field {
width: 40px;
min-width: 40px;
}
}

View File

@ -0,0 +1,131 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup
} from '@angular/forms';
import {
DataKey,
DataKeyComparisonSettings,
DataKeySettingsWithComparison,
DatasourceType
} from '@shared/models/widget.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-comparison-key-row',
templateUrl: './comparison-key-row.component.html',
styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ComparisonKeyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ComparisonKeyRowComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
modelValue: DataKey;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
}
ngOnInit() {
this.keyFormControl = this.fb.control(null, []);
this.keyRowFormGroup = this.fb.group({
showValuesForComparison: [null, []],
comparisonValuesLabel: [null, []],
color: [null, []]
});
this.keyRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
this.keyRowFormGroup.get('showValuesForComparison').valueChanges.subscribe(() => this.updateValidators());
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keyFormControl.disable({emitEvent: false});
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keyFormControl.enable({emitEvent: false});
this.keyRowFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: DataKey): void {
this.modelValue = value;
const comparisonSettings = (value?.settings as DataKeySettingsWithComparison)?.comparisonSettings;
this.keyRowFormGroup.patchValue(
comparisonSettings, {emitEvent: false}
);
this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
this.updateValidators();
this.cd.markForCheck();
}
private updateValidators() {
const showValuesForComparison: boolean = this.keyRowFormGroup.get('showValuesForComparison').value;
if (showValuesForComparison) {
this.keyFormControl.enable({emitEvent: false});
this.keyRowFormGroup.get('comparisonValuesLabel').enable({emitEvent: false});
this.keyRowFormGroup.get('color').enable({emitEvent: false});
} else {
this.keyFormControl.disable({emitEvent: false});
this.keyRowFormGroup.get('comparisonValuesLabel').disable({emitEvent: false});
this.keyRowFormGroup.get('color').disable({emitEvent: false});
}
}
private updateModel() {
const comparisonSettings: DataKeyComparisonSettings = this.keyRowFormGroup.value;
if (!this.modelValue.settings) {
this.modelValue.settings = {};
}
this.modelValue.settings.comparisonSettings = comparisonSettings;
this.propagateChange(this.modelValue);
}
}

View File

@ -0,0 +1,38 @@
<!--
Copyright © 2016-2024 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-comparison-keys-table tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-show-header" translate>widgets.time-series-chart.comparison.show</div>
<div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
<div class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body">
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-comparison-key-row
fxFlex
[datasourceType]="datasourceType"
[formControl]="keyControl">
</tb-comparison-key-row>
</div>
</div>
</div>
<ng-template #noKeys>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ 'widgets.chart.no-series' | translate }}</span>
</ng-template>

View File

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2024 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 '../../../../../../../../scss/constants';
.tb-comparison-keys-table {
.tb-form-table-header-cell {
&.tb-show-header {
width: 40px;
min-width: 40px;
}
&.tb-key-header {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 100px;
flex: 1 1 40%;
}
}
&.tb-label-header {
flex: 1;
@media #{$mat-gt-xs} {
min-width: 150px;
flex: 1 1 60%;
}
}
&.tb-color-header {
width: 40px;
min-width: 40px;
}
}
.tb-form-table-body {
tb-comparison-key-row {
overflow: hidden;
}
}
}

View File

@ -0,0 +1,110 @@
///
/// Copyright © 2016-2024 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, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { DataKey, DatasourceType } from '@shared/models/widget.models';
@Component({
selector: 'tb-comparison-keys-table',
templateUrl: './comparison-keys-table.component.html',
styleUrls: ['./comparison-keys-table.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ComparisonKeysTableComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ComparisonKeysTableComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
keysListFormGroup: UntypedFormGroup;
get noKeys(): boolean {
const keys: DataKey[] = this.keysListFormGroup.get('keys').value;
return keys.length === 0;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [this.fb.array([]), []]
});
this.keysListFormGroup.valueChanges.subscribe(
() => {
const keys: DataKey[] = this.keysListFormGroup.get('keys').value;
this.propagateChange(keys);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.keysListFormGroup.disable({emitEvent: false});
} else {
this.keysListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey[] | undefined): void {
this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false});
}
keysFormArray(): UntypedFormArray {
return this.keysListFormGroup.get('keys') as UntypedFormArray;
}
trackByKey(_index: number, keyControl: AbstractControl): any {
return keyControl;
}
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray {
const keysControls: Array<AbstractControl> = [];
if (keys) {
keys.forEach((key) => {
keysControls.push(this.fb.control(key, []));
});
}
return this.fb.array(keysControls);
}
}

View File

@ -25,22 +25,77 @@
forceSingleDatasource forceSingleDatasource
formControlName="datasources"> formControlName="datasources">
</tb-datasources> </tb-datasources>
<tb-data-keys-panel <div class="tb-form-panel">
panelTitle="{{ 'widgets.chart.series' | translate }}" <div fxLayout="row" fxLayoutAlign="space-between center">
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}" <div class="tb-form-panel-title">{{ 'widgets.chart.series' | translate }}</div>
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}" <tb-toggle-select [ngModel]="seriesMode" (ngModelChange)="seriesModeChange($event)"
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}" [ngModelOptions]="{ standalone: true }">
noKeysText="{{ 'widgets.chart.no-series' | translate }}" <tb-toggle-option value="series">{{ 'widgets.chart.series' | translate }}</tb-toggle-option>
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}" <tb-toggle-option value="comparison">{{ 'widgets.time-series-chart.comparison.comparison' | translate }}</tb-toggle-option>
timeSeriesChart </tb-toggle-select>
[yAxisIds]="yAxisIds" </div>
[showTimeSeriesType]="chartType === TimeSeriesChartType.default" <tb-data-keys-panel
hideSourceSelection *ngIf="seriesMode === 'series'"
[datasourceType]="datasource?.type" hidePanel
[deviceId]="datasource?.deviceId" panelTitle="{{ 'widgets.chart.series' | translate }}"
[entityAliasId]="datasource?.entityAliasId" addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
formControlName="series"> keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}"
</tb-data-keys-panel> removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
timeSeriesChart
[yAxisIds]="yAxisIds"
[showTimeSeriesType]="chartType === TimeSeriesChartType.default"
hideSourceSelection
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<div *ngIf="seriesMode === 'comparison'" class="tb-form-row no-border no-padding column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="comparisonEnabled">
{{ 'widgets.time-series-chart.comparison.comparison' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="timeSeriesChartWidgetConfigForm.get('timeForComparison').value === 'customInterval'"
appearance="outline" class="number flex-lt-md" subscriptSizing="dynamic">
<input matInput formControlName="comparisonCustomIntervalValue" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-time-series-chart-axis-settings-button
axisType="xAxis"
panelTitle="{{ 'widgets.time-series-chart.axis.comparison-x-axis-settings' | translate }}"
formControlName="comparisonXAxis">
</tb-time-series-chart-axis-settings-button>
</div>
</div>
<tb-comparison-keys-table
*ngIf="seriesMode === 'comparison'"
[datasourceType]="datasource?.type"
formControlName="series">
</tb-comparison-keys-table>
</div>
<tb-time-series-chart-states-panel <tb-time-series-chart-states-panel
*ngIf="chartType === TimeSeriesChartType.state" *ngIf="chartType === TimeSeriesChartType.state"
formControlName="states"> formControlName="states">

View File

@ -23,6 +23,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { import {
DataKey, DataKey,
Datasource, Datasource,
DatasourceType,
legendPositions, legendPositions,
legendPositionTranslationMap, legendPositionTranslationMap,
WidgetConfig, WidgetConfig,
@ -89,6 +90,8 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
chartType: TimeSeriesChartType = TimeSeriesChartType.default; chartType: TimeSeriesChartType = TimeSeriesChartType.default;
seriesMode = 'series';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent, protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector, private $injector: Injector,
@ -105,6 +108,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
} }
seriesModeChange(seriesMode: string) {
this.seriesMode = seriesMode;
this.updateSeriesState();
}
protected configForm(): UntypedFormGroup { protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm; return this.timeSeriesChartWidgetConfigForm;
} }
@ -135,6 +143,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
comparisonEnabled: [settings.comparisonEnabled, []], comparisonEnabled: [settings.comparisonEnabled, []],
timeForComparison: [settings.timeForComparison, []], timeForComparison: [settings.timeForComparison, []],
comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]], comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]],
comparisonXAxis: [settings.comparisonXAxis, []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
@ -187,6 +196,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
if (this.chartType === TimeSeriesChartType.state) { if (this.chartType === TimeSeriesChartType.state) {
this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, [])); this.timeSeriesChartWidgetConfigForm.addControl('states', this.fb.control(settings.states, []));
} }
this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').valueChanges.subscribe(() => this.updateSeriesState());
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
@ -209,6 +219,7 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled; this.widgetConfig.config.settings.comparisonEnabled = config.comparisonEnabled;
this.widgetConfig.config.settings.timeForComparison = config.timeForComparison; this.widgetConfig.config.settings.timeForComparison = config.timeForComparison;
this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue; this.widgetConfig.config.settings.comparisonCustomIntervalValue = config.comparisonCustomIntervalValue;
this.widgetConfig.config.settings.comparisonXAxis = config.comparisonXAxis;
this.widgetConfig.config.settings.thresholds = config.thresholds; this.widgetConfig.config.settings.thresholds = config.thresholds;
@ -254,16 +265,27 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate']; return ['comparisonEnabled', 'showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate'];
} }
protected updateValidators(emitEvent: boolean, trigger?: string) { protected updateValidators(emitEvent: boolean, trigger?: string) {
const comparisonEnabled: boolean = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value;
const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value; const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value; const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value;
const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value; const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value; const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetConfigForm.get('timeForComparison').enable();
this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').enable();
this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('timeForComparison').disable();
this.timeSeriesChartWidgetConfigForm.get('comparisonCustomIntervalValue').disable();
this.timeSeriesChartWidgetConfigForm.get('comparisonXAxis').disable();
}
if (showTitle) { if (showTitle) {
this.timeSeriesChartWidgetConfigForm.get('title').enable(); this.timeSeriesChartWidgetConfigForm.get('title').enable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').enable(); this.timeSeriesChartWidgetConfigForm.get('titleFont').enable();
@ -345,6 +367,19 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
} }
} }
private updateSeriesState() {
if (this.seriesMode === 'series') {
this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false});
} else {
const comparisonEnabled = this.timeSeriesChartWidgetConfigForm.get('comparisonEnabled').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetConfigForm.get('series').enable({emitEvent: false});
} else {
this.timeSeriesChartWidgetConfigForm.get('series').disable({emitEvent: false});
}
}
}
private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean { private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean {
let changed = false; let changed = false;
if (series) { if (series) {
@ -381,4 +416,6 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon
processor.update(Date.now()); processor.update(Date.now());
return processor.formatted; return processor.formatted;
} }
protected readonly DatasourceType = DatasourceType;
} }

View File

@ -15,8 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-form-panel tb-data-keys-panel"> <div class="tb-form-panel tb-data-keys-panel"
<div class="tb-form-panel-title">{{ panelTitle }}</div> [class.no-border]="hidePanel"
[class.no-padding]="hidePanel">
<div *ngIf="!hidePanel" class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-form-table"> <div class="tb-form-table">
<div class="tb-form-table-header no-padding-right"> <div class="tb-form-table-header no-padding-right">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>

View File

@ -96,6 +96,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@Input() @Input()
deviceId: string; deviceId: string;
@Input()
@coerceBoolean()
hidePanel = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
hideDataKeyColor = false; hideDataKeyColor = false;

View File

@ -49,6 +49,7 @@ import { TbInject } from '@shared/decorators/tb-inject';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { UtilsService } from '@core/services/utils.service';
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix
@ -86,6 +87,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
this.ctx.customDialog = $injector.get(CustomDialogService); this.ctx.customDialog = $injector.get(CustomDialogService);
this.ctx.resourceService = $injector.get(ResourceService); this.ctx.resourceService = $injector.get(ResourceService);
this.ctx.userSettingsService = $injector.get(UserSettingsService); this.ctx.userSettingsService = $injector.get(UserSettingsService);
this.ctx.utilsService = $injector.get(UtilsService);
this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService); this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
this.ctx.date = $injector.get(DatePipe); this.ctx.date = $injector.get(DatePipe);
this.ctx.imagePipe = $injector.get(ImagePipe); this.ctx.imagePipe = $injector.get(ImagePipe);

View File

@ -71,6 +71,7 @@ import { DatePipe } from '@angular/common';
import { BuiltinTextPosition } from 'zrender/src/core/types'; import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel'; import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import { WidgetTimewindow } from '@shared/models/time/time.models'; import { WidgetTimewindow } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
export enum TimeSeriesChartType { export enum TimeSeriesChartType {
default = 'default', default = 'default',
@ -932,6 +933,7 @@ export interface TimeSeriesChartXAxis extends TimeSeriesChartAxis {
export const createTimeSeriesYAxis = (units: string, export const createTimeSeriesYAxis = (units: string,
decimals: number, decimals: number,
settings: TimeSeriesChartYAxisSettings, settings: TimeSeriesChartYAxisSettings,
utils: UtilsService,
darkMode: boolean): TimeSeriesChartYAxis => { darkMode: boolean): TimeSeriesChartYAxis => {
const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel'); settings.tickLabelColor, darkMode, 'axis.tickLabel');
@ -986,7 +988,7 @@ export const createTimeSeriesYAxis = (units: string,
splitNumber, splitNumber,
interval, interval,
ticksGenerator, ticksGenerator,
name: settings.label, name: utils.customTranslation(settings.label, settings.label),
nameLocation: 'middle', nameLocation: 'middle',
nameRotate: settings.position === AxisPosition.left ? 90 : -90, nameRotate: settings.position === AxisPosition.left ? 90 : -90,
nameTextStyle: { nameTextStyle: {
@ -1044,6 +1046,7 @@ export const createTimeSeriesXAxis = (id: string,
settings: TimeSeriesChartXAxisSettings, settings: TimeSeriesChartXAxisSettings,
min: number, max: number, min: number, max: number,
datePipe: DatePipe, datePipe: DatePipe,
utils: UtilsService,
darkMode: boolean): TimeSeriesChartXAxis => { darkMode: boolean): TimeSeriesChartXAxis => {
const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont,
settings.tickLabelColor, darkMode, 'axis.tickLabel'); settings.tickLabelColor, darkMode, 'axis.tickLabel');
@ -1060,7 +1063,7 @@ export const createTimeSeriesXAxis = (id: string,
scale: true, scale: true,
position: settings.position, position: settings.position,
id, id,
name: settings.label, name: utils.customTranslation(settings.label, settings.label),
nameLocation: 'middle', nameLocation: 'middle',
nameTextStyle: { nameTextStyle: {
color: xAxisNameStyle.color, color: xAxisNameStyle.color,

View File

@ -170,7 +170,7 @@ export class TbTimeSeriesChart {
this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled; this.comparisonEnabled = !!this.ctx.defaultSubscription.comparisonEnabled;
this.stackMode = !this.comparisonEnabled && this.settings.stack; this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) { if (this.settings.states && this.settings.states.length) {
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.dashboard.utils, this.settings.states); this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states);
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
} }
const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
@ -503,12 +503,12 @@ export class TbTimeSeriesChart {
private setupXAxes(): void { private setupXAxes(): void {
const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, const mainXAxis = createTimeSeriesXAxis('main', this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode); this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.ctx.utilsService, this.darkMode);
this.xAxisList.push(mainXAxis); this.xAxisList.push(mainXAxis);
if (this.comparisonEnabled) { if (this.comparisonEnabled) {
const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis, const comparisonXAxis = createTimeSeriesXAxis('comparison', this.settings.comparisonXAxis,
this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime, this.ctx.defaultSubscription.comparisonTimeWindow.minTime, this.ctx.defaultSubscription.comparisonTimeWindow.maxTime,
this.ctx.date, this.darkMode); this.ctx.date, this.ctx.utilsService, this.darkMode);
this.xAxisList.push(comparisonXAxis); this.xAxisList.push(comparisonXAxis);
} }
} }
@ -526,7 +526,7 @@ export class TbTimeSeriesChart {
axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator; axisSettings.ticksGenerator = this.stateValueConverter.ticksGenerator;
axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter; axisSettings.ticksFormatter = this.stateValueConverter.ticksFormatter;
} }
const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode); const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.ctx.utilsService, this.darkMode);
this.yAxisList.push(yAxis); this.yAxisList.push(yAxis);
} }
} }

View File

@ -66,6 +66,35 @@
helpId="widget/lib/flot/tooltip_value_format_fn"> helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func> </tb-js-func>
</div> </div>
<div *ngIf="comparisonEnabled" class="tb-form-panel tb-slide-toggle" formGroupName="comparisonSettings">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.comparison.settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartKeySettingsForm.get('comparisonSettings.showValuesForComparison').value"
[disabled]="!timeSeriesChartKeySettingsForm.get('comparisonSettings.showValuesForComparison').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showValuesForComparison" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.time-series-chart.comparison.show-values-for-comparison' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.time-series-chart.comparison.comparison-values-label</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widgets.time-series-chart.comparison.comparison-values-label-auto' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.time-series-chart.comparison.comparison-data-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="color">
</tb-color-input>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container> </ng-container>
<ng-template #chartTypeTitle> <ng-template #chartTypeTitle>
<div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div> <div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div>

View File

@ -54,6 +54,8 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
yAxisIds: TimeSeriesChartYAxisId[]; yAxisIds: TimeSeriesChartYAxisId[];
comparisonEnabled: boolean;
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); functionScopeVariables = this.widgetService.getWidgetScopeVariables();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -73,6 +75,7 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
} }
const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings; const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings;
this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default']; this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default'];
this.comparisonEnabled = !!widgetSettings.comparisonEnabled;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -103,12 +106,14 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showInLegend', 'type']; return ['showInLegend', 'type', 'comparisonSettings.showValuesForComparison'];
} }
protected updateValidators(_emitEvent: boolean) { protected updateValidators(_emitEvent: boolean) {
const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value; const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value;
const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value; const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value;
const showValuesForComparison: boolean =
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('showValuesForComparison').value;
if (showInLegend) { if (showInLegend) {
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable(); this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable();
} else { } else {
@ -122,5 +127,17 @@ export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent
this.timeSeriesChartKeySettingsForm.get('lineSettings').disable(); this.timeSeriesChartKeySettingsForm.get('lineSettings').disable();
this.timeSeriesChartKeySettingsForm.get('barSettings').enable(); this.timeSeriesChartKeySettingsForm.get('barSettings').enable();
} }
if (this.comparisonEnabled) {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').enable({emitEvent: false});
if (showValuesForComparison) {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').enable();
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').enable();
} else {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('comparisonValuesLabel').disable();
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').get('color').disable();
}
} else {
this.timeSeriesChartKeySettingsForm.get('comparisonSettings').disable({emitEvent: false});
}
} }
} }

View File

@ -16,6 +16,46 @@
--> -->
<ng-container [formGroup]="timeSeriesChartWidgetSettingsForm"> <ng-container [formGroup]="timeSeriesChartWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-row no-border no-padding column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="comparisonEnabled">
{{ 'widgets.time-series-chart.comparison.comparison' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="timeSeriesChartWidgetSettingsForm.get('timeForComparison').value === 'customInterval'"
appearance="outline" class="number flex-lt-md" subscriptSizing="dynamic">
<input matInput formControlName="comparisonCustomIntervalValue" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-time-series-chart-axis-settings-button
axisType="xAxis"
panelTitle="{{ 'widgets.time-series-chart.axis.comparison-x-axis-settings' | translate }}"
formControlName="comparisonXAxis">
</tb-time-series-chart-axis-settings-button>
</div>
</div>
</div>
<tb-time-series-chart-states-panel <tb-time-series-chart-states-panel
*ngIf="chartType === TimeSeriesChartType.state" *ngIf="chartType === TimeSeriesChartType.state"
formControlName="states"> formControlName="states">

View File

@ -23,7 +23,7 @@ import {
WidgetSettings, WidgetSettings,
WidgetSettingsComponent WidgetSettingsComponent
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils';
@ -114,6 +114,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.timeSeriesChartWidgetSettingsForm = this.fb.group({ this.timeSeriesChartWidgetSettingsForm = this.fb.group({
comparisonEnabled: [settings.comparisonEnabled, []],
timeForComparison: [settings.timeForComparison, []],
comparisonCustomIntervalValue: [settings.comparisonCustomIntervalValue, [Validators.min(0)]],
comparisonXAxis: [settings.comparisonXAxis, []],
yAxes: [settings.yAxes, []], yAxes: [settings.yAxes, []],
thresholds: [settings.thresholds, []], thresholds: [settings.thresholds, []],
@ -154,14 +159,25 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['showLegend', 'showTooltip', 'tooltipShowDate']; return ['comparisonEnabled', 'showLegend', 'showTooltip', 'tooltipShowDate'];
} }
protected updateValidators(emitEvent: boolean) { protected updateValidators(emitEvent: boolean) {
const comparisonEnabled: boolean = this.timeSeriesChartWidgetSettingsForm.get('comparisonEnabled').value;
const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value; const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value; const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value; const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value;
if (comparisonEnabled) {
this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').enable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').enable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').enable();
} else {
this.timeSeriesChartWidgetSettingsForm.get('timeForComparison').disable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonCustomIntervalValue').disable();
this.timeSeriesChartWidgetSettingsForm.get('comparisonXAxis').disable();
}
if (showLegend) { if (showLegend) {
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable();
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable();

View File

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2024 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.
-->
<button type="button"
mat-stroked-button
color="primary"
[disabled]="disabled"
#matButton
(click)="openAxisSettingsPopup($event, matButton)"
matTooltip="{{ panelTitle }}"
matTooltipPosition="above">
{{ (axisType === 'xAxis' ? 'widgets.time-series-chart.axis.x-axis' : 'widgets.time-series-chart.axis.y-axis') | translate }}
<tb-icon matButtonIcon>settings</tb-icon>
</button>

View File

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2024 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, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models';
import {
TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
@Component({
selector: 'tb-time-series-chart-axis-settings-button',
templateUrl: './time-series-chart-axis-settings-button.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartAxisSettingsButtonComponent),
multi: true
}
]
})
export class TimeSeriesChartAxisSettingsButtonComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
axisType: 'xAxis' | 'yAxis' = 'xAxis';
@Input()
panelTitle: string;
@Input()
@coerceBoolean()
advanced = false;
private modelValue: TimeSeriesChartAxisSettings;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: TimeSeriesChartAxisSettings): void {
this.modelValue = value;
}
openAxisSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
axisSettings: this.modelValue,
axisType: this.axisType,
panelTitle: this.panelTitle,
advanced: this.advanced
};
const axisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
axisSettingsPanelPopover.tbComponentRef.instance.popover = axisSettingsPanelPopover;
axisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((axisSettings) => {
axisSettingsPanelPopover.hide();
this.modelValue = axisSettings;
this.propagateChange(this.modelValue);
});
}
}
}

View File

@ -15,17 +15,17 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-y-axis-settings-panel" [formGroup]="yAxisSettingsFormGroup"> <div class="tb-axis-settings-panel" [formGroup]="axisSettingsFormGroup">
<div class="tb-y-axis-settings-title">{{ 'widgets.time-series-chart.axis.y-axis-settings' | translate }}</div> <div class="tb-axis-settings-title">{{ panelTitle }}</div>
<div class="tb-y-axis-settings-panel-content"> <div class="tb-axis-settings-panel-content">
<tb-time-series-chart-axis-settings <tb-time-series-chart-axis-settings
formControlName="yAxis" formControlName="axis"
alwaysExpanded alwaysExpanded
[advanced]="advanced" [advanced]="advanced"
axisType="yAxis"> [axisType]="axisType">
</tb-time-series-chart-axis-settings> </tb-time-series-chart-axis-settings>
</div> </div>
<div class="tb-y-axis-settings-panel-buttons"> <div class="tb-axis-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
@ -35,8 +35,8 @@
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
(click)="applyYAxisSettings()" (click)="applyAxisSettings()"
[disabled]="yAxisSettingsFormGroup.invalid || !yAxisSettingsFormGroup.dirty"> [disabled]="axisSettingsFormGroup.invalid || !axisSettingsFormGroup.dirty">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </button>
</div> </div>

View File

@ -15,7 +15,7 @@
*/ */
@import '../../../../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-y-axis-settings-panel { .tb-axis-settings-panel {
width: 530px; width: 530px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,7 +23,7 @@
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
width: 90vw; width: 90vw;
} }
.tb-y-axis-settings-panel-content { .tb-axis-settings-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
@ -31,14 +31,14 @@
margin: -10px; margin: -10px;
padding: 10px; padding: 10px;
} }
.tb-y-axis-settings-title { .tb-axis-settings-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-y-axis-settings-panel-buttons { .tb-axis-settings-panel-buttons {
height: 40px; height: 40px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -17,40 +17,49 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; import {
TimeSeriesChartAxisSettings,
TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-time-series-chart-y-axis-settings-panel', selector: 'tb-time-series-chart-axis-settings-panel',
templateUrl: './time-series-chart-y-axis-settings-panel.component.html', templateUrl: './time-series-chart-axis-settings-panel.component.html',
providers: [], providers: [],
styleUrls: ['./time-series-chart-y-axis-settings-panel.component.scss'], styleUrls: ['./time-series-chart-axis-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit { export class TimeSeriesChartAxisSettingsPanelComponent implements OnInit {
@Input() @Input()
yAxisSettings: TimeSeriesChartYAxisSettings; axisType: 'xAxis' | 'yAxis' = 'xAxis';
@Input()
panelTitle: string;
@Input()
axisSettings: TimeSeriesChartAxisSettings;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
advanced = false; advanced = false;
@Input() @Input()
popover: TbPopoverComponent<TimeSeriesChartYAxisSettingsPanelComponent>; popover: TbPopoverComponent<TimeSeriesChartAxisSettingsPanelComponent>;
@Output() @Output()
yAxisSettingsApplied = new EventEmitter<TimeSeriesChartYAxisSettings>(); axisSettingsApplied = new EventEmitter<TimeSeriesChartAxisSettings>();
yAxisSettingsFormGroup: UntypedFormGroup; axisSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder) { constructor(private fb: UntypedFormBuilder) {
} }
ngOnInit(): void { ngOnInit(): void {
this.yAxisSettingsFormGroup = this.fb.group( this.axisSettingsFormGroup = this.fb.group(
{ {
yAxis: [this.yAxisSettings, []] axis: [this.axisSettings, []]
} }
); );
} }
@ -59,8 +68,8 @@ export class TimeSeriesChartYAxisSettingsPanelComponent implements OnInit {
this.popover?.hide(); this.popover?.hide();
} }
applyYAxisSettings() { applyAxisSettings() {
const yAxisSettings = this.yAxisSettingsFormGroup.get('yAxis').getRawValue(); const axisSettings = this.axisSettingsFormGroup.get('axis').getRawValue();
this.yAxisSettingsApplied.emit(yAxisSettings); this.axisSettingsApplied.emit(axisSettings);
} }
} }

View File

@ -36,9 +36,10 @@ import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { import {
TimeSeriesChartYAxisSettingsPanelComponent TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-time-series-chart-y-axis-row', selector: 'tb-time-series-chart-y-axis-row',
@ -76,6 +77,7 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
@ -143,16 +145,18 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const ctx: any = { const ctx: any = {
yAxisSettings: deepClone(this.modelValue), axisType: 'yAxis',
panelTitle: this.translate.instant('widgets.time-series-chart.axis.y-axis-settings'),
axisSettings: deepClone(this.modelValue),
advanced: this.advanced advanced: this.advanced
}; };
const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const yAxisSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimeSeriesChartYAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, TimeSeriesChartAxisSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx, ctx,
{}, {},
{}, {}, true); {}, {}, true);
yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover; yAxisSettingsPanelPopover.tbComponentRef.instance.popover = yAxisSettingsPanelPopover;
yAxisSettingsPanelPopover.tbComponentRef.instance.yAxisSettingsApplied.subscribe((yAxisSettings) => { yAxisSettingsPanelPopover.tbComponentRef.instance.axisSettingsApplied.subscribe((yAxisSettings) => {
yAxisSettingsPanelPopover.hide(); yAxisSettingsPanelPopover.hide();
this.modelValue = {...this.modelValue, ...yAxisSettings}; this.modelValue = {...this.modelValue, ...yAxisSettings};
this.axisFormGroup.patchValue( this.axisFormGroup.patchValue(

View File

@ -19,6 +19,7 @@
[class.tb-suffix-absolute]="!keysFormControl.value?.length"> [class.tb-suffix-absolute]="!keysFormControl.value?.length">
<mat-chip-grid #chipList [formControl]="keysFormControl"> <mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type" <mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
[removable]="removable"
(removed)="removeKey()"> (removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels"> <div class="tb-chip-labels">
@ -49,7 +50,8 @@
(click)="editKey()" mat-icon-button class="tb-mat-24"> (click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon> <mat-icon class="tb-mat-18">edit</mat-icon>
</button> </button>
<button matChipRemove <button *ngIf="removable"
matChipRemove
type="button" type="button"
mat-icon-button class="tb-mat-24"> mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon> <mat-icon class="tb-mat-18">close</mat-icon>

View File

@ -16,6 +16,13 @@
.tb-data-key-input { .tb-data-key-input {
.mat-mdc-form-field.tb-inline-field.tb-key-field { .mat-mdc-form-field.tb-inline-field.tb-key-field {
width: 100%; width: 100%;
&.mat-form-field-appearance-fill {
.mdc-text-field--filled.mdc-text-field--disabled {
&:before {
opacity: 0;
}
}
}
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
padding-left: 8px; padding-left: 8px;
padding-right: 0; padding-right: 0;

View File

@ -98,6 +98,10 @@ export class DataKeyInputComponent implements ControlValueAccessor, OnInit, OnCh
@coerceBoolean() @coerceBoolean()
editable = true; editable = true;
@Input()
@coerceBoolean()
removable = true;
@Input() @Input()
datasourceType: DatasourceType; datasourceType: DatasourceType;

View File

@ -116,8 +116,8 @@ import {
TimeSeriesChartYAxisRowComponent TimeSeriesChartYAxisRowComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component';
import { import {
TimeSeriesChartYAxisSettingsPanelComponent TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-settings-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
import { import {
TimeSeriesChartAnimationSettingsComponent TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
@ -139,6 +139,9 @@ import {
import { import {
TimeSeriesChartStatesPanelComponent TimeSeriesChartStatesPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component'; } from '@home/components/widget/lib/settings/common/chart/time-series-chart-states-panel.component';
import {
TimeSeriesChartAxisSettingsButtonComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-button.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -184,7 +187,8 @@ import {
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent, TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent, TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartThresholdSettingsComponent,
@ -241,7 +245,8 @@ import {
TimeSeriesNoAggregationBarWidthSettingsComponent, TimeSeriesNoAggregationBarWidthSettingsComponent,
TimeSeriesChartYAxesPanelComponent, TimeSeriesChartYAxesPanelComponent,
TimeSeriesChartYAxisRowComponent, TimeSeriesChartYAxisRowComponent,
TimeSeriesChartYAxisSettingsPanelComponent, TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent, TimeSeriesChartAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent, TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent, TimeSeriesChartThresholdSettingsComponent,

View File

@ -103,6 +103,7 @@ import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service'; import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -194,6 +195,7 @@ export class WidgetContext {
customDialog: CustomDialogService; customDialog: CustomDialogService;
resourceService: ResourceService; resourceService: ResourceService;
userSettingsService: UserSettingsService; userSettingsService: UserSettingsService;
utilsService: UtilsService;
telemetryWsService: TelemetryWebsocketService; telemetryWsService: TelemetryWebsocketService;
telemetrySubscribers?: TelemetrySubscriber[]; telemetrySubscribers?: TelemetrySubscriber[];
date: DatePipe; date: DatePipe;

View File

@ -6749,6 +6749,15 @@
"bar-width": "Bar width", "bar-width": "Bar width",
"bar-width-relative": "Percentage of time window", "bar-width-relative": "Percentage of time window",
"bar-width-absolute": "Absolute (ms)", "bar-width-absolute": "Absolute (ms)",
"comparison": {
"comparison": "Comparison",
"show": "Show",
"settings": "Comparison settings",
"show-values-for-comparison": "Show historical data for comparison",
"comparison-values-label": "Comparison key label",
"comparison-values-label-auto": "Auto",
"comparison-data-color": "Comparison data color"
},
"threshold": { "threshold": {
"thresholds": "Thresholds", "thresholds": "Thresholds",
"source": "Source", "source": "Source",
@ -6802,6 +6811,7 @@
"x-axis": "X axis", "x-axis": "X axis",
"y-axis": "Y axis", "y-axis": "Y axis",
"y-axis-settings": "Y axis settings", "y-axis-settings": "Y axis settings",
"comparison-x-axis-settings": "Comparison X axis settings",
"remove-y-axis": "Remove Y axis", "remove-y-axis": "Remove Y axis",
"id": "Id", "id": "Id",
"label": "Label", "label": "Label",

View File

@ -216,6 +216,18 @@
flex: 1; flex: 1;
width: auto; width: auto;
} }
&.flex-xs {
@media #{$mat-xs} {
width: auto;
flex: 1;
}
}
&.flex-lt-md {
@media #{$mat-lt-md} {
width: auto;
flex: 1;
}
}
} }
.fixed-title-width { .fixed-title-width {
min-width: 200px; min-width: 200px;