Browse Source

UI: Value card basic config

pull/8997/head
Igor Kulikov 3 years ago
parent
commit
c0309ac1aa
  1. 45
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  2. 28
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  3. 102
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts
  4. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  5. 12
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  6. 8
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  7. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  8. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html
  9. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts
  10. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html
  11. 148
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts
  12. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html
  13. 67
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss
  14. 70
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts
  15. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  16. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  17. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  18. 5
      ui-ngx/src/app/shared/components/help-markdown.component.ts
  19. 2
      ui-ngx/src/app/shared/components/unit-input.component.html
  20. 8
      ui-ngx/src/app/shared/pipe/date-ago.pipe.ts
  21. 88
      ui-ngx/src/assets/help/en_US/date/date-format.md
  22. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

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

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

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

102
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts

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

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

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

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

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

8
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts

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

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

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

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html

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

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts

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

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html

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

148
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts

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

47
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html

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

67
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss

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

70
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts

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

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html

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

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts

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

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

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

5
ui-ngx/src/app/shared/components/help-markdown.component.ts

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

2
ui-ngx/src/app/shared/components/unit-input.component.html

@ -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()"

8
ui-ngx/src/app/shared/pipe/date-ago.pipe.ts

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

88
ui-ngx/src/assets/help/en_US/date/date-format.md

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

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

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

Loading…
Cancel
Save