diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index c8947ed55d..107d08c2aa 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -40,7 +40,7 @@ import { EventContentDialogData } from '@home/components/event/event-content-dialog.component'; import { isEqual, sortObjectKeys } from '@core/utils'; -import { historyInterval, MINUTE } from '@shared/models/time/time.models'; +import { historyInterval, HOUR, MINUTE } from '@shared/models/time/time.models'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ComponentPortal } from '@angular/cdk/portal'; @@ -94,7 +94,7 @@ export class EventTableConfig extends EntityTableConfig { this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; - this.defaultTimewindowInterval = historyInterval(MINUTE * 15); + this.defaultTimewindowInterval = historyInterval(HOUR); this.detailsPanelEnabled = false; this.selectionEnabled = false; this.searchEnabled = false; diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 20ae8c02ff..7d8546c021 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -20,7 +20,8 @@ (selectedTabChange)="onTimewindowTypeChange()" [(selectedIndex)]="timewindow.selectedTab">
-
+
{{ 'timewindow.time-range' | translate }}
@@ -58,7 +59,7 @@
- + @@ -188,11 +189,7 @@ - - + diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index b3ce04cef4..ac29d4aaa5 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -132,7 +132,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { }); } - if (this.isEdit || (!this.timewindow.hideQuickInterval && this.quickIntervalOnly)) { + if (this.isEdit || !this.timewindow.hideQuickInterval || this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant('timewindow.relative'), value: this.realtimeTypes.INTERVAL diff --git a/ui-ngx/src/app/shared/components/time/timezone-panel.component.html b/ui-ngx/src/app/shared/components/time/timezone-panel.component.html new file mode 100644 index 0000000000..0dae7823ec --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-panel.component.html @@ -0,0 +1,25 @@ +
+
+
{{ 'timezone.timezone' | translate }}
+ + +
+
+
+ + +
diff --git a/ui-ngx/src/app/shared/components/time/timezone-panel.component.scss b/ui-ngx/src/app/shared/components/time/timezone-panel.component.scss new file mode 100644 index 0000000000..031d2bb5da --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-panel.component.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../scss/constants"; + +:host { + display: flex; + flex-direction: column; + max-height: 100%; + max-width: 100%; + background-color: #fff; + + .tb-form-panel { + padding: 12px; + gap: 12px; + } + + .tb-flex { + gap: 16px; + } + + .mat-content { + overflow: hidden; + } + + .mat-padding { + padding: 0 16px; + } + + .hide-label { + margin-bottom: 5px; + margin-right: 5px; + } + +} diff --git a/ui-ngx/src/app/shared/components/time/timezone-panel.component.ts b/ui-ngx/src/app/shared/components/time/timezone-panel.component.ts new file mode 100644 index 0000000000..4b6eb468aa --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-panel.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { TimeService } from '@core/services/time.service'; +import { TranslateService } from '@ngx-translate/core'; + +export interface TimezonePanelData { + timezone: string; + isEdit: boolean; +} + +export const TIMEZONE_PANEL_DATA = new InjectionToken('TimezonePanelData'); + +@Component({ + selector: 'tb-timezone-panel', + templateUrl: './timezone-panel.component.html', + styleUrls: ['./timezone-panel.component.scss'] +}) +export class TimezonePanelComponent extends PageComponent implements OnInit { + + timezone: string; + result: string; + + timezoneForm: FormGroup; + + constructor(@Inject(TIMEZONE_PANEL_DATA) public data: TimezonePanelData, + public overlayRef: OverlayRef, + protected store: Store, + public fb: FormBuilder, + private timeService: TimeService, + private translate: TranslateService, + public viewContainerRef: ViewContainerRef) { + super(store); + this.timezone = this.data.timezone; + this.timezoneForm = this.fb.group({ + timezone: [this.timezone] + }); + } + + ngOnInit(): void { + } + + update() { + this.result = this.timezoneForm.get('timezone').value; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } + +} diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts index bc9cc56cfa..976aa8436b 100644 --- a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts @@ -45,12 +45,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af defaultTimezoneId: string = null; - @Input() - subscriptSizing: SubscriptSizing = 'fixed'; - - @Input() - appearance: MatFormFieldAppearance = 'fill'; - @Input() set defaultTimezone(timezone: string) { if (this.defaultTimezoneId !== timezone) { @@ -58,6 +52,12 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af } } + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.html b/ui-ngx/src/app/shared/components/time/timezone.component.html new file mode 100644 index 0000000000..5aff9da59c --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone.component.html @@ -0,0 +1,56 @@ + + + + +
+ {{ computedTimezoneStyle.icon }} +
+ {{innerValue}} +
+ {{ computedTimezoneStyle.icon }} +
diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.scss b/ui-ngx/src/app/shared/components/time/timezone.component.scss new file mode 100644 index 0000000000..99f3d8367d --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + min-width: 48px; + margin: 8px 0; + max-width: 100%; + &.no-margin { + margin: 0; + } + .mdc-button { + max-width: 100%; + } + section.tb-timewindow { + padding: 0 8px; + &.no-padding { + padding: 0; + } + line-height: 32px; + pointer-events: all; + cursor: pointer; + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 4px; + width: 100%; + + .tb-timewindow-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .timezone-abbr { + font-weight: 500; + } + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.ts b/ui-ngx/src/app/shared/components/time/timezone.component.ts new file mode 100644 index 0000000000..79fe0b870f --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone.component.ts @@ -0,0 +1,330 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + HostBinding, + Injector, + Input, + OnChanges, + OnInit, Renderer2, + SimpleChanges, + StaticProvider, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { DatePipe } from '@angular/common'; +import { TimeService } from '@core/services/time.service'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + ComponentStyle, + defaultTimezoneStyle, + iconStyle, + textStyle, + TimezoneStyle +} from '@shared/models/widget-settings.models'; +import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; +import { fromEvent } from 'rxjs'; +import { + TIMEZONE_PANEL_DATA, + TimezonePanelComponent, + TimezonePanelData +} from '@shared/components/time/timezone-panel.component'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component'; + +// @dynamic +@Component({ + selector: 'tb-timezone', + templateUrl: './timezone.component.html', + styleUrls: ['./timezone.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimezoneComponent), + multi: true + } + ] +}) +export class TimezoneComponent implements ControlValueAccessor, OnInit, OnChanges { + + @HostBinding('class.no-margin') + @Input() + @coerceBoolean() + noMargin = false; + + @Input() + @coerceBoolean() + noPadding = false; + + @Input() + @coerceBoolean() + disablePanel = false; + + @Input() + @coerceBoolean() + isToolbar = false; + + @Input() + @coerceBoolean() + asButton = false; + + @Input() + @coerceBoolean() + strokedButton = false; + + @Input() + @coerceBoolean() + flatButton = false; + + @Input() + @coerceBoolean() + displayTimezoneValue = true; + + @Input() + @coerceBoolean() + hideLabel = false; + + isEditValue = false; + + @Input() + set isEdit(val) { + this.isEditValue = coerceBooleanProperty(val); + this.timezoneDisabled = this.isTimezoneDisabled(); + } + + get isEdit() { + return this.isEditValue; + } + + @Input() + tooltipPosition: TooltipPosition = 'above'; + + @Input() + timezoneStyle: TimezoneStyle; + + @Input() + @coerceBoolean() + disabled: boolean; + + private userTimezoneByDefaultValue: boolean; + get userTimezoneByDefault(): boolean { + return this.userTimezoneByDefaultValue; + } + @Input() + set userTimezoneByDefault(value: boolean) { + this.userTimezoneByDefaultValue = coerceBooleanProperty(value); + } + + private localBrowserTimezonePlaceholderOnEmptyValue: boolean; + get localBrowserTimezonePlaceholderOnEmpty(): boolean { + return this.localBrowserTimezonePlaceholderOnEmptyValue; + } + @Input() + set localBrowserTimezonePlaceholderOnEmpty(value: boolean) { + this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value); + }defaultTimezoneId: string = null; + + @Input() + set defaultTimezone(timezone: string) { + if (this.defaultTimezoneId !== timezone) { + this.defaultTimezoneId = timezone; + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + innerValue: string; + + timezoneDisabled: boolean; + + computedTimezoneStyle: TimezoneStyle; + timezoneComponentStyle: ComponentStyle; + timezoneIconStyle: ComponentStyle; + + private propagateChange = (_: any) => {}; + + constructor(private overlay: Overlay, + private translate: TranslateService, + private timeService: TimeService, + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, + private datePipe: DatePipe, + private cd: ChangeDetectorRef, + private nativeElement: ElementRef, + public viewContainerRef: ViewContainerRef, + private popoverService: TbPopoverService, + private renderer: Renderer2) { + } + + ngOnInit() { + this.updateTimezoneStyle(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'timezoneStyle') { + this.updateTimezoneStyle(); + this.updateDisplayValue(); + } + } + } + } + + toggleTimezone($event: Event) { + console.log($event); + if ($event) { + $event.stopPropagation(); + } + if (this.disablePanel) { + return; + } + // const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + // if (this.popoverService.hasPopover(trigger)) { + // this.popoverService.hidePopover(trigger); + // } else { + // const timezoneSelectionPopover = this.popoverService.displayPopover(trigger, this.renderer, + // this.viewContainerRef, TimewindowStylePanelComponent, 'left', true, null, + // ctx, + // {}, + // {}, {}, true); + // timewindowStylePanelPopover.tbComponentRef.instance.popover = timewindowStylePanelPopover; + // timewindowStylePanelPopover.tbComponentRef.instance.timewindowStyleApplied.subscribe((timewindowStyle) => { + // timewindowStylePanelPopover.hide(); + // this.modelValue = timewindowStyle; + // this.propagateChange(this.modelValue); + // }); + // + // // if (componentRef.instance.result) { + // // this.innerValue = componentRef.instance.result; + // // this.timezoneDisabled = this.isTimezoneDisabled(); + // // this.updateDisplayValue(); + // // this.notifyChanged(); + // // } + // } + const config = new OverlayConfig({ + panelClass: 'tb-timezone-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '30vh', + height: 'min-content' + }); + + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions(DEFAULT_OVERLAY_POSITIONS); + + // TODO: change panel to popover + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: TIMEZONE_PANEL_DATA, + useValue: { + timezone: this.innerValue, + isEdit: this.isEdit + } as TimezonePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(TimezonePanelComponent, + this.viewContainerRef, injector)); + const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => { + overlayRef.updatePosition(); + }); + componentRef.onDestroy(() => { + resizeWindows$.unsubscribe(); + // TODO: if value check makes impossible to select browser timezone (null) + if (componentRef.instance.result) { + this.innerValue = componentRef.instance.result; + this.timezoneDisabled = this.isTimezoneDisabled(); + this.updateDisplayValue(); + this.notifyChanged(); + } + }); + this.cd.detectChanges(); + } + + private updateTimezoneStyle() { + if (!this.asButton) { + this.computedTimezoneStyle = {...defaultTimezoneStyle, ...(this.timezoneStyle || {})}; + this.timezoneComponentStyle = textStyle(this.computedTimezoneStyle.font); + if (this.computedTimezoneStyle.color) { + this.timezoneComponentStyle.color = this.computedTimezoneStyle.color; + } + this.timezoneIconStyle = this.computedTimezoneStyle.iconSize ? iconStyle(this.computedTimezoneStyle.iconSize) : {}; + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.timezoneDisabled = this.isTimezoneDisabled(); + } + + writeValue(value: string): void { + this.innerValue = value; + this.timezoneDisabled = this.isTimezoneDisabled(); + this.updateDisplayValue(); + } + + notifyChanged() { + this.propagateChange(this.innerValue); + } + + displayValue(): string { + // TODO: only offset should be displayed, timezone name shouldn't be present + return this.displayTimezoneValue ? this.innerValue : this.translate.instant('timezone.timezone'); + } + + updateDisplayValue() { + this.cd.detectChanges(); + } + + private isTimezoneDisabled(): boolean { + return this.disabled; + } + +} diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index 5124458ff0..dbe75a5b36 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -262,6 +262,19 @@ export const defaultTimewindowStyle: TimewindowStyle = { displayTypePrefix: true }; +export interface TimezoneStyle { + showIcon: boolean; + icon?: string; + iconSize?: string; + iconPosition?: 'left' | 'right'; + font?: Font; + color?: string; +} + +export const defaultTimezoneStyle: TimezoneStyle = { + showIcon: false +}; + export const constantColor = (color: string): ColorSettings => ({ type: ColorType.constant, color, diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 567b80ed83..ac15cfc26e 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -219,6 +219,8 @@ import { ImageGalleryDialogComponent } from '@shared/components/image/image-gall import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component'; import { WidgetButtonComponent } from '@shared/components/button/widget-button.component'; import { HexInputComponent } from '@shared/components/color-picker/hex-input.component'; +import { TimezoneComponent } from '@shared/components/time/timezone.component'; +import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -302,6 +304,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TimewindowComponent, TimewindowPanelComponent, TimeintervalComponent, + TimezoneComponent, + TimezonePanelComponent, QuickTimeIntervalComponent, DashboardSelectComponent, DashboardSelectPanelComponent, @@ -506,6 +510,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TimewindowComponent, TimewindowPanelComponent, TimeintervalComponent, + TimezoneComponent, + TimezonePanelComponent, QuickTimeIntervalComponent, DashboardSelectComponent, DatetimePeriodComponent, diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 4d5ae09a15..08850ddbe8 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -268,7 +268,7 @@ pre.tb-highlight { letter-spacing: normal; } -.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel, .tb-panel-container { +.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel, .tb-panel-container, tb-timezone-panel { overflow: hidden; background: #fff; border-radius: 4px;