mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-29 19:11:24 +01:00
Add date/time format setting in portal (#16796)
* Add date/time format setting in portal Currently, the format used for rendering dates and times is derived from the language/locale selected by the user. The formats used in the en-US locale ("English" in Harbor's GUI) are ambiguous and hard to understand for many users. For example, is 10/11/21 the 10th of November, 2021, the 11th of October, 2021, or even something else like the 21nd of November, 2010? Even if one does know how to interpret it in theory, such dates are essentially enciphered and must be mentally deciphered by the user every time, incurring unnecessary cognitive load. Similarly, many users are used to the 24-hour clock rather than the 12-hour clock (AM/PM), and so on. This PR adds a dropdown next to the existing language selector that lets the user choose between the default format for the current locale and the internationally standardized, unambiguous ISO 8601 format. For example, when viewing a list of resources, the ISO 8601 option makes points in time display as > 2021-10-11, 13:37 instead of > 10/11/21, 1:37 PM thereby improving the user experience considerably for users not familiar with the US date/time format (or, in general, the default format for the locale they have selected). The localized versions of the "Default" label are copied from `SCANNER.DEFAULT` in each locale. Signed-off-by: Simon Alling <alling.simon@gmail.com> * Fix indentation Signed-off-by: Simon Alling <alling.simon@gmail.com> * Remove redundant localStorage existence check Signed-off-by: Simon Alling <alling.simon@gmail.com> * Run 'npm run lint -- --fix'
This commit is contained in:
parent
db45155365
commit
c4b782bc95
@ -36,7 +36,7 @@
|
||||
*ngIf="!isIntegrationMode">
|
||||
<button class="nav-icon nav-icon-width" clrDropdownToggle>
|
||||
<clr-icon shape="world" class="icon-left"></clr-icon>
|
||||
<span class="currentLang">{{ currentLang }}</span>
|
||||
<span class="currentLocale">{{ currentLang }}</span>
|
||||
<clr-icon size="10" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
@ -50,6 +50,29 @@
|
||||
>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<clr-dropdown
|
||||
class="dropdown-locale dropdown bottom-left"
|
||||
*ngIf="!isIntegrationMode">
|
||||
<button class="nav-icon nav-icon-width" clrDropdownToggle>
|
||||
<clr-icon shape="date" class="icon-left"></clr-icon>
|
||||
<span class="currentLocale">{{
|
||||
currentDatetimeRendering | translate
|
||||
}}</span>
|
||||
<clr-icon size="10" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<a
|
||||
*ngFor="let rendering of guiDatetimeRenderings"
|
||||
href="javascript:void(0)"
|
||||
clrDropdownItem
|
||||
(click)="switchDatetimeRendering(rendering[0])"
|
||||
[class.locale-selected]="
|
||||
matchDatetimeRendering(rendering[0])
|
||||
"
|
||||
>{{ rendering[1] | translate }}</a
|
||||
>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<div class="nav-divider"></div>
|
||||
<clr-dropdown class="dropdown" *ngIf="isSessionValid">
|
||||
<button class="nav-text" clrDropdownToggle>
|
||||
|
@ -15,7 +15,7 @@
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.lang-selected {
|
||||
.locale-selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
.icon-left {
|
||||
left: -8px;
|
||||
}
|
||||
.currentLang {
|
||||
.currentLocale {
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@
|
||||
.dropdown-item {
|
||||
outline: none;
|
||||
}
|
||||
.dropdown-lang {
|
||||
.dropdown-locale {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.user-down {
|
||||
|
@ -24,7 +24,11 @@ import { MessageHandlerService } from '../../services/message-handler.service';
|
||||
import { SkinableConfig } from '../../../services/skinable-config.service';
|
||||
import {
|
||||
CommonRoutes,
|
||||
DATETIME_RENDERINGS,
|
||||
DatetimeRendering,
|
||||
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY,
|
||||
DEFAULT_LANG_LOCALSTORAGE_KEY,
|
||||
DefaultDatetimeRendering,
|
||||
DeFaultLang,
|
||||
LANGUAGES,
|
||||
SupportedLanguage,
|
||||
@ -34,6 +38,7 @@ import {
|
||||
HAS_STYLE_MODE,
|
||||
StyleMode,
|
||||
} from '../../../services/theme';
|
||||
import { getDatetimeRendering } from '../../units/shared.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'navigator',
|
||||
@ -45,7 +50,9 @@ export class NavigatorComponent implements OnInit {
|
||||
@Output() showDialogModalAction = new EventEmitter<ModalEvent>();
|
||||
|
||||
readonly guiLanguages = Object.entries(LANGUAGES);
|
||||
readonly guiDatetimeRenderings = Object.entries(DATETIME_RENDERINGS);
|
||||
selectedLang: SupportedLanguage = DeFaultLang;
|
||||
selectedDatetimeRendering: DatetimeRendering = DefaultDatetimeRendering;
|
||||
appTitle: string = 'APP_TITLE.HARBOR';
|
||||
customStyle: CustomStyle;
|
||||
constructor(
|
||||
@ -63,6 +70,7 @@ export class NavigatorComponent implements OnInit {
|
||||
// custom skin
|
||||
this.customStyle = this.skinableConfig.getSkinConfig();
|
||||
this.selectedLang = this.translate.currentLang as SupportedLanguage;
|
||||
this.selectedDatetimeRendering = getDatetimeRendering();
|
||||
if (this.appConfigService.isIntegrationMode()) {
|
||||
this.appTitle = 'APP_TITLE.VIC';
|
||||
}
|
||||
@ -86,6 +94,10 @@ export class NavigatorComponent implements OnInit {
|
||||
return LANGUAGES[this.selectedLang];
|
||||
}
|
||||
|
||||
public get currentDatetimeRendering(): string {
|
||||
return DATETIME_RENDERINGS[this.selectedDatetimeRendering];
|
||||
}
|
||||
|
||||
public get admiralLink(): string {
|
||||
return this.appConfigService.getAdmiralEndpoint(window.location.href);
|
||||
}
|
||||
@ -123,6 +135,10 @@ export class NavigatorComponent implements OnInit {
|
||||
return lang === this.selectedLang;
|
||||
}
|
||||
|
||||
matchDatetimeRendering(datetime: DatetimeRendering): boolean {
|
||||
return datetime === this.selectedDatetimeRendering;
|
||||
}
|
||||
|
||||
// Open the account setting dialog
|
||||
openAccountSettingsModal(): void {
|
||||
this.showAccountSettingsModal.emit({
|
||||
@ -170,6 +186,14 @@ export class NavigatorComponent implements OnInit {
|
||||
this.translate.use(lang).subscribe(() => window.location.reload());
|
||||
}
|
||||
|
||||
switchDatetimeRendering(datetime: DatetimeRendering): void {
|
||||
this.selectedDatetimeRendering = datetime;
|
||||
localStorage.setItem(
|
||||
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY,
|
||||
datetime
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the home action
|
||||
homeAction(): void {
|
||||
if (this.session.getCurrentUser() != null) {
|
||||
|
@ -230,6 +230,18 @@ export const supportedLangs = Object.keys(LANGUAGES) as SupportedLanguage[];
|
||||
*/
|
||||
export const DEFAULT_LANG_LOCALSTORAGE_KEY = 'harbor-lang';
|
||||
|
||||
export type DatetimeRendering = keyof typeof DATETIME_RENDERINGS;
|
||||
export const DATETIME_RENDERINGS = {
|
||||
'locale-default': 'TOP_NAV.DATETIME_RENDERING_DEFAULT',
|
||||
'iso-8601': 'ISO 8601',
|
||||
} as const;
|
||||
export const DefaultDatetimeRendering = 'locale-default';
|
||||
/**
|
||||
* The default cookie key used to store current used datetime rendering preference.
|
||||
*/
|
||||
export const DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY =
|
||||
'harbor-datetime-rendering';
|
||||
|
||||
export const AdmiralQueryParamKey = 'admiral_redirect_url';
|
||||
|
||||
export const HarborQueryParamKey = 'harbor_redirect_url';
|
||||
|
@ -1,13 +1,25 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import {
|
||||
DatetimeRendering,
|
||||
DEFAULT_LANG_LOCALSTORAGE_KEY,
|
||||
DeFaultLang,
|
||||
} from '../entities/shared.const';
|
||||
import { isSupportedLanguage } from '../units/shared.utils';
|
||||
import {
|
||||
getDatetimeRendering,
|
||||
isSupportedLanguage,
|
||||
} from '../units/shared.utils';
|
||||
|
||||
const baseTimeLine: Date = new Date('1970-1-1');
|
||||
|
||||
const formatTransformers: Record<
|
||||
DatetimeRendering,
|
||||
(format: string) => string
|
||||
> = {
|
||||
'iso-8601': asISO8601,
|
||||
'locale-default': format => format,
|
||||
} as const;
|
||||
|
||||
@Pipe({
|
||||
name: 'harborDatetime',
|
||||
pure: false,
|
||||
@ -20,7 +32,43 @@ export class HarborDatetimePipe implements PipeTransform {
|
||||
}
|
||||
const savedLang = localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY);
|
||||
const lang = isSupportedLanguage(savedLang) ? savedLang : DeFaultLang;
|
||||
const formatTransformer = formatTransformers[getDatetimeRendering()];
|
||||
// default format medium
|
||||
return new DatePipe(lang).transform(value, format ? format : 'medium');
|
||||
return new DatePipe(lang).transform(
|
||||
value,
|
||||
formatTransformer(format ? format : 'medium')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function asISO8601<Format extends string>(format: Format) {
|
||||
switch (format) {
|
||||
// https://angular.io/api/common/DatePipe#pre-defined-format-options
|
||||
case 'short':
|
||||
return 'yyyy-MM-dd, HH:mm';
|
||||
case 'medium':
|
||||
return 'yyyy-MM-dd, HH:mm:ss';
|
||||
case 'long':
|
||||
return 'yyyy-MM-dd, HH:mm:ss z';
|
||||
case 'full':
|
||||
return 'EEEE yyyy-MM-dd, HH:mm:ss zzzz';
|
||||
case 'shortDate':
|
||||
return 'yyyy-MM-dd';
|
||||
case 'mediumDate':
|
||||
return 'yyyy-MM-dd';
|
||||
case 'longDate':
|
||||
return 'yyyy-MM-dd z';
|
||||
case 'fullDate':
|
||||
return 'EEEE yyyy-MM-dd zzzz';
|
||||
case 'shortTime':
|
||||
return 'HH:mm';
|
||||
case 'mediumTime':
|
||||
return 'HH:mm:ss';
|
||||
case 'longTime':
|
||||
return 'HH:mm:ss z';
|
||||
case 'fullTime':
|
||||
return 'HH:mm:ss zzzz';
|
||||
default:
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ import { NgForm } from '@angular/forms';
|
||||
import { MessageService } from '../components/global-message/message.service';
|
||||
import {
|
||||
AlertType,
|
||||
DatetimeRendering,
|
||||
DATETIME_RENDERINGS,
|
||||
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY,
|
||||
DefaultDatetimeRendering,
|
||||
httpStatusCode,
|
||||
SupportedLanguage,
|
||||
LANGUAGES,
|
||||
@ -285,3 +289,26 @@ export const errorHandler = function (error: any): string {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the datetime rendering setting saved by the user, or the default setting if no valid saved value is found.
|
||||
*/
|
||||
export function getDatetimeRendering(): DatetimeRendering {
|
||||
const savedDatetimeRendering = localStorage.getItem(
|
||||
DEFAULT_DATETIME_RENDERING_LOCALSTORAGE_KEY
|
||||
);
|
||||
if (isDatetimeRendering(savedDatetimeRendering)) {
|
||||
return savedDatetimeRendering;
|
||||
} else {
|
||||
console.warn(
|
||||
`Invalid saved datetime rendering setting ${JSON.stringify(
|
||||
savedDatetimeRendering
|
||||
)}; defaulting to ${JSON.stringify(DefaultDatetimeRendering)}.`
|
||||
);
|
||||
return DefaultDatetimeRendering;
|
||||
}
|
||||
}
|
||||
|
||||
function isDatetimeRendering(x: unknown): x is DatetimeRendering {
|
||||
return Object.keys(DATETIME_RENDERINGS).some(k => k === x);
|
||||
}
|
||||
|
@ -162,6 +162,9 @@
|
||||
"PLACEHOLDER": "Suche {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Suche Registry..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Standard"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Übersicht",
|
||||
"PROJECTS": "Projekte",
|
||||
|
@ -162,6 +162,9 @@
|
||||
"PLACEHOLDER": "Search {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Search Registry..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Default"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Dashboard",
|
||||
"PROJECTS": "Projects",
|
||||
|
@ -162,6 +162,9 @@
|
||||
"PLACEHOLDER": "Buscar en {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Buscar en el registro..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Default"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Panel",
|
||||
"PROJECTS": "Proyectos",
|
||||
|
@ -157,6 +157,9 @@
|
||||
"PLACEHOLDER": "Recherche {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Recherche dans le registre..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Défaut"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Tableau de bord",
|
||||
"PROJECTS": "Projets",
|
||||
|
@ -161,6 +161,9 @@
|
||||
"PLACEHOLDER": "Busca {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Busca de registro..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Padrão"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Painel de controle",
|
||||
"PROJECTS": "Projetos",
|
||||
|
@ -162,6 +162,9 @@
|
||||
"PLACEHOLDER": "Ara {{param}}...",
|
||||
"PLACEHOLDER_VIC": "Arama Kaydı..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "Default"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "Kontrol Paneli",
|
||||
"PROJECTS": "Projeler",
|
||||
|
@ -161,6 +161,9 @@
|
||||
"PLACEHOLDER": "搜索 {{param}}...",
|
||||
"PLACEHOLDER_VIC": "搜索 Registry..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "默认"
|
||||
},
|
||||
"SIDE_NAV": {
|
||||
"DASHBOARD": "仪表板",
|
||||
"PROJECTS": "项目",
|
||||
|
@ -161,6 +161,9 @@
|
||||
"PLACEHOLDER": "搜索{{param}}...",
|
||||
"PLACEHOLDER_VIC": "搜索Registry..."
|
||||
},
|
||||
"TOP_NAV": {
|
||||
"DATETIME_RENDERING_DEFAULT": "默認"
|
||||
},
|
||||
"SIDE_NAV":{
|
||||
"DASHBOARD": "儀表板",
|
||||
"PROJECTS": "項目",
|
||||
|
Loading…
Reference in New Issue
Block a user