39 changed files with 2258 additions and 65 deletions
@ -0,0 +1,94 @@ |
|||
<!-- |
|||
|
|||
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 [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-aggregated-data-key-row"> |
|||
<mat-form-field class="tb-inline-field tb-position-field" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="position"> |
|||
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position"> |
|||
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="tb-inline-field tb-aggregation-field" subscriptSizing="dynamic"> |
|||
<mat-chip-grid #chipList> |
|||
<mat-chip-row class="tb-datakey-chip"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"> |
|||
<div class="tb-chip-labels"> |
|||
<div class="tb-chip-label tb-chip-text"> |
|||
<ng-container *ngTemplateOutlet="keyName"></ng-container> |
|||
</div> |
|||
</div> |
|||
<button type="button" |
|||
(click)="editKey()" mat-icon-button class="tb-mat-24"> |
|||
<mat-icon class="tb-mat-18">edit</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-chip-row> |
|||
</mat-chip-grid> |
|||
<input matInput |
|||
type="text" |
|||
fxHide |
|||
readonly |
|||
[matChipInputFor]="chipList" |
|||
/> |
|||
</mat-form-field> |
|||
<div class="tb-units-field"> |
|||
<tb-unit-input formControlName="units"> |
|||
</tb-unit-input> |
|||
</div> |
|||
<div class="tb-decimals-field"> |
|||
<mat-form-field appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-font-field"> |
|||
<tb-font-settings formControlName="font" |
|||
[previewText]="valuePreviewFn"> |
|||
</tb-font-settings> |
|||
</div> |
|||
<div class="tb-color-field"> |
|||
<tb-color-settings formControlName="color"> |
|||
</tb-color-settings> |
|||
</div> |
|||
<div class="tb-form-table-row-cell-buttons"> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="keyRemoved.emit()" |
|||
matTooltip="{{ 'widgets.aggregated-value-card.remove-value' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #keyName> |
|||
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName"> |
|||
<strong><span>f(</span></strong><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><strong><span>)</span></strong> |
|||
</ng-container> |
|||
<ng-template #keyName> |
|||
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container> |
|||
</ng-template> |
|||
</ng-template> |
|||
<ng-template #keyNameTemplate> |
|||
<ng-container> |
|||
<strong><span class="tb-agg-func">{{ (modelValue?.aggregationType || aggregationTypes.NONE) }}</span></strong> |
|||
<span *ngIf="!modelValue?.aggregationType || modelValue?.aggregationType === aggregationTypes.NONE; else aggValue">({{ 'datakey.latest-value' | translate }})</span> |
|||
</ng-container> |
|||
<ng-template #aggValue> |
|||
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType !== comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta' | translate }}:{{ (modelValue?.comparisonResultType === comparisonResultTypes.DELTA_PERCENT ? 'datakey.percent' : 'datakey.absolute') | translate }})</span> |
|||
<span *ngIf="modelValue?.comparisonEnabled && modelValue?.comparisonResultType === comparisonResultTypes.PREVIOUS_VALUE">({{ 'datakey.delta-calculation-result-previous-value' | translate }})</span> |
|||
</ng-template> |
|||
</ng-template> |
|||
@ -0,0 +1,88 @@ |
|||
/** |
|||
* 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 '../../../../../../../../scss/constants'; |
|||
|
|||
.tb-aggregated-data-key-row { |
|||
.mat-mdc-form-field.tb-inline-field.tb-aggregation-field { |
|||
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { |
|||
padding-left: 8px; |
|||
padding-right: 0; |
|||
.mat-mdc-form-field-infix { |
|||
padding-top: 0; |
|||
padding-bottom: 6px; |
|||
.mdc-evolution-chip-set .mdc-evolution-chip { |
|||
margin: 0; |
|||
} |
|||
input.mat-mdc-chip-input { |
|||
height: 32px; |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
} |
|||
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { |
|||
.tb-attribute-chip { |
|||
.tb-chip-labels { |
|||
background: transparent; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-position-field { |
|||
width: 132px; |
|||
min-width: 132px; |
|||
} |
|||
|
|||
.tb-aggregation-field { |
|||
flex: 1; |
|||
min-width: 150px; |
|||
} |
|||
|
|||
.tb-units-field, .tb-decimals-field, .tb-font-field, .tb-color-field { |
|||
display: flex; |
|||
flex-direction: row; |
|||
place-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.tb-units-field { |
|||
width: 80px; |
|||
min-width: 80px; |
|||
} |
|||
|
|||
.tb-decimals-field { |
|||
width: 60px; |
|||
min-width: 60px; |
|||
} |
|||
|
|||
.tb-font-field { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
.tb-color-field { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
.tb-units-field, .tb-decimals-field { |
|||
display: none; |
|||
@media #{$mat-gt-sm} { |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,229 @@ |
|||
///
|
|||
/// 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 { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
EventEmitter, |
|||
forwardRef, |
|||
Input, |
|||
OnChanges, |
|||
OnInit, |
|||
Output, |
|||
SimpleChanges, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { |
|||
ComparisonResultType, |
|||
DataKey, |
|||
DataKeyConfigMode, |
|||
DatasourceType, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { AggregationType } from '@shared/models/time/time.models'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigDialogData |
|||
} from '@home/components/widget/config/data-key-config-dialog.component'; |
|||
import { deepClone, formatValue } from '@core/utils'; |
|||
import { |
|||
AggregatedValueCardKeyPosition, |
|||
aggregatedValueCardKeyPositionTranslations, |
|||
AggregatedValueCardKeySettings |
|||
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aggregated-data-key-row', |
|||
templateUrl: './aggregated-data-key-row.component.html', |
|||
styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AggregatedDataKeyRowComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { |
|||
|
|||
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = |
|||
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); |
|||
|
|||
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; |
|||
|
|||
dataKeyTypes = DataKeyType; |
|||
|
|||
aggregationTypes = AggregationType; |
|||
|
|||
comparisonResultTypes = ComparisonResultType; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
datasourceType: DatasourceType; |
|||
|
|||
@Input() |
|||
keyName: string; |
|||
|
|||
@Output() |
|||
keyRemoved = new EventEmitter(); |
|||
|
|||
keyRowFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: DataKey; |
|||
|
|||
valuePreviewFn = this._valuePreviewFn.bind(this); |
|||
|
|||
get callbacks(): DataKeysCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
get isEntityDatasource(): boolean { |
|||
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); |
|||
} |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private dialog: MatDialog, |
|||
private cd: ChangeDetectorRef, |
|||
public translate: TranslateService, |
|||
public truncate: TruncatePipe, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.keyRowFormGroup = this.fb.group({ |
|||
position: [null, []], |
|||
units: [null, []], |
|||
decimals: [null, []], |
|||
font: [null, []], |
|||
color: [null, []] |
|||
}); |
|||
this.keyRowFormGroup.valueChanges.subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (['keyName'].includes(propName)) { |
|||
if (change.currentValue) { |
|||
this.modelValue.name = change.currentValue; |
|||
setTimeout(() => { |
|||
this.updateModel(); |
|||
}, 0); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.keyRowFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.keyRowFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: DataKey): void { |
|||
this.modelValue = value || {} as DataKey; |
|||
const settings: AggregatedValueCardKeySettings = (this.modelValue.settings || {}); |
|||
this.keyRowFormGroup.patchValue( |
|||
{ |
|||
position: settings.position || AggregatedValueCardKeyPosition.center, |
|||
units: value?.units, |
|||
decimals: value?.decimals, |
|||
font: settings.font, |
|||
color: settings.color |
|||
}, {emitEvent: false} |
|||
); |
|||
this.cd.markForCheck(); |
|||
} |
|||
|
|||
dataKeyHasPostprocessing(): boolean { |
|||
return !!this.modelValue?.postFuncBody; |
|||
} |
|||
|
|||
editKey() { |
|||
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
dataKey: deepClone(this.modelValue), |
|||
dataKeyConfigMode: DataKeyConfigMode.general, |
|||
dataKeySettingsSchema: null, |
|||
dataKeySettingsDirective: null, |
|||
dashboard: null, |
|||
aliasController: null, |
|||
widget: null, |
|||
widgetType: widgetType.latest, |
|||
deviceId: null, |
|||
entityAliasId: null, |
|||
showPostProcessing: true, |
|||
callbacks: this.callbacks, |
|||
hideDataKeyName: true, |
|||
hideDataKeyLabel: true, |
|||
hideDataKeyColor: true |
|||
} |
|||
}).afterClosed().subscribe((updatedDataKey) => { |
|||
if (updatedDataKey) { |
|||
this.modelValue = updatedDataKey; |
|||
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); |
|||
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); |
|||
this.updateModel(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const value = this.keyRowFormGroup.value; |
|||
this.modelValue.settings = this.modelValue.settings || {}; |
|||
this.modelValue.settings.position = value.position; |
|||
this.modelValue.settings.font = value.font; |
|||
this.modelValue.settings.color = value.color; |
|||
this.modelValue.units = value.units; |
|||
this.modelValue.decimals = value.decimals; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
private _valuePreviewFn(): string { |
|||
const units: string = this.keyRowFormGroup.get('units').value; |
|||
const decimals: number = this.keyRowFormGroup.get('decimals').value; |
|||
return formatValue(22, decimals, units, true); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-form-panel tb-aggregated-data-keys-panel"> |
|||
<div class="tb-form-panel-title">{{ 'widgets.aggregated-value-card.values' | translate }}</div> |
|||
<div class="tb-form-table"> |
|||
<div class="tb-form-table-header"> |
|||
<div class="tb-form-table-header-cell tb-position-header" translate>widgets.aggregated-value-card.position</div> |
|||
<div class="tb-form-table-header-cell tb-aggregation-header" translate>widgets.aggregated-value-card.aggregation</div> |
|||
<div class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div> |
|||
<div class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div> |
|||
<div class="tb-form-table-header-cell tb-font-header" translate>widgets.aggregated-value-card.font</div> |
|||
<div class="tb-form-table-header-cell tb-color-header" translate>widgets.aggregated-value-card.color</div> |
|||
<div class="tb-form-table-header-cell tb-actions-header"></div> |
|||
</div> |
|||
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body"> |
|||
<div class="tb-form-table-row" |
|||
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;"> |
|||
<tb-aggregated-data-key-row fxFlex |
|||
[formControl]="keyControl" |
|||
[datasourceType]="datasourceType" |
|||
[keyName]="keyName" |
|||
(keyRemoved)="removeKey($index)"> |
|||
</tb-aggregated-data-key-row> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> |
|||
{{ 'widgets.aggregated-value-card.add-value' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noKeys> |
|||
<span fxLayoutAlign="center center" |
|||
class="tb-prompt">{{ 'widgets.aggregated-value-card.no-values' | translate }}</span> |
|||
</ng-template> |
|||
@ -0,0 +1,69 @@ |
|||
/** |
|||
* 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 '../../../../../../../../scss/constants'; |
|||
|
|||
.tb-aggregated-data-keys-panel { |
|||
.tb-form-table-header-cell { |
|||
&.tb-position-header { |
|||
width: 132px; |
|||
min-width: 132px; |
|||
} |
|||
|
|||
&.tb-aggregation-header { |
|||
flex: 1; |
|||
min-width: 150px; |
|||
} |
|||
|
|||
&.tb-units-header { |
|||
width: 80px; |
|||
min-width: 80px; |
|||
} |
|||
|
|||
&.tb-decimals-header { |
|||
width: 60px; |
|||
min-width: 60px; |
|||
} |
|||
|
|||
&.tb-font-header { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
&.tb-color-header { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
&.tb-actions-header { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
&.tb-units-header, &.tb-decimals-header { |
|||
display: none; |
|||
@media #{$mat-gt-sm} { |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
.tb-form-table-body { |
|||
tb-aggregated-data-key-row { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,173 @@ |
|||
///
|
|||
/// 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 { |
|||
ChangeDetectorRef, |
|||
Component, |
|||
forwardRef, |
|||
Input, |
|||
OnChanges, |
|||
OnInit, |
|||
SimpleChanges, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup |
|||
} from '@angular/forms'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aggregated-data-keys-panel', |
|||
templateUrl: './aggregated-data-keys-panel.component.html', |
|||
styleUrls: ['./aggregated-data-keys-panel.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => AggregatedDataKeysPanelComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
datasourceType: DatasourceType; |
|||
|
|||
@Input() |
|||
keyName: string; |
|||
|
|||
dataKeyType: DataKeyType; |
|||
|
|||
keysListFormGroup: UntypedFormGroup; |
|||
|
|||
get widgetType(): widgetType { |
|||
return this.widgetConfigComponent.widgetType; |
|||
} |
|||
|
|||
get callbacks(): DataKeysCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
get noKeys(): boolean { |
|||
const keys: DataKey[] = this.keysListFormGroup.get('keys').value; |
|||
return keys.length === 0; |
|||
} |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private dialog: MatDialog, |
|||
private cd: ChangeDetectorRef, |
|||
private utils: UtilsService, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.keysListFormGroup = this.fb.group({ |
|||
keys: [this.fb.array([]), []] |
|||
}); |
|||
this.keysListFormGroup.valueChanges.subscribe( |
|||
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value) |
|||
); |
|||
this.updateParams(); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (['datasourceType'].includes(propName)) { |
|||
this.updateParams(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private updateParams() { |
|||
if (this.datasourceType === DatasourceType.function) { |
|||
this.dataKeyType = DataKeyType.function; |
|||
} else { |
|||
this.dataKeyType = DataKeyType.timeseries; |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
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; |
|||
} |
|||
|
|||
removeKey(index: number) { |
|||
(this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); |
|||
} |
|||
|
|||
addKey() { |
|||
const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null); |
|||
dataKey.decimals = 0; |
|||
dataKey.settings = {...aggregatedValueCardDefaultKeySettings}; |
|||
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; |
|||
const keyControl = this.fb.control(dataKey, []); |
|||
keysArray.push(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); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="aggregatedValueCardWidgetConfigForm"> |
|||
<tb-timewindow-config-panel formControlName="timewindowConfig"> |
|||
</tb-timewindow-config-panel> |
|||
<tb-datasources |
|||
[configMode]="basicMode" |
|||
hideDataKeyLabel |
|||
hideDataKeyColor |
|||
hideDataKeyUnits |
|||
hideDataKeyDecimals |
|||
hideLatestDataKeys |
|||
formControlName="datasources"> |
|||
</tb-datasources> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.appearance</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle"> |
|||
{{ 'widget-config.title' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="titleFont" |
|||
clearButton |
|||
[previewText]="aggregatedValueCardWidgetConfigForm.get('title').value" |
|||
[initialPreviewStyle]="widgetConfig.config.titleStyle"> |
|||
</tb-font-settings> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="titleColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon"> |
|||
{{ 'widgets.value-card.icon' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> |
|||
<tb-material-icon-select asBoxInput |
|||
iconClearButton |
|||
[color]="aggregatedValueCardWidgetConfigForm.get('iconColor').value" |
|||
formControlName="icon"> |
|||
</tb-material-icon-select> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="iconColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showSubtitle"> |
|||
{{ 'widgets.aggregated-value-card.subtitle' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="subtitle" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-font-settings formControlName="subtitleFont" |
|||
clearButton |
|||
[previewText]="aggregatedValueCardWidgetConfigForm.get('subtitle').value"> |
|||
</tb-font-settings> |
|||
<tb-color-settings formControlName="subtitleColor"> |
|||
</tb-color-settings> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row column-xs"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate"> |
|||
{{ 'widgets.value-card.date' | translate }} |
|||
</mat-slide-toggle> |
|||
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select> |
|||
<tb-font-settings formControlName="dateFont" |
|||
[previewText]="datePreviewFn"> |
|||
</tb-font-settings> |
|||
<tb-color-settings formControlName="dateColor"> |
|||
</tb-color-settings> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showChart"> |
|||
{{ 'widgets.aggregated-value-card.chart' | translate }} |
|||
</mat-slide-toggle> |
|||
<tb-color-settings formControlName="chartColor"> |
|||
</tb-color-settings> |
|||
</div> |
|||
</div> |
|||
<tb-aggregated-data-keys-panel |
|||
[datasourceType]="datasource?.type" |
|||
[keyName]="keyName" |
|||
formControlName="values"> |
|||
</tb-aggregated-data-keys-panel> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between column-lt-md"> |
|||
<div translate>widget-config.show-card-buttons</div> |
|||
<mat-chip-listbox multiple formControlName="cardButtons"> |
|||
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-border-radius' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<tb-widget-actions-panel |
|||
formControlName="actions"> |
|||
</tb-widget-actions-panel> |
|||
</ng-container> |
|||
@ -0,0 +1,291 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, Injector } from '@angular/core'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
import { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
getTimewindowConfig, |
|||
setTimewindowConfig |
|||
} from '@home/components/widget/config/timewindow-config-panel.component'; |
|||
import { isUndefined } from '@core/utils'; |
|||
import { |
|||
cssSizeToStrSize, |
|||
DateFormatProcessor, |
|||
DateFormatSettings, getDataKey, |
|||
resolveCssSize |
|||
} from '@shared/models/widget-settings.models'; |
|||
import { |
|||
aggregatedValueCardDefaultSettings, |
|||
AggregatedValueCardWidgetSettings, |
|||
createDefaultAggregatedValueLatestDataKeys |
|||
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
|||
import { |
|||
AggregationType, |
|||
HistoryWindowType, |
|||
HOUR, |
|||
QuickTimeInterval, |
|||
TimewindowType |
|||
} from '@shared/models/time/time.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aggregated-value-card-basic-config', |
|||
templateUrl: './aggregated-value-card-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss'] |
|||
}) |
|||
export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
public get datasource(): Datasource { |
|||
const datasources: Datasource[] = this.aggregatedValueCardWidgetConfigForm.get('datasources').value; |
|||
if (datasources && datasources.length) { |
|||
return datasources[0]; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public get keyName(): string { |
|||
const dataKey = getDataKey(this.aggregatedValueCardWidgetConfigForm.get('datasources').value); |
|||
if (dataKey) { |
|||
return dataKey.name; |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
aggregatedValueCardWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
datePreviewFn = this._datePreviewFn.bind(this); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected widgetConfigComponent: WidgetConfigComponent, |
|||
private cd: ChangeDetectorRef, |
|||
private $injector: Injector, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store, widgetConfigComponent); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.aggregatedValueCardWidgetConfigForm; |
|||
} |
|||
|
|||
protected setupDefaults(configData: WidgetConfigComponentData) { |
|||
this.setupDefaultDatasource(configData, [ |
|||
{ name: 'watermeter', label: 'Watermeter', type: DataKeyType.timeseries } |
|||
], |
|||
createDefaultAggregatedValueLatestDataKeys('watermeter', 'm³') |
|||
); |
|||
configData.config.useDashboardTimewindow = false; |
|||
configData.config.displayTimewindow = true; |
|||
configData.config.timewindow = { |
|||
selectedTab: TimewindowType.HISTORY, |
|||
history: { |
|||
historyType: HistoryWindowType.INTERVAL, |
|||
quickInterval: QuickTimeInterval.CURRENT_MONTH_SO_FAR, |
|||
}, |
|||
aggregation: { |
|||
type: AggregationType.AVG, |
|||
interval: 12 * HOUR, |
|||
limit: 5000 |
|||
} |
|||
}; |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
const settings: AggregatedValueCardWidgetSettings = {...aggregatedValueCardDefaultSettings, ...(configData.config.settings || {})}; |
|||
const iconSize = resolveCssSize(configData.config.iconSize); |
|||
this.aggregatedValueCardWidgetConfigForm = this.fb.group({ |
|||
timewindowConfig: [getTimewindowConfig(configData.config), []], |
|||
datasources: [configData.config.datasources, []], |
|||
|
|||
showTitle: [configData.config.showTitle, []], |
|||
title: [configData.config.title, []], |
|||
titleFont: [configData.config.titleFont, []], |
|||
titleColor: [configData.config.titleColor, []], |
|||
|
|||
showIcon: [configData.config.showTitleIcon, []], |
|||
iconSize: [iconSize[0], [Validators.min(0)]], |
|||
iconSizeUnit: [iconSize[1], []], |
|||
icon: [configData.config.titleIcon, []], |
|||
iconColor: [configData.config.iconColor, []], |
|||
|
|||
showSubtitle: [settings.showSubtitle, []], |
|||
subtitle: [settings.subtitle, []], |
|||
subtitleFont: [settings.subtitleFont, []], |
|||
subtitleColor: [settings.subtitleColor, []], |
|||
|
|||
showDate: [settings.showDate, []], |
|||
dateFormat: [settings.dateFormat, []], |
|||
dateFont: [settings.dateFont, []], |
|||
dateColor: [settings.dateColor, []], |
|||
|
|||
showChart: [settings.showChart, []], |
|||
chartColor: [settings.chartColor, []], |
|||
|
|||
values: [this.getValues(configData.config.datasources), []], |
|||
|
|||
background: [settings.background, []], |
|||
|
|||
cardButtons: [this.getCardButtons(configData.config), []], |
|||
borderRadius: [configData.config.borderRadius, []], |
|||
|
|||
actions: [configData.config.actions || {}, []] |
|||
}); |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); |
|||
this.widgetConfig.config.datasources = config.datasources; |
|||
|
|||
this.widgetConfig.config.showTitle = config.showTitle; |
|||
this.widgetConfig.config.title = config.title; |
|||
this.widgetConfig.config.titleFont = config.titleFont; |
|||
this.widgetConfig.config.titleColor = config.titleColor; |
|||
|
|||
this.widgetConfig.config.showTitleIcon = config.showIcon; |
|||
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); |
|||
this.widgetConfig.config.titleIcon = config.icon; |
|||
this.widgetConfig.config.iconColor = config.iconColor; |
|||
|
|||
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
|||
|
|||
this.widgetConfig.config.settings.showSubtitle = config.showSubtitle; |
|||
this.widgetConfig.config.settings.subtitle = config.subtitle; |
|||
this.widgetConfig.config.settings.subtitleFont = config.subtitleFont; |
|||
this.widgetConfig.config.settings.subtitleColor = config.subtitleColor; |
|||
|
|||
this.widgetConfig.config.settings.showDate = config.showDate; |
|||
this.widgetConfig.config.settings.dateFormat = config.dateFormat; |
|||
this.widgetConfig.config.settings.dateFont = config.dateFont; |
|||
this.widgetConfig.config.settings.dateColor = config.dateColor; |
|||
|
|||
this.widgetConfig.config.settings.showChart = config.showChart; |
|||
this.widgetConfig.config.settings.chartColor = config.chartColor; |
|||
|
|||
this.setValues(config.values, this.widgetConfig.config.datasources); |
|||
|
|||
this.widgetConfig.config.settings.background = config.background; |
|||
|
|||
this.setCardButtons(config.cardButtons, this.widgetConfig.config); |
|||
this.widgetConfig.config.borderRadius = config.borderRadius; |
|||
|
|||
this.widgetConfig.config.actions = config.actions; |
|||
return this.widgetConfig; |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return ['showTitle', 'showIcon', 'showSubtitle', 'showDate', 'showChart']; |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
const showTitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showTitle').value; |
|||
const showIcon: boolean = this.aggregatedValueCardWidgetConfigForm.get('showIcon').value; |
|||
const showSubtitle: boolean = this.aggregatedValueCardWidgetConfigForm.get('showSubtitle').value; |
|||
const showDate: boolean = this.aggregatedValueCardWidgetConfigForm.get('showDate').value; |
|||
const showChart: boolean = this.aggregatedValueCardWidgetConfigForm.get('showChart').value; |
|||
|
|||
if (showTitle) { |
|||
this.aggregatedValueCardWidgetConfigForm.get('title').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('titleFont').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('titleColor').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); |
|||
if (showIcon) { |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSize').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('icon').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconColor').enable(); |
|||
} else { |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
} else { |
|||
this.aggregatedValueCardWidgetConfigForm.get('title').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('titleFont').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('titleColor').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSize').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconSizeUnit').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('icon').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('iconColor').disable(); |
|||
} |
|||
|
|||
if (showSubtitle) { |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitle').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').enable(); |
|||
} else { |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitle').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitleFont').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('subtitleColor').disable(); |
|||
} |
|||
|
|||
if (showDate) { |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateFont').enable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateColor').enable(); |
|||
} else { |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateFormat').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateFont').disable(); |
|||
this.aggregatedValueCardWidgetConfigForm.get('dateColor').disable(); |
|||
} |
|||
|
|||
if (showChart) { |
|||
this.aggregatedValueCardWidgetConfigForm.get('chartColor').enable(); |
|||
} else { |
|||
this.aggregatedValueCardWidgetConfigForm.get('chartColor').disable(); |
|||
} |
|||
} |
|||
|
|||
private getValues(datasources?: Datasource[]): DataKey[] { |
|||
if (datasources && datasources.length) { |
|||
return datasources[0].latestDataKeys || []; |
|||
} |
|||
return []; |
|||
} |
|||
|
|||
private setValues(values: DataKey[], datasources?: Datasource[]) { |
|||
if (datasources && datasources.length) { |
|||
datasources[0].latestDataKeys = values; |
|||
} |
|||
} |
|||
|
|||
private getCardButtons(config: WidgetConfig): string[] { |
|||
const buttons: string[] = []; |
|||
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { |
|||
buttons.push('fullscreen'); |
|||
} |
|||
return buttons; |
|||
} |
|||
|
|||
private setCardButtons(buttons: string[], config: WidgetConfig) { |
|||
config.enableFullscreen = buttons.includes('fullscreen'); |
|||
} |
|||
|
|||
private _datePreviewFn(): string { |
|||
const dateFormat: DateFormatSettings = this.aggregatedValueCardWidgetConfigForm.get('dateFormat').value; |
|||
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); |
|||
processor.update(Date.now()); |
|||
return processor.formatted; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
<!-- |
|||
|
|||
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-aggregated-value-card-panel" [style]="backgroundStyle"> |
|||
<div class="tb-aggregated-value-card-overlay" [style]="overlayStyle"></div> |
|||
<div class="tb-aggregated-value-card-title-panel"> |
|||
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
|||
<ng-container *ngTemplateOutlet="subtitleTpl"></ng-container> |
|||
</div> |
|||
<ng-container *ngTemplateOutlet="valuesTpl"></ng-container> |
|||
<ng-container *ngTemplateOutlet="chartTpl"></ng-container> |
|||
<ng-container *ngTemplateOutlet="dateTpl"></ng-container> |
|||
</div> |
|||
|
|||
<ng-template #subtitleTpl> |
|||
<div *ngIf="showSubtitle" class="tb-aggregated-value-card-subtitle" |
|||
[style]="subtitleStyle" [style.color]="subtitleColor.color">{{ subtitle$ | async }}</div> |
|||
</ng-template> |
|||
<ng-template #valuesTpl> |
|||
<div *ngIf="showValues" class="tb-aggregated-value-card-values"> |
|||
<div class="tb-aggregated-value-card-values-container"> |
|||
<div class="tb-aggregated-value-card-values-section left"> |
|||
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftTop]"> |
|||
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftTop]}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftBottom]"> |
|||
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftBottom]}"></ng-container> |
|||
</ng-container> |
|||
</div> |
|||
<div class="tb-aggregated-value-card-values-section center"> |
|||
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.center]"> |
|||
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.center]}"></ng-container> |
|||
</ng-container> |
|||
</div> |
|||
<div class="tb-aggregated-value-card-values-section right"> |
|||
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightTop]"> |
|||
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightTop]}"></ng-container> |
|||
</ng-container> |
|||
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.rightBottom]"> |
|||
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.rightBottom]}"></ng-container> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #chartTpl> |
|||
<div *ngIf="showChart" class="tb-aggregated-value-card-chart"> |
|||
<div class="tb-aggregated-value-card-chart-ticks"> |
|||
<div>{{tickMax$ | async}}</div> |
|||
<div>{{tickMin$ | async}}</div> |
|||
</div> |
|||
<div class="tb-aggregated-value-card-chart-container"> |
|||
<div class="tb-aggregated-value-card-chart-element" #chartElement></div> |
|||
<div class="tb-aggregated-value-card-chart-boundary top left"></div> |
|||
<div class="tb-aggregated-value-card-chart-boundary top right"></div> |
|||
<div class="tb-aggregated-value-card-chart-boundary bottom left"></div> |
|||
<div class="tb-aggregated-value-card-chart-boundary bottom right"></div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #dateTpl> |
|||
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color" [innerHTML]="dateFormat.formatted"></div> |
|||
</ng-template> |
|||
<ng-template #valueTpl let-value="value"> |
|||
<div class="tb-aggregated-value-card-value" [style]="value.style" [style.color]="value.color.color"> |
|||
<div *ngIf="value.showArrow" class="value-arrow-container"> |
|||
<span *ngIf="!value.upArrow && !value.downArrow" class="value-arrow"></span> |
|||
<tb-icon *ngIf="value.upArrow" class="value-arrow">arrow_upward</tb-icon> |
|||
<tb-icon *ngIf="value.downArrow" class="value-arrow">arrow_downward</tb-icon> |
|||
</div> |
|||
<div class="value-text"> |
|||
<span>{{ value.value }}</span> |
|||
<span class="units" [class]="{'small': value.center}">{{ value.units }}</span> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
@ -0,0 +1,156 @@ |
|||
/** |
|||
* 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 { |
|||
.tb-aggregated-value-card-panel { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: stretch; |
|||
gap: 8px; |
|||
padding: 20px 24px 24px; |
|||
> div:not(.tb-value-card-overlay) { |
|||
z-index: 1; |
|||
} |
|||
.tb-aggregated-value-card-overlay { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
} |
|||
> div.tb-aggregated-value-card-title-panel { |
|||
display: flex; |
|||
flex-direction: column; |
|||
.tb-aggregated-value-card-subtitle { |
|||
margin-left: 28px; |
|||
} |
|||
} |
|||
.tb-aggregated-value-card-values, .tb-aggregated-value-card-chart { |
|||
flex: 1; |
|||
min-height: 0; |
|||
overflow: hidden; |
|||
} |
|||
.tb-aggregated-value-card-values-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
padding: 8px 0; |
|||
display: grid; |
|||
grid-template-columns: minmax(0, 1fr) fit-content(100%) minmax(0, 1fr); |
|||
.tb-aggregated-value-card-values-section { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
overflow: hidden; |
|||
&.left { |
|||
align-items: flex-start; |
|||
} |
|||
&.center { |
|||
align-items: center; |
|||
} |
|||
&.right { |
|||
align-items: flex-end; |
|||
} |
|||
} |
|||
} |
|||
.tb-aggregated-value-card-chart { |
|||
display: flex; |
|||
gap: 8px; |
|||
flex-direction: row; |
|||
.tb-aggregated-value-card-chart-ticks { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
place-content: flex-end space-between; |
|||
align-items: flex-end; |
|||
font-size: 11px; |
|||
line-height: 16px; |
|||
font-weight: 400; |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
.tb-aggregated-value-card-chart-container { |
|||
position: relative; |
|||
flex: 1; |
|||
margin-top: 8px; |
|||
margin-bottom: 8px; |
|||
.tb-aggregated-value-card-chart-element { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
.tb-aggregated-value-card-chart-boundary { |
|||
position: absolute; |
|||
width: 6px; |
|||
height: 6px; |
|||
&.top { |
|||
top: 0; |
|||
border-top: 2px solid rgba(0,0,0,0.38); |
|||
} |
|||
&.left { |
|||
left: 0; |
|||
border-left: 2px solid rgba(0,0,0,0.38); |
|||
} |
|||
&.right { |
|||
right: 0; |
|||
border-right: 2px solid rgba(0,0,0,0.38); |
|||
} |
|||
&.bottom { |
|||
bottom: 0; |
|||
border-bottom: 2px solid rgba(0,0,0,0.38); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.tb-aggregated-value-card-value { |
|||
white-space: nowrap; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: row; |
|||
place-content: center; |
|||
align-items: center; |
|||
.value-arrow-container { |
|||
display: flex; |
|||
} |
|||
.value-text { |
|||
line-height: 1; |
|||
} |
|||
.value-arrow { |
|||
font-size: 1.1em; |
|||
height: 1.1em; |
|||
line-height: 1.1em; |
|||
min-width: 1em; |
|||
width: 1em; |
|||
} |
|||
.units { |
|||
font-size: 85%; |
|||
padding-left: 0.2em; |
|||
&.small { |
|||
font-size: 50%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-aggregated-value-card-panel { |
|||
> div.tb-aggregated-value-card-title-panel { |
|||
.tb-widget-title { |
|||
padding: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterViewInit, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
Input, |
|||
OnInit, |
|||
TemplateRef, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { |
|||
aggregatedValueCardDefaultSettings, |
|||
AggregatedValueCardKeyPosition, |
|||
AggregatedValueCardValue, |
|||
AggregatedValueCardWidgetSettings, |
|||
computeAggregatedCardValue, |
|||
getTsValueByLatestDataKey |
|||
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { |
|||
backgroundStyle, |
|||
ColorProcessor, |
|||
ComponentStyle, |
|||
DateFormatProcessor, getDataKey, |
|||
getLatestSingleTsValue, |
|||
overlayStyle, |
|||
textStyle |
|||
} from '@shared/models/widget-settings.models'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { TbFlot } from '@home/components/widget/lib/flot-widget'; |
|||
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; |
|||
import { DataKey } from '@shared/models/widget.models'; |
|||
import { formatNumberValue, formatValue, isDefined, isNumeric } from '@core/utils'; |
|||
import { map } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aggregated-value-card-widget', |
|||
templateUrl: './aggregated-value-card-widget.component.html', |
|||
styleUrls: ['./aggregated-value-card-widget.component.scss'] |
|||
}) |
|||
export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit { |
|||
|
|||
@ViewChild('chartElement', {static: false}) chartElement: ElementRef; |
|||
|
|||
aggregatedValueCardKeyPosition = AggregatedValueCardKeyPosition; |
|||
|
|||
settings: AggregatedValueCardWidgetSettings; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
widgetTitlePanel: TemplateRef<any>; |
|||
|
|||
showSubtitle = true; |
|||
subtitle$: Observable<string>; |
|||
subtitleStyle: ComponentStyle = {}; |
|||
subtitleColor: ColorProcessor; |
|||
|
|||
showValues = false; |
|||
|
|||
values: {[key: string]: AggregatedValueCardValue} = {}; |
|||
|
|||
showChart = true; |
|||
chartColor: ColorProcessor; |
|||
|
|||
showDate = true; |
|||
dateFormat: DateFormatProcessor; |
|||
dateStyle: ComponentStyle = {}; |
|||
dateColor: ColorProcessor; |
|||
|
|||
backgroundStyle: ComponentStyle = {}; |
|||
overlayStyle: ComponentStyle = {}; |
|||
|
|||
private flot: TbFlot; |
|||
private flotDataKey: DataKey; |
|||
|
|||
private lastUpdateTs: number; |
|||
|
|||
tickMin$: Observable<string>; |
|||
tickMax$: Observable<string>; |
|||
|
|||
constructor(private date: DatePipe, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.aggregatedValueCardWidget = this; |
|||
this.settings = {...aggregatedValueCardDefaultSettings, ...this.ctx.settings}; |
|||
this.showSubtitle = this.settings.showSubtitle; |
|||
const subtitle = this.settings.subtitle; |
|||
this.subtitle$ = this.ctx.registerLabelPattern(subtitle, this.subtitle$); |
|||
this.subtitleStyle = textStyle(this.settings.subtitleFont, '0.25px'); |
|||
this.subtitleColor = ColorProcessor.fromSettings(this.settings.subtitleColor); |
|||
|
|||
const dataKey = getDataKey(this.ctx.defaultSubscription.datasources); |
|||
if (dataKey?.name && this.ctx.defaultSubscription.firstDatasource?.latestDataKeys?.length) { |
|||
const dataKeys = this.ctx.defaultSubscription.firstDatasource?.latestDataKeys; |
|||
for (const position of Object.keys(AggregatedValueCardKeyPosition)) { |
|||
const value = computeAggregatedCardValue(dataKeys, dataKey?.name, AggregatedValueCardKeyPosition[position]); |
|||
if (value) { |
|||
this.values[position] = value; |
|||
} |
|||
} |
|||
this.showValues = !!Object.keys(this.values).length; |
|||
} |
|||
|
|||
this.showChart = this.settings.showChart; |
|||
this.chartColor = ColorProcessor.fromSettings(this.settings.chartColor); |
|||
if (this.showChart) { |
|||
if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { |
|||
this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; |
|||
this.flotDataKey.settings = { |
|||
fillLines: false, |
|||
showLines: true, |
|||
lineWidth: 2 |
|||
} as TbFlotKeySettings; |
|||
this.flotDataKey.color = this.chartColor.color; |
|||
} |
|||
} |
|||
|
|||
this.showDate = this.settings.showDate; |
|||
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat); |
|||
this.dateStyle = textStyle(this.settings.dateFont, '0.25px'); |
|||
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor); |
|||
|
|||
this.backgroundStyle = backgroundStyle(this.settings.background); |
|||
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
if (this.showChart) { |
|||
const settings = { |
|||
shadowSize: 0, |
|||
smoothLines: false, |
|||
grid: { |
|||
tickColor: 'rgba(0,0,0,0.12)', |
|||
horizontalLines: true, |
|||
verticalLines: false, |
|||
outlineWidth: 0, |
|||
minBorderMargin: 0, |
|||
margin: 0 |
|||
}, |
|||
yaxis: { |
|||
showLabels: false, |
|||
tickGenerator: 'return [(axis.max + axis.min) / 2];' |
|||
}, |
|||
xaxis: { |
|||
showLabels: false |
|||
} |
|||
} as TbFlotSettings; |
|||
this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); |
|||
this.tickMin$ = this.flot.yMin$.pipe( |
|||
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), |
|||
(this.flotDataKey?.units || this.ctx.units)) |
|||
)); |
|||
this.tickMax$ = this.flot.yMax$.pipe( |
|||
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals), |
|||
(this.flotDataKey?.units || this.ctx.units)) |
|||
)); |
|||
} |
|||
} |
|||
|
|||
public onInit() { |
|||
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
|||
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
const tsValue = getLatestSingleTsValue(this.ctx.data); |
|||
let ts; |
|||
let value; |
|||
if (tsValue) { |
|||
ts = tsValue[0]; |
|||
value = tsValue[1]; |
|||
} |
|||
this.subtitleColor.update(value); |
|||
this.dateColor.update(value); |
|||
|
|||
if (this.showChart) { |
|||
this.chartColor.update(value); |
|||
this.flot.updateSeriesColor(this.chartColor.color); |
|||
this.flot.update(); |
|||
} |
|||
|
|||
this.updateLastUpdateTs(ts); |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
public onLatestDataUpdated() { |
|||
if (this.showValues) { |
|||
for (const aggValue of Object.values(this.values)) { |
|||
const tsValue = getTsValueByLatestDataKey(this.ctx.latestData, aggValue.key); |
|||
let ts; |
|||
let value; |
|||
if (tsValue) { |
|||
ts = tsValue[0]; |
|||
value = tsValue[1]; |
|||
aggValue.value = formatValue(value, (aggValue.key.decimals || this.ctx.decimals), null, false); |
|||
} else { |
|||
aggValue.value = 'N/A'; |
|||
} |
|||
const numeric = formatNumberValue(value, (aggValue.key.decimals || this.ctx.decimals)); |
|||
aggValue.color.update(numeric); |
|||
if (aggValue.showArrow && isDefined(numeric)) { |
|||
aggValue.upArrow = numeric > 0; |
|||
aggValue.downArrow = numeric < 0; |
|||
} else { |
|||
aggValue.upArrow = aggValue.downArrow = false; |
|||
} |
|||
this.updateLastUpdateTs(ts); |
|||
} |
|||
this.cd.detectChanges(); |
|||
} |
|||
} |
|||
|
|||
public onResize() { |
|||
if (this.showChart) { |
|||
this.flot.resize(); |
|||
} |
|||
} |
|||
|
|||
public onEditModeChanged() { |
|||
if (this.showChart) { |
|||
this.flot.checkMouseEvents(); |
|||
} |
|||
} |
|||
|
|||
public onDestroy() { |
|||
if (this.showChart) { |
|||
this.flot.destroy(); |
|||
} |
|||
} |
|||
|
|||
private updateLastUpdateTs(ts: number) { |
|||
if (ts && (!this.lastUpdateTs || ts > this.lastUpdateTs)) { |
|||
this.lastUpdateTs = ts; |
|||
this.dateFormat.update(ts); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,232 @@ |
|||
///
|
|||
/// 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 { |
|||
BackgroundSettings, |
|||
BackgroundType, |
|||
ColorProcessor, |
|||
ColorSettings, |
|||
ColorType, |
|||
ComponentStyle, |
|||
constantColor, |
|||
DateFormatSettings, |
|||
Font, |
|||
iconStyle, |
|||
lastUpdateAgoDateFormat, |
|||
textStyle |
|||
} from '@shared/models/widget-settings.models'; |
|||
import { ComparisonResultType, DataKey, DatasourceData } from '@shared/models/widget.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { AggregationType } from '@shared/models/time/time.models'; |
|||
|
|||
export interface AggregatedValueCardWidgetSettings { |
|||
showSubtitle: boolean; |
|||
subtitle: string; |
|||
subtitleFont: Font; |
|||
subtitleColor: ColorSettings; |
|||
showDate: boolean; |
|||
dateFormat: DateFormatSettings; |
|||
dateFont: Font; |
|||
dateColor: ColorSettings; |
|||
showChart: boolean; |
|||
chartColor: ColorSettings; |
|||
background: BackgroundSettings; |
|||
} |
|||
|
|||
export enum AggregatedValueCardKeyPosition { |
|||
center = 'center', |
|||
rightTop = 'rightTop', |
|||
rightBottom = 'rightBottom', |
|||
leftTop = 'leftTop', |
|||
leftBottom = 'leftBottom' |
|||
} |
|||
|
|||
export const aggregatedValueCardKeyPositionTranslations = new Map<AggregatedValueCardKeyPosition, string>( |
|||
[ |
|||
[AggregatedValueCardKeyPosition.center, 'widgets.aggregated-value-card.position-center'], |
|||
[AggregatedValueCardKeyPosition.rightTop, 'widgets.aggregated-value-card.position-right-top'], |
|||
[AggregatedValueCardKeyPosition.rightBottom, 'widgets.aggregated-value-card.position-right-bottom'], |
|||
[AggregatedValueCardKeyPosition.leftTop, 'widgets.aggregated-value-card.position-left-top'], |
|||
[AggregatedValueCardKeyPosition.leftBottom, 'widgets.aggregated-value-card.position-left-bottom'] |
|||
] |
|||
); |
|||
|
|||
export interface AggregatedValueCardKeySettings { |
|||
position: AggregatedValueCardKeyPosition; |
|||
font: Font; |
|||
color: ColorSettings; |
|||
showArrow: boolean; |
|||
} |
|||
|
|||
export interface AggregatedValueCardValue { |
|||
key: DataKey; |
|||
value: string; |
|||
units: string; |
|||
style: ComponentStyle; |
|||
color: ColorProcessor; |
|||
center: boolean; |
|||
showArrow: boolean; |
|||
upArrow: boolean; |
|||
downArrow: boolean; |
|||
} |
|||
|
|||
export const computeAggregatedCardValue = (dataKeys: DataKey[], keyName: string, position: AggregatedValueCardKeyPosition): AggregatedValueCardValue => { |
|||
const key = dataKeys.find(dataKey => ( dataKey.name === keyName && (dataKey.settings?.position === position || |
|||
(!dataKey.settings?.position && position === AggregatedValueCardKeyPosition.center)) )); |
|||
if (key) { |
|||
const settings: AggregatedValueCardKeySettings = key.settings; |
|||
return { |
|||
key, |
|||
value: '', |
|||
units: key.units, |
|||
style: textStyle(settings.font, '0.25px'), |
|||
color: ColorProcessor.fromSettings(settings.color), |
|||
center: position === AggregatedValueCardKeyPosition.center, |
|||
showArrow: settings.showArrow, |
|||
upArrow: false, |
|||
downArrow: false |
|||
}; |
|||
} |
|||
}; |
|||
|
|||
export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dataKey: DataKey): [number, any] => { |
|||
if (latestData?.length) { |
|||
const dsData = latestData.find(data => data.dataKey === dataKey); |
|||
if (dsData?.data?.length) { |
|||
return dsData.data[0]; |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
export const aggregatedValueCardDefaultSettings: AggregatedValueCardWidgetSettings = { |
|||
showSubtitle: true, |
|||
subtitle: '${entityName}', |
|||
subtitleFont: { |
|||
family: 'Roboto', |
|||
size: 12, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '400', |
|||
lineHeight: '16px' |
|||
}, |
|||
subtitleColor: constantColor('rgba(0, 0, 0, 0.38)'), |
|||
showDate: true, |
|||
dateFormat: lastUpdateAgoDateFormat(), |
|||
dateFont: { |
|||
family: 'Roboto', |
|||
size: 12, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '400', |
|||
lineHeight: '16px' |
|||
}, |
|||
dateColor: constantColor('rgba(0, 0, 0, 0.38)'), |
|||
showChart: true, |
|||
chartColor: constantColor('rgba(0, 0, 0, 0.87)'), |
|||
background: { |
|||
type: BackgroundType.color, |
|||
color: '#fff', |
|||
overlay: { |
|||
enabled: false, |
|||
color: 'rgba(255,255,255,0.72)', |
|||
blur: 3 |
|||
} |
|||
} |
|||
}; |
|||
|
|||
export const aggregatedValueCardDefaultKeySettings: AggregatedValueCardKeySettings = { |
|||
position: AggregatedValueCardKeyPosition.center, |
|||
font: { |
|||
family: 'Roboto', |
|||
size: 14, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '500', |
|||
lineHeight: '1' |
|||
}, |
|||
color: constantColor('rgba(0, 0, 0, 0.87)'), |
|||
showArrow: false |
|||
}; |
|||
|
|||
export const createDefaultAggregatedValueLatestDataKeys = (keyName: string, units): DataKey[] => [ |
|||
{ |
|||
name: keyName, label: keyName, type: DataKeyType.timeseries, units, decimals: 0, |
|||
aggregationType: AggregationType.NONE, |
|||
settings: { |
|||
position: AggregatedValueCardKeyPosition.center, |
|||
font: { |
|||
family: 'Roboto', |
|||
size: 52, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '500', |
|||
lineHeight: '1' |
|||
}, |
|||
color: constantColor('rgba(0, 0, 0, 0.87)'), |
|||
showArrow: false |
|||
} as AggregatedValueCardKeySettings |
|||
}, |
|||
{ |
|||
name: keyName, label: 'Delta percent ' + keyName, type: DataKeyType.timeseries, units: '%', decimals: 0, |
|||
aggregationType: AggregationType.AVG, |
|||
comparisonEnabled: true, |
|||
timeForComparison: 'previousInterval', |
|||
comparisonResultType: ComparisonResultType.DELTA_PERCENT, |
|||
settings: { |
|||
position: AggregatedValueCardKeyPosition.rightTop, |
|||
font: { |
|||
family: 'Roboto', |
|||
size: 14, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '500', |
|||
lineHeight: '1' |
|||
}, |
|||
color: { |
|||
color: 'rgba(0, 0, 0, 0.87)', |
|||
type: ColorType.range, |
|||
rangeList: [ |
|||
{to: 0, color: '#198038'}, |
|||
{from: 0, to: 0, color: 'rgba(0, 0, 0, 0.87)'}, |
|||
{from: 0, color: '#D12730'} |
|||
], |
|||
colorFunction: '' |
|||
}, |
|||
showArrow: true |
|||
} as AggregatedValueCardKeySettings |
|||
}, |
|||
{ |
|||
name: keyName, label: 'Delta absolute ' + keyName, type: DataKeyType.timeseries, units, decimals: 1, |
|||
aggregationType: AggregationType.AVG, |
|||
comparisonEnabled: true, |
|||
timeForComparison: 'previousInterval', |
|||
comparisonResultType: ComparisonResultType.DELTA_ABSOLUTE, |
|||
settings: { |
|||
position: AggregatedValueCardKeyPosition.rightBottom, |
|||
font: { |
|||
family: 'Roboto', |
|||
size: 11, |
|||
sizeUnit: 'px', |
|||
style: 'normal', |
|||
weight: '400', |
|||
lineHeight: '1' |
|||
}, |
|||
color: constantColor('rgba(0, 0, 0, 0.38)'), |
|||
showArrow: false |
|||
} as AggregatedValueCardKeySettings |
|||
} |
|||
]; |
|||
@ -0,0 +1,48 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="aggregatedValueCardKeySettingsForm"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widgets.aggregated-value-card.value-appearance</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.aggregated-value-card.position' | translate }}</div> |
|||
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="position"> |
|||
<mat-option *ngFor="let position of aggregatedValueCardKeyPositions" [value]="position"> |
|||
{{ aggregatedValueCardKeyPositionTranslationMap.get(position) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.aggregated-value-card.font' | translate }}</div> |
|||
<tb-font-settings formControlName="font" |
|||
previewText="22 °C"> |
|||
</tb-font-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.aggregated-value-card.color' | translate }}</div> |
|||
<tb-color-settings formControlName="color"> |
|||
</tb-color-settings> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="showArrow"> |
|||
{{ 'widgets.aggregated-value-card.display-up-down-arrow' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
@ -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 } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
aggregatedValueCardDefaultKeySettings, |
|||
AggregatedValueCardKeyPosition, |
|||
aggregatedValueCardKeyPositionTranslations |
|||
} from '@home/components/widget/lib/cards/aggregated-value-card.models'; |
|||
import { constantColor } from '@shared/models/widget-settings.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-aggregated-value-card-key-settings', |
|||
templateUrl: './aggregated-value-card-key-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'] |
|||
}) |
|||
export class AggregatedValueCardKeySettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
aggregatedValueCardKeyPositions: AggregatedValueCardKeyPosition[] = |
|||
Object.keys(AggregatedValueCardKeyPosition).map(value => AggregatedValueCardKeyPosition[value]); |
|||
|
|||
aggregatedValueCardKeyPositionTranslationMap = aggregatedValueCardKeyPositionTranslations; |
|||
|
|||
aggregatedValueCardKeySettingsForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.aggregatedValueCardKeySettingsForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return {...aggregatedValueCardDefaultKeySettings}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.aggregatedValueCardKeySettingsForm = this.fb.group({ |
|||
position: [settings.position, []], |
|||
font: [settings.font, []], |
|||
color: [settings.color, []], |
|||
showArrow: [settings.showArrow, []] |
|||
}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue