UI: Value card basic config

This commit is contained in:
Igor Kulikov 2023-07-24 19:50:40 +03:00
parent 9d8a9943bf
commit c0309ac1aa
22 changed files with 780 additions and 31 deletions

View File

@ -55,5 +55,50 @@
</tb-color-settings>
</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 fxFlex appearance="outline" class="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
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.value-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input fxFlex formControlName="units"></tb-unit-input>
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
{{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle>
<div fxFlex 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>
</ng-container>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component } from '@angular/core';
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';
@ -28,8 +28,13 @@ import {
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { isDefinedAndNotNull, isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@home/components/widget/config/widget-settings.models';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
DateFormatProcessor,
DateFormatSettings,
getLabel,
setLabel
} from '@home/components/widget/config/widget-settings.models';
import {
valueCardDefaultSettings,
ValueCardLayout,
@ -65,9 +70,14 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
valueCardWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
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);
}
@ -251,4 +261,16 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.valueCardWidgetConfigForm.get('units').value;
const decimals: number = this.valueCardWidgetConfigForm.get('decimals').value;
return formatValue(22, decimals, units, true);
}
private _datePreviewFn(): string {
const dateFormat: DateFormatSettings = this.valueCardWidgetConfigForm.get('dateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

View File

@ -16,6 +16,10 @@
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 { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { TranslateService } from '@ngx-translate/core';
export type ComponentStyle = {[klass: string]: any};
@ -168,6 +172,104 @@ class FunctionColorProcessor extends ColorProcessor {
}
}
export interface DateFormatSettings {
format?: string;
lastUpdateAgo?: boolean;
custom?: boolean;
}
export const simpleDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: false
});
export const lastUpdateAgoDateFormat = (): DateFormatSettings => ({
format: null,
lastUpdateAgo: true,
custom: false
});
export const customDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: true
});
export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'yyyy MMM dd HH:mm',
'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'yyyy/MM/dd HH:mm:ss']
.map(f => simpleDateFormat(f)).concat([lastUpdateAgoDateFormat(), customDateFormat('EEE, MMMM dd, yyyy')]);
export const compareDateFormats = (df1: DateFormatSettings, df2: DateFormatSettings): boolean => {
if (df1 === df2) {
return true;
} else if (df1 && df2) {
if (df1.lastUpdateAgo && df2.lastUpdateAgo) {
return true;
} else if (df1.custom && df2.custom) {
return true;
} else if (!df1.lastUpdateAgo && !df2.lastUpdateAgo && !df1.custom && !df2.custom) {
return df1.format === df2.format;
}
}
return false;
};
export abstract class DateFormatProcessor {
static fromSettings($injector: Injector, settings: DateFormatSettings): DateFormatProcessor {
if (settings.lastUpdateAgo) {
return new LastUpdateAgoDateFormatProcessor($injector, settings);
} else {
return new SimpleDateFormatProcessor($injector, settings);
}
}
formatted = '';
protected constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
}
abstract update(ts: string | number | Date): void;
}
export class SimpleDateFormatProcessor extends DateFormatProcessor {
private datePipe: DatePipe;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.datePipe = $injector.get(DatePipe);
}
update(ts: string| number | Date): void {
this.formatted = this.datePipe.transform(ts, this.settings.format);
}
}
export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
private dateAgoPipe: DateAgoPipe;
private translate: TranslateService;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.dateAgoPipe = $injector.get(DateAgoPipe);
this.translate = $injector.get(TranslateService);
}
update(ts: string| number | Date): void {
this.formatted = this.translate.instant('date.last-update-n-ago-text',
{agoText: this.dateAgoPipe.transform(ts, {applyAgo: true, short: true, textPart: true})});
}
}
export enum BackgroundType {
image = 'image',
imageUrl = 'imageUrl',

View File

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

View File

@ -21,7 +21,7 @@ import { DatePipe } from '@angular/common';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
ComponentStyle, DateFormatProcessor,
getDataKey,
getLabel,
getSingleTsValue,
@ -62,7 +62,7 @@ export class ValueCardWidgetComponent implements OnInit {
valueColor: ColorProcessor;
showDate = true;
dateText = '';
dateFormat: DateFormatProcessor;
dateStyle: ComponentStyle = {};
dateColor: ColorProcessor;
@ -70,7 +70,6 @@ export class ValueCardWidgetComponent implements OnInit {
overlayStyle: ComponentStyle = {};
private horizontal = false;
private dateFormat: string;
private decimals = 0;
private units = '';
@ -110,7 +109,7 @@ export class ValueCardWidgetComponent implements OnInit {
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.showDate = this.settings.showDate;
this.dateFormat = this.settings.dateFormat;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat);
this.dateStyle = textStyle(this.settings.dateFont, '1.33', '0.25px');
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
@ -126,15 +125,16 @@ export class ValueCardWidgetComponent implements OnInit {
public onDataUpdated() {
const tsValue = getSingleTsValue(this.ctx.data);
let ts;
let value;
if (tsValue) {
ts = tsValue[0];
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, true);
this.dateText = this.date.transform(tsValue[0], this.dateFormat);
} else {
this.valueText = 'N/A';
this.dateText = '';
}
this.dateFormat.update(ts);
this.iconColor.update(value);
this.labelColor.update(value);
this.valueColor.update(value);

View File

@ -19,8 +19,8 @@ import {
BackgroundType,
ColorSettings,
constantColor,
cssUnit,
Font
cssUnit, DateFormatSettings,
Font, lastUpdateAgoDateFormat
} from '@home/components/widget/config/widget-settings.models';
export enum ValueCardLayout {
@ -75,7 +75,7 @@ export interface ValueCardWidgetSettings {
valueFont: Font;
valueColor: ColorSettings;
showDate: boolean;
dateFormat: string;
dateFormat: DateFormatSettings;
dateFont: Font;
dateColor: ColorSettings;
background: BackgroundSettings;
@ -106,7 +106,7 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
},
valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showDate: true,
dateFormat: 'yyyy-MM-dd HH:mm:ss',
dateFormat: lastUpdateAgoDateFormat(),
dateFont: {
family: 'Roboto',
size: 12,

View File

@ -108,6 +108,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
@ -115,7 +116,8 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
const newRange: ColorRange = {
color: 'rgba(0,0,0,0.87)'
};
this.rangeListFormArray.push(this.colorRangeControl(newRange), {emitEvent: true});
this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="cssUnitFormControl">
<mat-option *ngFor="let cssUnit of cssUnitsList" [value]="cssUnit">{{ cssUnit }}</mat-option>
</mat-select>
</mat-form-field>

View File

@ -0,0 +1,82 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { cssUnit, cssUnits } from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-css-unit-select',
templateUrl: './css-unit-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CssUnitSelectComponent),
multi: true
}
]
})
export class CssUnitSelectComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
cssUnitsList = cssUnits;
cssUnitFormControl: UntypedFormControl;
modelValue: cssUnit;
private propagateChange = null;
constructor() {}
ngOnInit(): void {
this.cssUnitFormControl = new UntypedFormControl();
this.cssUnitFormControl.valueChanges.subscribe((value: cssUnit) => {
this.updateModel(value);
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cssUnitFormControl.disable();
} else {
this.cssUnitFormControl.enable();
}
}
writeValue(value: cssUnit): void {
this.modelValue = value;
this.cssUnitFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: cssUnit): void {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
}

View File

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="dateFormatFormControl" [compareWith]="dateFormatsCompare" >
<mat-option *ngFor="let dateFormat of dateFormatList" [value]="dateFormat">{{ dateFormatDisplayValue(dateFormat) }}</mat-option>
</mat-select>
<button #customFormatButton
*ngIf="dateFormatFormControl.value?.custom" matSuffix mat-icon-button (click)="openDateFormatSettingsPopup($event, customFormatButton)">
<tb-icon>edit</tb-icon>
</button>
</mat-form-field>

View File

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import {
compareDateFormats,
dateFormats,
DateFormatSettings
} from '@home/components/widget/config/widget-settings.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { deepClone } from '@core/utils';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
@Component({
selector: 'tb-date-format-select',
templateUrl: './date-format-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateFormatSelectComponent),
multi: true
}
]
})
export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
@ViewChild('customFormatButton', {static: false})
customFormatButton: MatButton;
@Input()
disabled: boolean;
dateFormatList = dateFormats;
dateFormatsCompare = compareDateFormats;
dateFormatFormControl: UntypedFormControl;
modelValue: DateFormatSettings;
private propagateChange = null;
private formatCache: {[format: string]: string} = {};
constructor(private translate: TranslateService,
private date: DatePipe,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl();
this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => {
this.updateModel(value);
if (value?.custom) {
setTimeout(() => {
this.openDateFormatSettingsPopup(null, this.customFormatButton);
}, 0);
}
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.dateFormatFormControl.disable();
} else {
this.dateFormatFormControl.enable();
}
}
writeValue(value: DateFormatSettings): void {
this.modelValue = value;
this.dateFormatFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: DateFormatSettings): void {
if (!compareDateFormats(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
dateFormatDisplayValue(value: DateFormatSettings): string {
if (value.custom) {
return this.translate.instant('date.custom-date');
} else if (value.lastUpdateAgo) {
return this.translate.instant('date.last-update-n-ago');
} else {
if (!this.formatCache[value.format]) {
this.formatCache[value.format] = this.date.transform(Date.now(), value.format);
}
return this.formatCache[value.format];
}
}
openDateFormatSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
dateFormat: deepClone(this.modelValue)
};
const dateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DateFormatSettingsPanelComponent, 'top', true, null,
ctx,
{},
{}, {}, true);
dateFormatSettingsPanelPopover.tbComponentRef.instance.popover = dateFormatSettingsPanelPopover;
dateFormatSettingsPanelPopover.tbComponentRef.instance.dateFormatApplied.subscribe((dateFormat) => {
dateFormatSettingsPanelPopover.hide();
this.modelValue = dateFormat;
this.propagateChange(this.modelValue);
});
}
}
}

