diff --git a/src/components/DirectoryItems.jsx b/src/components/DirectoryItems.jsx index d93ac556..57604e3c 100644 --- a/src/components/DirectoryItems.jsx +++ b/src/components/DirectoryItems.jsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "react-router-dom"; import KopiaTable from "./KopiaTable"; -import { objectLink, rfc3339TimestampForDisplay } from "../utils/formatutils"; +import { objectLink, LocaleFormatUtils } from "../utils/formatutils"; import { sizeWithFailures } from "../utils/uiutil"; import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import PropTypes from "prop-types"; @@ -41,10 +41,12 @@ function directoryLinkOrDownload(x, state) { export function DirectoryItems({ historyState, items }) { const context = React.useContext(UIPreferencesContext); - const { bytesStringBase2 } = context; + const { bytesStringBase2, locale } = context; + const fmt = new LocaleFormatUtils(locale); const columns = [ { id: "name", + accessorFn: (x) => objectName(x.name, x.type), header: "Name", width: "", cell: (x) => directoryLinkOrDownload(x.row.original, historyState), @@ -54,26 +56,30 @@ export function DirectoryItems({ historyState, items }) { accessorFn: (x) => x.mtime, header: "Last Modification", width: 200, - cell: (x) => rfc3339TimestampForDisplay(x.cell.getValue()), + cell: (x) => fmt.timestamp(x.cell.getValue()), }, { id: "size", accessorFn: (x) => sizeInfo(x), header: "Size", width: 100, - cell: (x) => sizeWithFailures(x.cell.getValue(), x.row.original.summ, bytesStringBase2), + cell: (x) => ( +
{sizeWithFailures(x.cell.getValue(), x.row.original.summ, bytesStringBase2)}
+ ), }, { id: "files", accessorFn: (x) => (x.summ ? x.summ.files : undefined), header: "Files", width: 100, + cell: (x) =>
{fmt.number(x.getValue())}
, }, { id: "dirs", accessorFn: (x) => (x.summ ? x.summ.dirs : undefined), header: "Directories", width: 100, + cell: (x) =>
{fmt.number(x.getValue())}
, }, ]; diff --git a/src/contexts/UIPreferencesContext.tsx b/src/contexts/UIPreferencesContext.tsx index 985f80c9..d8ff52f9 100644 --- a/src/contexts/UIPreferencesContext.tsx +++ b/src/contexts/UIPreferencesContext.tsx @@ -11,6 +11,7 @@ const DEFAULT_PREFERENCES = { theme: getDefaultTheme(), preferWebDav: false, fontSize: "fs-6", + locale: "en-US", } as SerializedUIPreferences; const PREFERENCES_URL = "/api/v1/ui-preferences"; @@ -24,11 +25,13 @@ export interface UIPreferences { get bytesStringBase2(): boolean; get defaultSnapshotViewAll(): boolean; get fontSize(): FontSize; + get locale(): string; setTheme: (theme: Theme) => void; setPageSize: (pageSize: number) => void; setByteStringBase: (bytesStringBase2: string) => void; setDefaultSnapshotViewAll: (defaultSnapshotViewAll: boolean) => void; setFontSize: (size: string) => void; + setLocale: (locale: string) => void; } interface SerializedUIPreferences { @@ -37,6 +40,7 @@ interface SerializedUIPreferences { defaultSnapshotViewAll?: boolean; theme: Theme; fontSize: FontSize; + locale: string; } export interface UIPreferenceProviderProps { @@ -108,6 +112,11 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) { [], ); + const setLocale = (input: string) => + setPreferences((oldPreferences) => { + return { ...oldPreferences, locale: input }; + }); + useEffect(() => { axios .get(PREFERENCES_URL) @@ -124,6 +133,9 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) { } else { storedPreferences.pageSize = normalizePageSize(storedPreferences.pageSize); } + if (!storedPreferences.locale || (storedPreferences.locale as string) === "") { + storedPreferences.locale = DEFAULT_PREFERENCES.locale; + } setTheme(storedPreferences.theme); setFontSize(storedPreferences.fontSize); setPreferences(storedPreferences); @@ -164,6 +176,7 @@ export function UIPreferenceProvider(props: UIPreferenceProviderProps) { setByteStringBase, setDefaultSnapshotViewAll, setFontSize, + setLocale, } as UIPreferences; return {props.children}; diff --git a/src/css/App.css b/src/css/App.css index f7640eaa..fd3ea033 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -97,6 +97,10 @@ body { background-color: var(--background-color); } +#kopia .table .align-right { + text-align: right; +} + #kopia nav.navbar { padding-left: 10px; padding-right: 10px; diff --git a/src/pages/Preferences.jsx b/src/pages/Preferences.jsx index 2fe976ab..5e08081f 100644 --- a/src/pages/Preferences.jsx +++ b/src/pages/Preferences.jsx @@ -12,7 +12,7 @@ import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; * Class that exports preferences */ export function Preferences() { - const { theme, bytesStringBase2, fontSize, setByteStringBase, setTheme, setFontSize } = + const { theme, bytesStringBase2, fontSize, locale, setByteStringBase, setTheme, setFontSize, setLocale } = useContext(UIPreferencesContext); return ( @@ -62,6 +62,17 @@ export function Preferences() { + + Locale + setLocale(e.target.value)} + /> + diff --git a/src/pages/SnapshotHistory.jsx b/src/pages/SnapshotHistory.jsx index 4dd8853b..9f353e31 100644 --- a/src/pages/SnapshotHistory.jsx +++ b/src/pages/SnapshotHistory.jsx @@ -1,5 +1,5 @@ import axios from "axios"; -import React, { Component, useContext } from "react"; +import React, { Component } from "react"; import Badge from "react-bootstrap/Badge"; import Form from "react-bootstrap/Form"; import Row from "react-bootstrap/Row"; @@ -9,7 +9,7 @@ import Spinner from "react-bootstrap/Spinner"; import { Link, useNavigate, useLocation } from "react-router-dom"; import KopiaTable from "../components/KopiaTable"; import { CLIEquivalent } from "../components/CLIEquivalent"; -import { compare, objectLink, parseQuery, rfc3339TimestampForDisplay } from "../utils/formatutils"; +import { compare, objectLink, parseQuery, LocaleFormatUtils } from "../utils/formatutils"; import { errorAlert, redirect, sizeWithFailures } from "../utils/uiutil"; import { sourceQueryStringParams } from "../utils/policyutil"; import { GoBackButton } from "../components/GoBackButton"; @@ -350,7 +350,8 @@ class SnapshotHistoryInternal extends Component { render() { let { snapshots, unfilteredCount, uniqueCount, isLoading, error } = this.state; - const { bytesStringBase2 } = this.context; + const { bytesStringBase2, locale } = this.context; + const fmt = new LocaleFormatUtils(locale); if (error) { return

{error.message}

; } @@ -385,10 +386,9 @@ class SnapshotHistoryInternal extends Component { header: "Start time", width: 200, cell: (x) => { - let timestamp = rfc3339TimestampForDisplay(x.row.original.startTime); return ( - {timestamp} + {fmt.timestamp(x.row.original.startTime)} ); }, @@ -441,17 +441,23 @@ class SnapshotHistoryInternal extends Component { header: "Size", accessorFn: (x) => x.summary.size, width: 100, - cell: (x) => sizeWithFailures(x.cell.getValue(), x.row.original.summary, bytesStringBase2), + cell: (x) => ( +
+ {sizeWithFailures(x.cell.getValue(), x.row.original.summary, bytesStringBase2)} +
+ ), }, { header: "Files", accessorFn: (x) => x.summary.files, width: 100, + cell: (x) =>
{fmt.number(x.cell.getValue())}
, }, { header: "Dirs", accessorFn: (x) => x.summary.dirs, width: 100, + cell: (x) =>
{fmt.number(x.cell.getValue())}
, }, ]; @@ -664,6 +670,7 @@ class SnapshotHistoryInternal extends Component { ); } } +SnapshotHistoryInternal.contextType = UIPreferencesContext; SnapshotHistoryInternal.propTypes = { host: PropTypes.string, @@ -676,7 +683,6 @@ SnapshotHistoryInternal.propTypes = { export function SnapshotHistory(props) { const navigate = useNavigate(); const location = useLocation(); - useContext(UIPreferencesContext); return ; } diff --git a/src/pages/Snapshots.jsx b/src/pages/Snapshots.jsx index cc993623..0aadad68 100644 --- a/src/pages/Snapshots.jsx +++ b/src/pages/Snapshots.jsx @@ -340,14 +340,17 @@ export class Snapshots extends Component { header: "Size", width: 120, accessorFn: (x) => (x.lastSnapshot ? x.lastSnapshot.stats.totalSize : 0), - cell: (x) => - sizeWithFailures( - x.cell.getValue(), - x.row.original.lastSnapshot && x.row.original.lastSnapshot.rootEntry - ? x.row.original.lastSnapshot.rootEntry.summ - : null, - bytesStringBase2, - ), + cell: (x) => ( +
+ {sizeWithFailures( + x.cell.getValue(), + x.row.original.lastSnapshot && x.row.original.lastSnapshot.rootEntry + ? x.row.original.lastSnapshot.rootEntry.summ + : null, + bytesStringBase2, + )} +
+ ), }, { id: "lastSnapshotTime", diff --git a/src/utils/formatutils.js b/src/utils/formatutils.js index cb65de00..81b243d9 100644 --- a/src/utils/formatutils.js +++ b/src/utils/formatutils.js @@ -224,3 +224,29 @@ export function formatDuration(from, to, useMultipleUnits = false) { return formatMilliseconds(ms, useMultipleUnits); } + +export class LocaleFormatUtils { + constructor(locale) { + if (!locale || locale === "") { + this.locale = undefined; + } else { + this.locale = locale; + } + } + + number(f) { + if (isNaN(parseFloat(f))) { + return ""; + } + return f.toLocaleString(this.locale); + } + + timestamp(ts) { + if (!ts) { + return ""; + } + + let dt = new Date(ts); + return dt.toLocaleString(this.locale); + } +}