Browse Source

UI: Aggregated value card widget

pull/9091/head
Igor Kulikov 3 years ago
parent
commit
ffe3acd92d
  1. 8
      ui-ngx/src/app/core/auth/auth.service.ts
  2. 12
      ui-ngx/src/app/core/utils.ts
  3. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  4. 94
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html
  5. 88
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss
  6. 229
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  7. 51
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html
  8. 69
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss
  9. 173
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts
  10. 136
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html
  11. 291
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts
  12. 1
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  13. 9
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  15. 13
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  16. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  17. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  18. 5
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html
  19. 3
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts
  20. 59
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  21. 90
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html
  22. 156
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss
  23. 260
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  24. 232
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts
  25. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  26. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
  27. 1
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  28. 46
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  29. 48
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html
  30. 64
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts
  31. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts
  32. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html
  33. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  34. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  35. 9
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  36. 10
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  37. 60
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  38. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  39. 27
      ui-ngx/src/form.scss

8
ui-ngx/src/app/core/auth/auth.service.ts

@ -385,10 +385,10 @@ export class AuthService {
} else if (authPayload.authUser) {
authPayload.authUser.authority = Authority.ANONYMOUS;
}
if (authPayload.authUser.isPublic) {
if (authPayload.authUser?.isPublic) {
authPayload.forceFullscreen = true;
}
if (authPayload.authUser.isPublic) {
if (authPayload.authUser?.isPublic) {
this.loadSystemParams().subscribe(
(sysParams) => {
authPayload = {...authPayload, ...sysParams};
@ -399,10 +399,10 @@ export class AuthService {
loadUserSubject.error(err);
}
);
} else if (authPayload.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
} else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) {
loadUserSubject.next(authPayload);
loadUserSubject.complete();
} else if (authPayload.authUser.userId) {
} else if (authPayload.authUser?.userId) {
this.userService.getUser(authPayload.authUser.userId).subscribe(
(user) => {
authPayload.userDetails = user;

12
ui-ngx/src/app/core/utils.ts

@ -130,7 +130,7 @@ export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object);
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => {
if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {
let formatted: string | number = Number(value);
@ -150,6 +150,16 @@ export function formatValue(value: any, dec?: number, units?: string, showZeroDe
}
}
export const formatNumberValue = (value: any, dec?: number): number | undefined => {
if (isDefinedAndNotNull(value) && isNumeric(value)) {
let formatted: string | number = Number(value);
if (isDefinedAndNotNull(dec)) {
formatted = formatted.toFixed(dec);
}
return Number(formatted);
}
}
export function objectValues(obj: any): any[] {
return Object.keys(obj).map(e => obj[e]);
}

18
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -40,6 +40,15 @@ import {
import {
ValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/value-card-basic-config.component';
import {
AggregatedValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component';
import {
AggregatedDataKeyRowComponent
} from '@home/components/widget/config/basic/cards/aggregated-data-key-row.component';
import {
AggregatedDataKeysPanelComponent
} from '@home/components/widget/config/basic/cards/aggregated-data-keys-panel.component';
@NgModule({
declarations: [
@ -50,6 +59,9 @@ import {
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
AggregatedValueCardBasicConfigComponent,
AggregatedDataKeyRowComponent,
AggregatedDataKeysPanelComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
@ -66,6 +78,9 @@ import {
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
AggregatedValueCardBasicConfigComponent,
AggregatedDataKeyRowComponent,
AggregatedDataKeysPanelComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
@ -79,5 +94,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent,
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent,
'tb-value-card-basic-config': ValueCardBasicConfigComponent
'tb-value-card-basic-config': ValueCardBasicConfigComponent,
'tb-aggregated-value-card-basic-config': AggregatedValueCardBasicConfigComponent
};

94
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html

@ -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>

88
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.scss

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

229
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

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

51
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>

69
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.scss

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

173
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts

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

136
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html

@ -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>

291
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts

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

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

@ -45,6 +45,7 @@
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
[hideDataKeyName]="data.hideDataKeyName"
[hideDataKeyLabel]="data.hideDataKeyLabel"
[hideDataKeyColor]="data.hideDataKeyColor"
[hideDataKeyUnits]="data.hideDataKeyUnits"

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

@ -50,10 +50,11 @@ export interface DataKeyConfigDialogData {
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;
hideDataKeyLabel: boolean;
hideDataKeyColor: boolean;
hideDataKeyUnits: boolean;
hideDataKeyDecimals: boolean;
hideDataKeyName?: boolean;
hideDataKeyLabel?: boolean;
hideDataKeyColor?: boolean;
hideDataKeyUnits?: boolean;
hideDataKeyDecimals?: boolean;
}
@Component({

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

@ -20,7 +20,7 @@
<div class="mat-padding" fxLayout="column" style="gap: 16px;">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>datakey.general</div>
<mat-form-field *ngIf="modelValue.type !== dataKeyTypes.function" subscriptSizing="dynamic">
<mat-form-field *ngIf="modelValue.type !== dataKeyTypes.function && !hideDataKeyName" subscriptSizing="dynamic">
<mat-label>{{ 'entity.key' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}"
#keyInput

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

@ -126,6 +126,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
showPostProcessing = true;
@Input()
@coerceBoolean()
hideDataKeyName = false;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@ -144,8 +148,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit') postFuncBodyEdit: JsFuncComponent;
@ViewChild('funcBodyEdit', {static: false}) funcBodyEdit: JsFuncComponent;
@ViewChild('postFuncBodyEdit', {static: false}) postFuncBodyEdit: JsFuncComponent;
hasAdvanced = false;
@ -438,10 +442,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
public validateOnSubmit() {
if (this.modelValue.type === DataKeyType.function) {
if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) {
this.funcBodyEdit.validateOnSubmit();
} else if ((this.modelValue.type === DataKeyType.timeseries ||
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) {
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value &&
this.postFuncBodyEdit) {
this.postFuncBodyEdit.validateOnSubmit();
}
}

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

@ -88,7 +88,7 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
public get hasAdditionalLatestDataKeys(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys && !this.hideLatestDataKeys;
}
public get dataKeysOptional(): boolean {
@ -143,6 +143,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideDataKeys;
}
public get hideLatestDataKeys(): boolean {
return this.datasourcesComponent?.hideLatestDataKeys;
}
@Input()
disabled: boolean;

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

@ -113,6 +113,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean()
hideDataKeys = false;
@Input()
@coerceBoolean()
hideLatestDataKeys = false;
@Input()
@coerceBoolean()
forceSingleDatasource = false;

5
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html

@ -65,6 +65,11 @@
asBoxInput>
</tb-color-input>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="displayTypePrefix">
{{ 'timewindow.displayTypePrefix' | translate }}
</mat-slide-toggle>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding timewindow-preview">
<div class="fixed-title-width" translate>timewindow.preview</div>

3
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts

@ -63,7 +63,8 @@ export class TimewindowStylePanelComponent extends PageComponent implements OnIn
icon: [computedTimewindowStyle.icon, []],
iconPosition: [computedTimewindowStyle.iconPosition, []],
font: [computedTimewindowStyle.font, []],
color: [computedTimewindowStyle.color, []]
color: [computedTimewindowStyle.color, []],
displayTypePrefix: [computedTimewindowStyle.displayTypePrefix, []]
}
);
this.updatePreviewStyle(this.timewindowStyle);

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

@ -119,7 +119,7 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.configForm().valid;
}
protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) {
protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[], latestKeys?: DataKey[]) {
let datasources = configData.config.datasources;
if (!datasources || !datasources.length) {
datasources = [
@ -135,23 +135,58 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
dataKeys = [];
datasources[0].dataKeys = dataKeys;
}
let latestDataKeys = datasources[0].latestDataKeys;
if (!latestDataKeys) {
latestDataKeys = [];
datasources[0].latestDataKeys = latestDataKeys;
}
if (keys && keys.length) {
dataKeys.length = 0;
keys.forEach(key => {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
if (key.label) {
dataKey.label = key.label;
}
if (key.units) {
dataKey.units = key.units;
}
if (isDefinedAndNotNull(key.decimals)) {
dataKey.decimals = key.decimals;
}
const dataKey = this.constructDataKey(configData, key);
dataKeys.push(dataKey);
});
}
if (latestKeys && latestKeys.length) {
latestDataKeys.length = 0;
latestKeys.forEach(key => {
const dataKey = this.constructDataKey(configData, key);
latestDataKeys.push(dataKey);
});
}
}
protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
if (key.label) {
dataKey.label = key.label;
}
if (key.units) {
dataKey.units = key.units;
}
if (isDefinedAndNotNull(key.decimals)) {
dataKey.decimals = key.decimals;
}
if (isDefinedAndNotNull(key.settings)) {
dataKey.settings = key.settings;
}
if (isDefinedAndNotNull(key.aggregationType)) {
dataKey.aggregationType = key.aggregationType;
}
if (isDefinedAndNotNull(key.comparisonEnabled)) {
dataKey.comparisonEnabled = key.comparisonEnabled;
}
if (isDefinedAndNotNull(key.timeForComparison)) {
dataKey.timeForComparison = key.timeForComparison;
}
if (isDefinedAndNotNull(key.comparisonCustomIntervalValue)) {
dataKey.comparisonCustomIntervalValue = key.comparisonCustomIntervalValue;
}
if (isDefinedAndNotNull(key.comparisonResultType)) {
dataKey.comparisonResultType = key.comparisonResultType;
}
return dataKey;
}
protected abstract configForm(): UntypedFormGroup;

90
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html

@ -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>

156
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss

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

260
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts

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

232
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.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
}
];

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html

@ -66,7 +66,7 @@
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label$ | async }}</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color" [innerHTML]="dateFormat.formatted"></div>
</ng-template>
<ng-template #valueTpl>
<div class="tb-value-card-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts

@ -83,7 +83,7 @@ export function loadNodeCtxFunction<F extends (...args: any[]) => any>(functionB
}
export function materialIconHtml(materialIcon: string): string {
return '<mat-icon class="node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>';
return '<mat-icon class="mat-icon node-icon material-icons" role="img" aria-hidden="false">' + materialIcon + '</mat-icon>';
}
export function iconUrlHtml(iconUrl: string): string {

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

@ -132,6 +132,7 @@ export interface TbFlotYAxisSettings {
ticksFormatter: string;
tickDecimals: number;
tickSize: number;
tickGenerator: string;
}
export interface TbFlotBaseSettings {

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

@ -18,7 +18,8 @@
import { WidgetContext } from '@home/models/widget-component.models';
import {
createLabelFromDatasource,
deepClone, formattedDataFormDatasourceData,
deepClone,
formattedDataFormDatasourceData,
insertVariable,
isDefined,
isDefinedAndNotNull,
@ -59,6 +60,7 @@ import { AggregationType } from '@shared/models/time/time.models';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject } from 'rxjs';
import Timeout = NodeJS.Timeout;
const moment = moment_;
@ -130,9 +132,15 @@ export class TbFlot {
private pieAnimationLastTime: number;
private pieAnimationCaf: CancelAnimationFrame;
constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>) {
private yMinSubject = new BehaviorSubject(-1);
private yMaxSubject = new BehaviorSubject(1);
yMin$ = this.yMinSubject.asObservable();
yMax$ = this.yMaxSubject.asObservable();
constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>, settings?: TbFlotSettings) {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
this.settings = settings || (ctx.settings as TbFlotSettings);
this.utils = this.ctx.$injector.get(UtilsService);
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
this.selectionMode = this.enableSelection ? 'x' : null;
@ -209,6 +217,12 @@ export class TbFlot {
} else {
this.yaxis.tickSize = null;
}
if (this.settings.yaxis.tickGenerator?.length) {
try {
this.yaxis.ticks = new Function('axis',
this.settings.yaxis.tickGenerator);
} catch (e) {}
}
if (isNumber(this.settings.yaxis.tickDecimals)) {
this.yaxis.tickDecimals = this.settings.yaxis.tickDecimals;
} else {
@ -717,6 +731,15 @@ export class TbFlot {
}
}
public updateSeriesColor(color: string) {
if (this.subscription?.data?.length) {
const series = this.subscription.data[0] as TbFlotSeries;
series.dataKey.color = color;
series.color = color;
series.highlightColor = tinycolor(color).setAlpha(.75).toRgbString();
}
}
private latestDataByDataIndex(index: number): FormattedData {
if (this.latestData[index]) {
return this.latestData[index];
@ -802,6 +825,8 @@ export class TbFlot {
clearTimeout(this.resizeTimeoutHandle);
this.resizeTimeoutHandle = null;
}
this.yMinSubject.complete();
this.yMaxSubject.complete();
}
private createPlot() {
@ -818,6 +843,7 @@ export class TbFlot {
} else {
this.plot = $.plot(this.$element, this.subscription.data, this.options) as JQueryPlot;
}
this.updateYMinMax();
} else {
this.createPlotTimeoutHandle = setTimeout(this.createPlot.bind(this), 30);
}
@ -830,6 +856,20 @@ export class TbFlot {
this.plot.setupGrid();
}
this.plot.draw();
this.updateYMinMax();
}
private updateYMinMax() {
if (this.plot?.getYAxes().length) {
const min = this.plot?.getYAxes()[0].min;
const max = this.plot?.getYAxes()[0].max;
if (this.yMinSubject.value !== min) {
this.yMinSubject.next(min);
}
if (this.yMaxSubject.value !== max) {
this.yMaxSubject.next(max);
}
}
}
private redrawPlot() {

48
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html

@ -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>

64
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.ts

@ -0,0 +1,64 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } 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, []]
});
}
}

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

@ -74,7 +74,8 @@ export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSetting
color: null,
tickSize: null,
tickDecimals: 0,
ticksFormatter: ''
ticksFormatter: '',
tickGenerator: ''
}
};
if (chartType === 'graph') {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html

@ -21,7 +21,7 @@
[disabled]="disabled"
#matButton
(click)="openColorSettingsPopup($event, matButton)">
<tb-icon matButtonIcon *ngIf="modelValue.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
<tb-icon matButtonIcon *ngIf="modelValue?.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
</button>
<ng-template #colorPreview>
<div class="tb-color-preview box" [ngClass]="{'disabled': disabled}">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts

@ -99,7 +99,7 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
}
private updateColorStyle() {
if (!this.disabled) {
if (!this.disabled && this.modelValue) {
let colors: string[] = [this.modelValue.color];
if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) {
const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color);

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

@ -267,6 +267,9 @@ import {
ValueCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import {
AggregatedValueCardKeySettingsComponent
} from '@home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component';
@NgModule({
declarations: [
@ -366,7 +369,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
ValueCardWidgetSettingsComponent
ValueCardWidgetSettingsComponent,
AggregatedValueCardKeySettingsComponent
],
imports: [
CommonModule,
@ -471,7 +475,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
ValueCardWidgetSettingsComponent
ValueCardWidgetSettingsComponent,
AggregatedValueCardKeySettingsComponent
]
})
export class WidgetSettingsModule {
@ -541,5 +546,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent,
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent,
'tb-aggregated-value-card-key-settings': AggregatedValueCardKeySettingsComponent,
};

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

@ -44,6 +44,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/value-card-widget.component';
import {
AggregatedValueCardWidgetComponent
} from '@home/components/widget/lib/cards/aggregated-value-card-widget.component';
@NgModule({
declarations:
@ -68,7 +71,8 @@ import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/valu
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
ValueCardWidgetComponent
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
imports: [
CommonModule,
@ -97,7 +101,8 @@ import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/valu
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
ValueCardWidgetComponent
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

10
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -89,6 +89,11 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
return this.historyOnlyValue;
}
get displayTypePrefix(): boolean {
return isDefinedAndNotNull(this.computedTimewindowStyle?.displayTypePrefix)
? this.computedTimewindowStyle?.displayTypePrefix : true;
}
@HostBinding('class.no-margin')
@Input()
@coerceBoolean()
@ -198,6 +203,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'timewindowStyle') {
this.updateTimewindowStyle();
this.updateDisplayValue();
}
}
}
@ -316,7 +322,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
updateDisplayValue() {
if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) {
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ';
this.innerValue.displayValue = this.displayTypePrefix ? (this.translate.instant('timewindow.realtime') + ' - ') : '';
if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval));
} else {
@ -324,7 +330,7 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
}
} else {
this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ?
this.innerValue.displayValue = this.displayTypePrefix && (!this.historyOnly || this.alwaysDisplayTypePrefix) ?
(this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +

60
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -17,7 +17,7 @@
import { isDefinedAndNotNull, isNumber, isNumeric, parseFunction } from '@core/utils';
import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe, formatDate } from '@angular/common';
import { DatePipe } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { TranslateService } from '@ngx-translate/core';
@ -97,13 +97,15 @@ export interface TimewindowStyle {
iconPosition: 'left' | 'right';
font?: Font;
color?: string;
displayTypePrefix?: boolean;
}
export const defaultTimewindowStyle: TimewindowStyle = {
showIcon: true,
icon: 'query_builder',
iconSize: '24px',
iconPosition: 'left'
iconPosition: 'left',
displayTypePrefix: true
};
export const constantColor = (color: string): ColorSettings => ({
@ -117,18 +119,44 @@ export const constantColor = (color: string): ColorSettings => ({
'return \'blue\';'
});
export const cssSizeToStrSize = (size?: number, unit?: cssUnit): string => (isDefinedAndNotNull(size) ? size + '' : '0') + (unit || 'px');
export const resolveCssSize = (strSize?: string): [number, cssUnit] => {
if (!strSize || !strSize.trim().length) {
return [0, 'px'];
}
let resolvedUnit: cssUnit;
let resolvedSize = strSize;
for (const unit of cssUnits) {
if (strSize.endsWith(unit)) {
resolvedUnit = unit;
break;
}
}
if (resolvedUnit) {
resolvedSize = strSize.substring(0, strSize.length - resolvedUnit.length);
}
resolvedUnit = resolvedUnit || 'px';
let numericSize = 0;
if (isNumeric(resolvedSize)) {
numericSize = Number(resolvedSize);
}
return [numericSize, resolvedUnit];
};
type ValueColorFunction = (value: any) => string;
export abstract class ColorProcessor {
static fromSettings(color: ColorSettings): ColorProcessor {
switch (color.type) {
const settings = color || constantColor('rgba(0, 0, 0, 0.87)');
switch (settings.type) {
case ColorType.constant:
return new ConstantColorProcessor(color);
return new ConstantColorProcessor(settings);
case ColorType.range:
return new RangeColorProcessor(color);
return new RangeColorProcessor(settings);
case ColorType.function:
return new FunctionColorProcessor(color);
return new FunctionColorProcessor(settings);
}
}
@ -164,13 +192,19 @@ class RangeColorProcessor extends ColorProcessor {
if (this.settings.rangeList?.length && isDefinedAndNotNull(value) && isNumeric(value)) {
const num = Number(value);
for (const range of this.settings.rangeList) {
if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) {
if (this.constantRange(range) && range.from === num) {
return range.color;
} else if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) {
return range.color;
}
}
}
return this.settings.color;
}
private constantRange(range: ColorRange): boolean {
return isNumber(range.from) && isNumber(range.to) && range.from === range.to;
}
}
class FunctionColorProcessor extends ColorProcessor {
@ -242,7 +276,7 @@ export abstract class DateFormatProcessor {
}
}
formatted = '';
formatted = '&nbsp;';
protected constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
@ -412,3 +446,13 @@ export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] =>
}
return null;
};
export const getLatestSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
if (data.length) {
const dsData = data[0];
if (dsData.data.length) {
return dsData.data[dsData.data.length - 1];
}
}
return null;
};

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

@ -1208,7 +1208,11 @@
"delta-calculation-result-delta-absolute": "Delta (absolute)",
"delta-calculation-result-delta-percent": "Delta (percent)",
"source": "Source",
"latest": "Latest"
"latest": "Latest",
"latest-value": "Latest value",
"delta": "delta",
"percent": "percent",
"absolute": "absolute"
},
"datasource": {
"type": "Datasource type",
@ -3911,6 +3915,7 @@
"icon-position-right": "Right",
"font": "Font",
"color": "Color",
"displayTypePrefix": "Display Realtime/History prefix",
"preview": "Preview"
},
"unit": {
@ -5718,6 +5723,25 @@
"date": "Date",
"value-card-style": "Value card style"
},
"aggregated-value-card": {
"subtitle": "Subtitle",
"chart": "Chart",
"values": "Values",
"value-appearance": "Value appearance",
"position": "Position",
"position-center": "Center",
"position-right-top": "Right top",
"position-right-bottom": "Right bottom",
"position-left-top": "Left top",
"position-left-bottom": "Left bottom",
"font": "Font",
"color": "Color",
"display-up-down-arrow": "Display Up/Down arrow",
"add-value": "Add value",
"remove-value": "Remove value",
"no-values": "No values configured",
"aggregation": "Aggregation"
},
"table": {
"common-table-settings": "Common Table Settings",
"enable-search": "Enable search",

27
ui-ngx/src/form.scss

@ -122,18 +122,6 @@
font: inherit;
}
}
.mat-slide {
margin: 0;
&.margin {
margin: 8px 0;
}
.mdc-form-field>label {
font-weight: 400;
font-size: 16px;
line-height: 24px;
margin-left: 12px;
}
}
}
.tb-form-panel-title {
@ -200,6 +188,21 @@
}
}
.tb-form-panel, .tb-form-row {
.mat-slide {
margin: 0;
&.margin {
margin: 8px 0;
}
.mdc-form-field>label {
font-weight: 400;
font-size: 16px;
line-height: 24px;
margin-left: 12px;
}
}
}
.tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field {
&.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) {

Loading…
Cancel
Save