View File

@ -0,0 +1,47 @@
<!--
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-date-format-settings-panel">
<div class="tb-date-format-settings-title" translate>date.custom-date</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>date.format</div>
<mat-form-field class="tb-date-format-input" required fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput [formControl]="dateFormatFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding date-format-preview">
<div class="fixed-title-width" translate>date.preview</div>
<div class="preview-text" fxFlex>{{ previewText }}</div>
</div>
<div class="tb-date-format-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyDateFormat()"
[disabled]="dateFormatFormControl.invalid || !dateFormatFormControl.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

View File

@ -0,0 +1,67 @@
/**
* 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-date-format-settings-panel {
width: 500px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-date-format-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.date-format-preview {
align-items: flex-start;
.preview-text {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.38);
}
}
.mat-mdc-form-field.tb-date-format-input {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-icon-suffix {
display: flex;
align-items: center;
line-height: normal;
}
}
}
}
.tb-date-format-settings-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

View File

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { DateFormatSettings } from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormControl, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
@Component({
selector: 'tb-date-format-settings-panel',
templateUrl: './date-format-settings-panel.component.html',
providers: [],
styleUrls: ['./date-format-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DateFormatSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
dateFormat: DateFormatSettings;
@Input()
popover: TbPopoverComponent<DateFormatSettingsPanelComponent>;
@Output()
dateFormatApplied = new EventEmitter<DateFormatSettings>();
dateFormatFormControl: UntypedFormControl;
previewText = '';
constructor(private date: DatePipe,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl(this.dateFormat.format, [Validators.required]);
this.dateFormatFormControl.valueChanges.subscribe((value: string) => {
this.previewText = this.date.transform(Date.now(), value);
});
this.previewText = this.date.transform(Date.now(), this.dateFormat.format);
}
cancel() {
this.popover?.hide();
}
applyDateFormat() {
this.dateFormat.format = this.dateFormatFormControl.value;
this.dateFormatApplied.emit(this.dateFormat);
}
}

View File

@ -23,11 +23,7 @@
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="sizeUnit">
<mat-option *ngFor="let cssUnit of cssUnitsList" [value]="cssUnit">{{ cssUnit }}</mat-option>
</mat-select>
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="sizeUnit"></tb-css-unit-select>
</div>
</div>
<div class="tb-form-row no-border no-padding">

View File

@ -28,7 +28,6 @@ import { PageComponent } from '@shared/components/page.component';
import {
commonFonts,
ComponentStyle,
cssUnits,
Font,
fontStyles,
fontStyleTranslations,
@ -66,8 +65,6 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
@ViewChild('familyInput', {static: true}) familyInput: ElementRef;
cssUnitsList = cssUnits;
fontWeightsList = fontWeights;
fontWeightTranslationsMap = fontWeightTranslations;

View File

@ -276,6 +276,11 @@ import { ColorSettingsComponent } from '@home/components/widget/lib/settings/com
import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
import { CssUnitSelectComponent } from '@home/components/widget/lib/settings/common/css-unit-select.component';
import { DateFormatSelectComponent } from '@home/components/widget/lib/settings/common/date-format-select.component';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
@NgModule({
declarations: [
@ -383,7 +388,10 @@ import {
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent
ColorSettingsPanelComponent,
CssUnitSelectComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent
],
imports: [
CommonModule,
@ -495,7 +503,10 @@ import {
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent
ColorSettingsPanelComponent,
CssUnitSelectComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent
]
})
export class WidgetSettingsModule {

View File

@ -24,6 +24,7 @@ import {
import { BehaviorSubject } from 'rxjs';
import { share } from 'rxjs/operators';
import { HelpService } from '@core/services/help.service';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-help-markdown',
@ -36,7 +37,9 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
@Input() helpContent: string;
@Input() visible: boolean;
@Input()
@coerceBoolean()
visible: boolean;
@Input() style: { [klass: string]: any } = {};

View File

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic" style="width: 100%;">
<input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"

View File

@ -37,19 +37,21 @@ export class DateAgoPipe implements PipeTransform {
}
transform(value: number, args?: any): string {
transform(value: string| number | Date, args?: any): string {
if (value) {
const applyAgo = !!args?.applyAgo;
const short = !!args?.short;
const textPart = !!args?.textPart;
const ms = Math.floor((+new Date() - +new Date(value)));
if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now'
return this.translate.instant('timewindow.just-now');
return this.translate.instant(textPart ? 'timewindow.just-now-lower' : 'timewindow.just-now');
}
let counter;
// eslint-disable-next-line guard-for-in
for (const i in intervals) {
counter = Math.floor(ms / intervals[i]);
if (counter > 0) {
let res = this.translate.instant(`timewindow.${i}`, {[i]: counter});
let res = this.translate.instant(`timewindow.${i+(short ? '-short' : '')}`, {[i]: counter});
if (applyAgo) {
res += ' ' + this.translate.instant('timewindow.ago');
}

View File

@ -0,0 +1,88 @@
#### Pre-defined format options
| Option | Equivalent to | Examples (given in `en-US` locale) |
|------------|---------------------------------|-----------------------------------------------|
| short | M/d/yy, h:mm a | 6/15/15, 9:03 AM |
| medium | MMM d, y, h:mm:ss a | Jun 15, 2015, 9:03:01 AM |
| long | MMMM d, y, h:mm:ss a z | June 15, 2015 at 9:03:01 AM GMT+1 |
| full | EEEE, MMMM d, y, h:mm:ss a zzzz | Monday, June 15, 2015 at 9:03:01 AM GMT+01:00 |
| shortDate | M/d/yy | 6/15/15 |
| mediumDate | MMM d, y | Jun 15, 2015 |
| longDate | MMMM d, y | June 15, 2015 |
| fullDate | EEEE, MMMM d, y | Monday, June 15, 2015 |
| shortTime | h:mm a | 9:03 AM |
| mediumTime | h:mm:ss a | 9:03:01 AM |
| longTime | h:mm:ss a z | 9:03:01 AM GMT+1 |
| fullTime | h:mm:ss a zzzz | 9:03:01 AM GMT+01:00 |
#### Custom format options
You can construct a format string using symbols to specify the components
of a date-time value, as described in the following table.
Format details depend on the locale.
Fields marked with (*) are only available in the extra data set for the given locale.
| Field type | Format | Description | Example Value |
|---------------------|-------------|--------------------------------------------------------------|------------------------------------------------------------|
| Era | G, GG & GGG | Abbreviated | AD |
| | GGGG | Wide | Anno Domini |
| | GGGGG | Narrow | A |
| Year | y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
| | yy | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
| | yyy | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
| | yyyy | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
| Week-numbering year | Y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
| | YY | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
| | YYY | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
| | YYYY | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
| Month | M | Numeric: 1 digit | 9, 12 |
| | MM | Numeric: 2 digits + zero padded | 09, 12 |
| | MMM | Abbreviated | Sep |
| | MMMM | Wide | September |
| | MMMMM | Narrow | S |
| Month standalone | L | Numeric: 1 digit | 9, 12 |
| | LL | Numeric: 2 digits + zero padded | 09, 12 |
| | LLL | Abbreviated | Sep |
| | LLLL | Wide | September |
| | LLLLL | Narrow | S |
| Week of year | w | Numeric: minimum digits | 1... 53 |
| | ww | Numeric: 2 digits + zero padded | 01... 53 |
| Week of month | W | Numeric: 1 digit | 1... 5 |
| Day of month | d | Numeric: minimum digits | 1 |
| | dd | Numeric: 2 digits + zero padded | 01 |
| Week day | E, EE & EEE | Abbreviated | Tue |
| | EEEE | Wide | Tuesday |
| | EEEEE | Narrow | T |
| | EEEEEE | Short | Tu |
| Week day standalone | c, cc | Numeric: 1 digit | 2 |
| | ccc | Abbreviated | Tue |
| | cccc | Wide | Tuesday |
| | ccccc | Narrow | T |
| | cccccc | Short | Tu |
| Period | a, aa & aaa | Abbreviated | am/pm or AM/PM |
| | aaaa | Wide (fallback to `a` when missing) | ante meridiem/post meridiem |
| | aaaaa | Narrow | a/p |
| Period* | B, BB & BBB | Abbreviated | mid. |
| | BBBB | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
| | BBBBB | Narrow | md |
| Period standalone* | b, bb & bbb | Abbreviated | mid. |
| | bbbb | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
| | bbbbb | Narrow | md |
| Hour 1-12 | h | Numeric: minimum digits | 1, 12 |
| | hh | Numeric: 2 digits + zero padded | 01, 12 |
| Hour 0-23 | H | Numeric: minimum digits | 0, 23 |
| | HH | Numeric: 2 digits + zero padded | 00, 23 |
| Minute | m | Numeric: minimum digits | 8, 59 |
| | mm | Numeric: 2 digits + zero padded | 08, 59 |
| Second | s | Numeric: minimum digits | 0... 59 |
| | ss | Numeric: 2 digits + zero padded | 00... 59 |
| Fractional seconds | S | Numeric: 1 digit | 0... 9 |
| | SS | Numeric: 2 digits + zero padded | 00... 99 |
| | SSS | Numeric: 3 digits + zero padded (= milliseconds) | 000... 999 |
| Zone | z, zz & zzz | Short specific non location format (fallback to O) | GMT-8 |
| | zzzz | Long specific non location format (fallback to OOOO) | GMT-08:00 |
| | Z, ZZ & ZZZ | ISO8601 basic format | -0800 |
| | ZZZZ | Long localized GMT format | GMT-8:00 |
| | ZZZZZ | ISO8601 extended format + Z indicator for offset 0 (= XXXXX) | -08:00 |
| | O, OO & OOO | Short localized GMT format | GMT-8 |
| | OOOO | Long localized GMT format | GMT-08:00 |

View File

@ -940,6 +940,13 @@
"edges": "Customer edge instances",
"manage-edges": "Manage edges"
},
"date": {
"last-update-n-ago": "Last update N ago",
"last-update-n-ago-text": "Last update {{ agoText }}",
"custom-date": "Custom date",
"format": "Format",
"preview": "Preview"
},
"datetime": {
"date-from": "Date from",
"time-from": "Time from",
@ -3832,15 +3839,22 @@
"timewindow": {
"timewindow": "Timewindow",
"years": "{ years, plural, =1 { year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
"months-short": "{{ months }}M",
"weeks": "{ weeks, plural, =1 { week } other {# weeks } }",
"weeks-short": "{{ weeks }}w",
"days": "{ days, plural, =1 { day } other {# days } }",
"days-short": "{{ days }}d",
"hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr",
"hr-short": "{{ hr }}h",
"minutes": "{ minutes, plural, =0 { minute } =1 {1 minute } other {# minutes } }",
"min": "{{ min }} min",
"min-short": "{{ min }}m",
"seconds": "{ seconds, plural, =0 { second } =1 {1 second } other {# seconds } }",
"sec": "{{ sec }} sec",
"sec-short": "{{ sec }}s",
"short": {
"days": "{ days, plural, =1 {1 day } other {# days } }",
"hours": "{ hours, plural, =1 {1 hour } other {# hours } }",
@ -3859,6 +3873,7 @@
"hide": "Hide",
"interval": "Interval",
"just-now": "Just now",
"just-now-lower": "just now",
"ago": "ago"
},
"unit": {
@ -4208,6 +4223,7 @@
"decimals": "Number of digits after floating point",
"units-short": "Units",
"decimals-short": "Decimals",
"decimals-suffix": "decimals",
"timewindow": "Timewindow",
"use-dashboard-timewindow": "Use dashboard timewindow",
"use-widget-timewindow": "Use widget timewindow",
@ -5235,7 +5251,10 @@
"layout-simplified": "Simplified",
"layout-horizontal": "Horizontal",
"layout-horizontal-reversed": "Horizontal reversed",
"label": "Label"
"label": "Label",
"icon": "Icon",
"value": "Value",
"date": "Date"
},
"table": {
"common-table-settings": "Common Table Settings",