diff --git a/modules/web/i18n/de/messages.de.xlf b/modules/web/i18n/de/messages.de.xlf index 036f36645b5a..921d3656f325 100644 --- a/modules/web/i18n/de/messages.de.xlf +++ b/modules/web/i18n/de/messages.de.xlf @@ -2680,6 +2680,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/es/messages.es.xlf b/modules/web/i18n/es/messages.es.xlf index 3be64eb2ca50..4a1fadf5e2ce 100644 --- a/modules/web/i18n/es/messages.es.xlf +++ b/modules/web/i18n/es/messages.es.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/fr/messages.fr.xlf b/modules/web/i18n/fr/messages.fr.xlf index 30b315d787ec..19650d4f35c4 100644 --- a/modules/web/i18n/fr/messages.fr.xlf +++ b/modules/web/i18n/fr/messages.fr.xlf @@ -2668,6 +2668,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/ja/messages.ja.xlf b/modules/web/i18n/ja/messages.ja.xlf index 6cfdb6643836..3be47a969989 100644 --- a/modules/web/i18n/ja/messages.ja.xlf +++ b/modules/web/i18n/ja/messages.ja.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/ko/messages.ko.xlf b/modules/web/i18n/ko/messages.ko.xlf index 3d7d8376f4bd..8e5e35256d9b 100644 --- a/modules/web/i18n/ko/messages.ko.xlf +++ b/modules/web/i18n/ko/messages.ko.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/messages.xlf b/modules/web/i18n/messages.xlf index 4739a587fcc7..104c3060b385 100644 --- a/modules/web/i18n/messages.xlf +++ b/modules/web/i18n/messages.xlf @@ -1979,6 +1979,12 @@ Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + + + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/zh-Hans/messages.zh-Hans.xlf b/modules/web/i18n/zh-Hans/messages.zh-Hans.xlf index e84355efac43..309df508702c 100644 --- a/modules/web/i18n/zh-Hans/messages.zh-Hans.xlf +++ b/modules/web/i18n/zh-Hans/messages.zh-Hans.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf b/modules/web/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf index ef55efed9f0e..8a4508826060 100644 --- a/modules/web/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf +++ b/modules/web/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/i18n/zh-Hant/messages.zh-Hant.xlf b/modules/web/i18n/zh-Hant/messages.zh-Hant.xlf index 55da7a4d01a0..e07c9c855c32 100644 --- a/modules/web/i18n/zh-Hant/messages.zh-Hant.xlf +++ b/modules/web/i18n/zh-Hant/messages.zh-Hant.xlf @@ -2672,6 +2672,14 @@ Hides all-namespaces option from the namespaces dropdown list in UI. Hides all-namespaces option from the namespaces dropdown list in UI. + + Timezone + Timezone + + + Change the timezone of the dashboard + Change the timezone of the dashboard + \ No newline at end of file diff --git a/modules/web/package.json b/modules/web/package.json index b5e2b0d55f80..5428c9aa0559 100644 --- a/modules/web/package.json +++ b/modules/web/package.json @@ -60,6 +60,7 @@ "@swimlane/ngx-charts": "21.1.3", "ace-builds": "1.37.5", "core-js": "3.40.0", + "countries-and-timezones": "^3.7.2", "crop-url": "4.0.1", "d3-shape": "3.2.0", "d3-time-format": "4.1.0", diff --git a/modules/web/src/common/components/date/component.ts b/modules/web/src/common/components/date/component.ts index adb77f11336a..9b78662fbc9e 100644 --- a/modules/web/src/common/components/date/component.ts +++ b/modules/web/src/common/components/date/component.ts @@ -24,6 +24,7 @@ import { import {Subject, timer} from 'rxjs'; import {switchMap} from 'rxjs/operators'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {TimezoneService} from '@common/services/global/timezone'; /** * Display a date @@ -64,6 +65,10 @@ export class DateComponent implements OnChanges { return this.relative_; } + get timezone(): string { + return this._timezone.utcOffset; + } + iteration = 0; private relative_: boolean; @@ -75,7 +80,10 @@ export class DateComponent implements OnChanges { 24, // Hours in a day ]; - constructor(private readonly cdr_: ChangeDetectorRef) {} + constructor( + private readonly cdr_: ChangeDetectorRef, + private readonly _timezone: TimezoneService + ) {} ngOnChanges() { if (this.relative_) { diff --git a/modules/web/src/common/components/date/template.html b/modules/web/src/common/components/date/template.html index aee4691f95df..747a28415506 100644 --- a/modules/web/src/common/components/date/template.html +++ b/modules/web/src/common/components/date/template.html @@ -16,7 +16,7 @@ - {{ relative ? (date | kdRelativeTime: iteration) : (date | date) }} + {{ relative ? (date | kdRelativeTime: iteration) : (date | date: undefined: timezone) }} diff --git a/modules/web/src/common/services/global/localsettings.ts b/modules/web/src/common/services/global/localsettings.ts index f649f8c15d43..1e757ddb3fa9 100644 --- a/modules/web/src/common/services/global/localsettings.ts +++ b/modules/web/src/common/services/global/localsettings.ts @@ -16,15 +16,20 @@ import {Injectable} from '@angular/core'; import {LocalSettings} from '@api/root.api'; import {ThemeService} from './theme'; +import {TimezoneService} from './timezone'; @Injectable() export class LocalSettingsService { private readonly _settingsKey = 'localSettings'; private settings_: LocalSettings = { theme: ThemeService.SystemTheme, + timezone: TimezoneService.DefaultTimezone, }; - constructor(private readonly theme_: ThemeService) {} + constructor( + private readonly theme_: ThemeService, + private readonly timezone_: TimezoneService + ) {} init(): void { const cookieValue = localStorage.getItem(this._settingsKey); @@ -43,6 +48,12 @@ export class LocalSettingsService { this.updateCookie_(); } + handleTimezoneChange(timezone: string): void { + this.settings_.timezone = timezone; + this.timezone_.timezone = timezone; + this.updateCookie_(); + } + updateCookie_(): void { localStorage.setItem(this._settingsKey, JSON.stringify(this.settings_)); } diff --git a/modules/web/src/common/services/global/module.ts b/modules/web/src/common/services/global/module.ts index e011dcbaa694..f10dce8d9b47 100644 --- a/modules/web/src/common/services/global/module.ts +++ b/modules/web/src/common/services/global/module.ts @@ -37,6 +37,7 @@ import {TitleService} from './title'; import {VerberService} from './verber'; import {PinnerService} from './pinner'; import {MeService} from '@common/services/global/me'; +import {TimezoneService} from './timezone'; @NgModule({ providers: [ @@ -61,6 +62,7 @@ import {MeService} from '@common/services/global/me'; ParamsService, LocalConfigLoaderService, DecoderService, + TimezoneService, { provide: APP_INITIALIZER, useFactory: init, @@ -73,6 +75,7 @@ import {MeService} from '@common/services/global/me'; ThemeService, LocalConfigLoaderService, MeService, + TimezoneService, ], multi: true, }, @@ -99,7 +102,8 @@ export function init( history: HistoryService, theme: ThemeService, loader: LocalConfigLoaderService, - me: MeService + me: MeService, + timezone: TimezoneService ): Function { return async () => { await loader.init(); @@ -108,6 +112,7 @@ export function init( config.init(); history.init(); theme.init(); + timezone.init(); await me.init(); return await globalSettings.init(); }; diff --git a/modules/web/src/common/services/global/timezone.ts b/modules/web/src/common/services/global/timezone.ts new file mode 100644 index 000000000000..7bb225e6b98f --- /dev/null +++ b/modules/web/src/common/services/global/timezone.ts @@ -0,0 +1,48 @@ +// Copyright 2017 The Kubernetes 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 {Timezone} from '@api/root.api'; +import {getAllTimezones, getTimezone} from 'countries-and-timezones'; + +export class TimezoneService { + private _timezones: Timezone[] = []; + static DefaultTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; + private _timezone = TimezoneService.DefaultTimezone; + constructor() {} + + get timezone(): string { + return this._timezone; + } + + set timezone(timezone: string) { + this._timezone = timezone; + } + + get timezones(): Timezone[] { + return this._timezones; + } + + get utcOffset(): string { + return getTimezone(this._timezone).utcOffsetStr; + } + + init(): void { + this._timezones = Object.entries(getAllTimezones()) + .sort((a, b) => a[1].utcOffset - b[1].utcOffset) + .map(([key, value]) => ({ + label: `${key} (UTC ${value.utcOffsetStr})`, + value: key, + })); + } +} diff --git a/modules/web/src/index.component.ts b/modules/web/src/index.component.ts index 0a46e1c3fb57..d473383b2313 100644 --- a/modules/web/src/index.component.ts +++ b/modules/web/src/index.component.ts @@ -17,18 +17,21 @@ import {Component, ElementRef, OnInit} from '@angular/core'; import {LocalSettingsService} from '@common/services/global/localsettings'; import {ThemeService} from '@common/services/global/theme'; +import {TimezoneService} from '@common/services/global/timezone'; import {TitleService} from '@common/services/global/title'; @Component({selector: 'kd-root', template: ''}) export class RootComponent implements OnInit { private _theme = this._themeService.theme; + private _timezone = this._timezoneService.timezone; constructor( private readonly _themeService: ThemeService, private readonly _localSettingService: LocalSettingsService, private readonly _overlayContainer: OverlayContainer, private readonly _kdRootRef: ElementRef, - private readonly _titleService: TitleService + private readonly _titleService: TitleService, + private readonly _timezoneService: TimezoneService ) {} ngOnInit(): void { @@ -36,9 +39,15 @@ export class RootComponent implements OnInit { this._themeService.subscribe(this.onThemeChange_.bind(this)); const localSettings = this._localSettingService.get(); - if (localSettings && localSettings.theme) { - this._theme = localSettings.theme; - this._themeService.theme = localSettings.theme; + if (localSettings) { + if (localSettings.theme) { + this._theme = localSettings.theme; + this._themeService.theme = localSettings.theme; + } + if (localSettings.timezone) { + this._timezone = localSettings.timezone; + this._timezoneService.timezone = localSettings.timezone; + } } this.applyOverlayContainerTheme_('', this._theme); diff --git a/modules/web/src/settings/local/component.ts b/modules/web/src/settings/local/component.ts index 3857998d8c58..24f6cdc6e524 100644 --- a/modules/web/src/settings/local/component.ts +++ b/modules/web/src/settings/local/component.ts @@ -15,13 +15,14 @@ import {DOCUMENT} from '@angular/common'; import {Component, Inject, OnInit, ViewChild} from '@angular/core'; import {MatSelect} from '@angular/material/select'; -import {LocalSettings, Theme} from '@api/root.api'; +import {LocalSettings, Theme, Timezone} from '@api/root.api'; import {IConfig, LanguageConfig} from '@api/root.ui'; import {LocalSettingsService} from '@common/services/global/localsettings'; import {ThemeService} from '@common/services/global/theme'; import {environment} from '@environments/environment'; import {CookieService} from 'ngx-cookie-service'; import {CONFIG_DI_TOKEN} from '../../index.config'; +import {TimezoneService} from '@common/services/global/timezone'; @Component({ selector: 'kd-local-settings', @@ -35,6 +36,8 @@ export class LocalSettingsComponent implements OnInit { themes: Theme[]; selectedTheme: string; systemTheme: string; + timezones: Timezone[] = []; + selectedTimezone: string; @ViewChild(MatSelect, {static: true}) private readonly select_: MatSelect; @@ -42,6 +45,7 @@ export class LocalSettingsComponent implements OnInit { private readonly settings_: LocalSettingsService, private readonly theme_: ThemeService, private readonly cookies_: CookieService, + private readonly timezone_: TimezoneService, @Inject(DOCUMENT) private readonly document_: Document, @Inject(CONFIG_DI_TOKEN) private readonly appConfig_: IConfig ) {} @@ -55,6 +59,9 @@ export class LocalSettingsComponent implements OnInit { this.themes = this.theme_.themes; this.selectedTheme = this.theme_.theme; this.systemTheme = ThemeService.SystemTheme; + + this.timezones = this.timezone_.timezones; + this.selectedTimezone = this.timezone_.timezone; } onThemeChange(): void { @@ -67,6 +74,11 @@ export class LocalSettingsComponent implements OnInit { this.document_.location.reload(); } + onTimezoneSelected(selectedTimezone: string) { + this.settings.timezone = selectedTimezone; + this.settings_.handleTimezoneChange(this.settings.timezone); + } + isProdMode(): boolean { return environment.production; } diff --git a/modules/web/src/settings/local/template.html b/modules/web/src/settings/local/template.html index bfccf8b63696..4233ea589664 100644 --- a/modules/web/src/settings/local/template.html +++ b/modules/web/src/settings/local/template.html @@ -70,5 +70,26 @@ + + + + + + {{ timezone.label }} + + + + diff --git a/modules/web/src/typings/root.api.ts b/modules/web/src/typings/root.api.ts index ad36f2b5f9e1..6a0f023bbaaa 100644 --- a/modules/web/src/typings/root.api.ts +++ b/modules/web/src/typings/root.api.ts @@ -782,6 +782,7 @@ export interface CsrfToken { export interface LocalSettings { theme: string; + timezone: string; } export interface Theme { @@ -1308,3 +1309,8 @@ export interface User { name: string; authenticated: boolean; } + +export interface Timezone { + value: string; + label: string; +} diff --git a/modules/web/yarn.lock b/modules/web/yarn.lock index 94621c9c71e3..d2cf3f75df2d 100644 --- a/modules/web/yarn.lock +++ b/modules/web/yarn.lock @@ -9634,6 +9634,13 @@ __metadata: languageName: node linkType: hard +"countries-and-timezones@npm:^3.7.2": + version: 3.7.2 + resolution: "countries-and-timezones@npm:3.7.2" + checksum: 6375ea8a4b1da023d57f6a7fb6bc14ace31da686be14b941d255749471a2797f1a66e77d95d7ef9ac15a0d74e08f753f747e9240f57bece26b784be40b940a0e + languageName: node + linkType: hard + "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -14771,6 +14778,7 @@ __metadata: codelyzer: 6.0.2 concurrently: 9.1.2 core-js: 3.40.0 + countries-and-timezones: ^3.7.2 crop-url: 4.0.1 cypress: 14.0.1 cypress-fail-fast: 7.1.1