mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
refactor: Update usage of date formatting (#439)
This commit is contained in:
parent
72c094e494
commit
1751b2e16d
18 changed files with 226 additions and 129 deletions
5
.changeset/shy-doors-matter.md
Normal file
5
.changeset/shy-doors-matter.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': major
|
||||
---
|
||||
|
||||
Propogate isUTC and clock settings (12h/24h) across the app
|
||||
|
|
@ -23,7 +23,7 @@ import { withAppNav } from './layout';
|
|||
import { Tags } from './Tags';
|
||||
import type { Alert, AlertHistory, LogView } from './types';
|
||||
import { AlertState } from './types';
|
||||
import { formatHumanReadableDate } from './utils';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
|
||||
import styles from '../styles/AlertsPage.module.scss';
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ function AckAlert({ alert }: { alert: Alert }) {
|
|||
</>
|
||||
) : null}{' '}
|
||||
on <br />
|
||||
{formatHumanReadableDate(new Date(alert.silenced?.at))}
|
||||
<FormatTime value={alert.silenced?.at} />
|
||||
.<br />
|
||||
</Menu.Label>
|
||||
|
||||
|
|
@ -174,8 +174,7 @@ function AckAlert({ alert }: { alert: Alert }) {
|
|||
'Alert resumed.'
|
||||
) : (
|
||||
<>
|
||||
Resumes{' '}
|
||||
{formatHumanReadableDate(new Date(alert.silenced.until))}
|
||||
Resumes <FormatTime value={alert.silenced.until} />.
|
||||
</>
|
||||
)}
|
||||
</Menu.Label>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import {
|
|||
import HyperDX from '@hyperdx/browser';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
CloseButton,
|
||||
Collapse,
|
||||
Group,
|
||||
Input,
|
||||
Loader,
|
||||
ScrollArea,
|
||||
|
|
@ -23,6 +25,7 @@ import {
|
|||
import { useDisclosure } from '@mantine/hooks';
|
||||
|
||||
import { version } from '../package.json';
|
||||
import { useUserPreferences } from '../src/useUserPreferences';
|
||||
|
||||
import api from './api';
|
||||
import {
|
||||
|
|
@ -911,6 +914,10 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
{ close: closeUserPreferences, open: openUserPreferences },
|
||||
] = useDisclosure(false);
|
||||
|
||||
const {
|
||||
userPreferences: { isUTC },
|
||||
} = useUserPreferences();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthLoadingBlocker />
|
||||
|
|
@ -931,7 +938,21 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
<Icon size={22} />
|
||||
</div>
|
||||
) : (
|
||||
<Logo />
|
||||
<Group gap="xs" align="center">
|
||||
<Logo />
|
||||
{isUTC && (
|
||||
<Badge
|
||||
size="xs"
|
||||
color="gray"
|
||||
bg="gray.8"
|
||||
variant="light"
|
||||
fw="normal"
|
||||
title="Showing time in UTC"
|
||||
>
|
||||
UTC
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Link>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import cx from 'classnames';
|
||||
import { add, format } from 'date-fns';
|
||||
import { add } from 'date-fns';
|
||||
import { withErrorBoundary } from 'react-error-boundary';
|
||||
import {
|
||||
Bar,
|
||||
|
|
@ -28,9 +28,9 @@ import {
|
|||
seriesToUrlSearchQueryParam,
|
||||
} from './ChartUtils';
|
||||
import type { ChartSeries, NumberFormat } from './types';
|
||||
import { useUserPreferences } from './useUserPreferences';
|
||||
import { FormatTime, useFormatTime } from './useFormatTime';
|
||||
import { formatNumber } from './utils';
|
||||
import { semanticKeyedColor, TIME_TOKENS, truncateMiddle } from './utils';
|
||||
import { semanticKeyedColor, truncateMiddle } from './utils';
|
||||
|
||||
import styles from '../styles/HDXLineChart.module.scss';
|
||||
|
||||
|
|
@ -38,16 +38,12 @@ const MAX_LEGEND_ITEMS = 4;
|
|||
|
||||
const HDXLineChartTooltip = withErrorBoundary(
|
||||
memo((props: any) => {
|
||||
const {
|
||||
userPreferences: { timeFormat },
|
||||
} = useUserPreferences();
|
||||
const tsFormat = TIME_TOKENS[timeFormat];
|
||||
const { active, payload, label, numberFormat } = props;
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className={styles.chartTooltip}>
|
||||
<div className={styles.chartTooltipHeader}>
|
||||
{format(new Date(label * 1000), tsFormat)}
|
||||
<FormatTime value={label * 1000} />
|
||||
</div>
|
||||
<div className={styles.chartTooltipContent}>
|
||||
{payload
|
||||
|
|
@ -221,11 +217,14 @@ const MemoChart = memo(function MemoChart({
|
|||
}, [groupKeys, graphResults, displayType, lineNames, lineColors]);
|
||||
|
||||
const sizeRef = useRef<[number, number]>([0, 0]);
|
||||
const {
|
||||
userPreferences: { timeFormat },
|
||||
} = useUserPreferences();
|
||||
const tsFormat = TIME_TOKENS[timeFormat];
|
||||
// Gets the preffered time format from User Preferences, then converts it to a formattable token
|
||||
|
||||
const formatTime = useFormatTime();
|
||||
const xTickFormatter = useCallback(
|
||||
(value: number) => {
|
||||
return formatTime(value * 1000);
|
||||
},
|
||||
[formatTime],
|
||||
);
|
||||
|
||||
const tickFormatter = useCallback(
|
||||
(value: number) =>
|
||||
|
|
@ -290,7 +289,7 @@ const MemoChart = memo(function MemoChart({
|
|||
interval="preserveStartEnd"
|
||||
scale="time"
|
||||
type="number"
|
||||
tickFormatter={tick => format(new Date(tick * 1000), tsFormat)}
|
||||
tickFormatter={xTickFormatter}
|
||||
minTickGap={50}
|
||||
tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import TimelineChart from './TimelineChart';
|
|||
import { dateRangeToString } from './timeQuery';
|
||||
import type { StacktraceBreadcrumb, StacktraceFrame } from './types';
|
||||
import { Dictionary } from './types';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import {
|
||||
formatDistanceToNowStrictShort,
|
||||
useFirstNonNullValue,
|
||||
|
|
@ -664,10 +665,7 @@ function TraceSubpanel({
|
|||
</>
|
||||
)}
|
||||
<span className="text-muted">at</span>{' '}
|
||||
{format(
|
||||
new Date(selectedLogData.timestamp),
|
||||
'MMM d HH:mm:ss.SSS',
|
||||
)}
|
||||
<FormatTime value={selectedLogData.timestamp} format="withMs" />
|
||||
</div>
|
||||
{isNetworkRequestSpan({ logData: selectedLogData }) && (
|
||||
<ErrorBoundary
|
||||
|
|
@ -1549,10 +1547,7 @@ function PropertySubpanel({
|
|||
{isException && (
|
||||
<span className="text-danger me-2">Exception</span>
|
||||
)}
|
||||
{format(
|
||||
new Date(event.timestamp / 1000),
|
||||
'MMM d HH:mm:ss.SSS',
|
||||
)}
|
||||
<FormatTime value={event.timestamp / 1000} format="withMs" />
|
||||
</div>
|
||||
{isException ? (
|
||||
<ExceptionEvent
|
||||
|
|
@ -2059,7 +2054,7 @@ function SidePanelHeader({
|
|||
) : null}
|
||||
<span className="me-2">
|
||||
<span className="text-muted">at</span>{' '}
|
||||
{format(new Date(logData.timestamp), 'MMM d HH:mm:ss.SSS')}{' '}
|
||||
<FormatTime value={logData.timestamp} format="withMs" />{' '}
|
||||
<span className="text-muted">
|
||||
·{' '}
|
||||
{formatDistanceToNowStrictShort(new Date(logData.timestamp))} ago
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import HyperJson from './components/HyperJson';
|
|||
import { TableCellButton } from './components/Table';
|
||||
import { UNDEFINED_WIDTH } from './tableUtils';
|
||||
import type { StacktraceBreadcrumb, StacktraceFrame } from './types';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import { useLocalStorage } from './utils';
|
||||
|
||||
import styles from '../styles/LogSidePanel.module.scss';
|
||||
|
|
@ -286,7 +287,7 @@ export const breadcrumbColumns: ColumnDef<StacktraceBreadcrumb>[] = [
|
|||
size: 220,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-slate-500">
|
||||
{format(new Date(row.original.timestamp * 1000), 'MMM d HH:mm:ss.SSS')}
|
||||
<FormatTime value={row.original.timestamp * 1000} format="withMs" />
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import curry from 'lodash/curry';
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
import { CSVLink } from 'react-csv';
|
||||
|
|
@ -28,9 +26,9 @@ import InstallInstructionsModal from './InstallInstructionsModal';
|
|||
import LogLevel from './LogLevel';
|
||||
import { useSearchEventStream } from './search';
|
||||
import { UNDEFINED_WIDTH } from './tableUtils';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import { useUserPreferences } from './useUserPreferences';
|
||||
import { useLocalStorage, usePrevious, useWindowSize } from './utils';
|
||||
import { TIME_TOKENS } from './utils';
|
||||
|
||||
import styles from '../styles/LogTable.module.scss';
|
||||
type Row = Record<string, any> & { duration: number };
|
||||
|
|
@ -273,15 +271,13 @@ export const RawLogTable = memo(
|
|||
const { width } = useWindowSize();
|
||||
const isSmallScreen = (width ?? 1000) < 900;
|
||||
const {
|
||||
userPreferences: { timeFormat, isUTC },
|
||||
userPreferences: { isUTC },
|
||||
} = useUserPreferences();
|
||||
const tsFormat = TIME_TOKENS[timeFormat];
|
||||
|
||||
const [columnSizeStorage, setColumnSizeStorage] = useLocalStorage<
|
||||
Record<string, number>
|
||||
>(`${tableId}-column-sizes`, {});
|
||||
|
||||
const tsShortFormat = 'HH:mm:ss';
|
||||
//once the user has scrolled within 500px of the bottom of the table, fetch more data if there is any
|
||||
const FETCH_NEXT_PAGE_PX = 500;
|
||||
|
||||
|
|
@ -337,13 +333,10 @@ export const RawLogTable = memo(
|
|||
const date = new Date(info.getValue<string>());
|
||||
return (
|
||||
<span className="text-muted">
|
||||
{isUTC
|
||||
? formatInTimeZone(
|
||||
date,
|
||||
'Etc/UTC',
|
||||
isSmallScreen ? tsShortFormat : tsFormat,
|
||||
)
|
||||
: format(date, isSmallScreen ? tsShortFormat : tsFormat)}
|
||||
<FormatTime
|
||||
value={date}
|
||||
format={isSmallScreen ? 'short' : 'withMs'}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
@ -436,7 +429,6 @@ export const RawLogTable = memo(
|
|||
columnSizeStorage,
|
||||
showServiceColumn,
|
||||
columnNameMap,
|
||||
tsFormat,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import { Group } from '@mantine/core';
|
||||
|
|
@ -8,34 +7,13 @@ import Checkbox from './Checkbox';
|
|||
import type { PlaybarMarker } from './PlaybarSlider';
|
||||
import { PlaybarSlider } from './PlaybarSlider';
|
||||
import { useSessionEvents } from './sessionUtils';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import { getShortUrl, useLocalStorage } from './utils';
|
||||
|
||||
import 'react-bootstrap-range-slider/dist/react-bootstrap-range-slider.css';
|
||||
|
||||
function formatTs({
|
||||
ts,
|
||||
minTs,
|
||||
showRelativeTime,
|
||||
}: {
|
||||
ts: number | null | undefined;
|
||||
minTs: number;
|
||||
showRelativeTime: boolean;
|
||||
}) {
|
||||
if (ts == null) {
|
||||
return '--:--';
|
||||
} else if (showRelativeTime) {
|
||||
const value = Math.max(ts - minTs, 0);
|
||||
const minutes = Math.floor(value / 1000 / 60);
|
||||
const seconds = Math.floor((value / 1000) % 60);
|
||||
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
} else {
|
||||
try {
|
||||
return format(new Date(ts), 'hh:mm:ss a');
|
||||
} catch (err) {
|
||||
console.error(err, ts);
|
||||
return '--:--';
|
||||
}
|
||||
}
|
||||
function formatRelativeTime(seconds: number) {
|
||||
const minutes = Math.floor(Math.max(seconds, 0) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
||||
}
|
||||
|
||||
export default function Playbar({
|
||||
|
|
@ -185,7 +163,11 @@ export default function Playbar({
|
|||
setShowRelativeTime(!showRelativeTime);
|
||||
}}
|
||||
>
|
||||
{formatTs({ ts: focus?.ts, minTs, showRelativeTime })}
|
||||
{showRelativeTime ? (
|
||||
formatRelativeTime((focus?.ts || 0) - minTs / 1000)
|
||||
) : (
|
||||
<FormatTime value={focus?.ts} format="short" />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-100 d-flex align-self-stretch align-items-center me-3">
|
||||
<PlaybarSlider
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import { format } from 'date-fns';
|
||||
import { Slider, Tooltip } from '@mantine/core';
|
||||
|
||||
import { useFormatTime } from './useFormatTime';
|
||||
import { truncateText } from './utils';
|
||||
|
||||
import styles from '../styles/PlaybarSlider.module.scss';
|
||||
|
|
@ -32,16 +33,18 @@ export const PlaybarSlider = ({
|
|||
onChange,
|
||||
setPlayerState,
|
||||
}: PlaybarSliderProps) => {
|
||||
const formatTime = useFormatTime();
|
||||
|
||||
const valueLabelFormat = React.useCallback(
|
||||
(ts: number) => {
|
||||
const value = Math.max(ts - min, 0);
|
||||
const minutes = Math.floor(value / 1000 / 60);
|
||||
const seconds = Math.floor((value / 1000) % 60);
|
||||
const timestamp = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||
const time = format(new Date(ts), 'hh:mm:ss a');
|
||||
const time = formatTime(ts, { format: 'short' });
|
||||
return `${timestamp} at ${time}`;
|
||||
},
|
||||
[min],
|
||||
[formatTime, min],
|
||||
);
|
||||
|
||||
const markersContent = React.useMemo(
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import Head from 'next/head';
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import cx from 'classnames';
|
||||
import { clamp, format, sub } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import { clamp, sub } from 'date-fns';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
|
@ -48,32 +47,18 @@ import SearchTimeRangePicker from './SearchTimeRangePicker';
|
|||
import { Tags } from './Tags';
|
||||
import { useTimeQuery } from './timeQuery';
|
||||
import { useDisplayedColumns } from './useDisplayedColumns';
|
||||
import { useUserPreferences } from './useUserPreferences';
|
||||
import { FormatTime, useFormatTime } from './useFormatTime';
|
||||
|
||||
import 'react-modern-drawer/dist/index.css';
|
||||
import styles from '../styles/SearchPage.module.scss';
|
||||
|
||||
const formatDate = (
|
||||
date: Date,
|
||||
isUTC: boolean,
|
||||
strFormat = 'MMM d HH:mm:ss',
|
||||
) => {
|
||||
return isUTC
|
||||
? formatInTimeZone(date, 'Etc/UTC', strFormat)
|
||||
: format(date, strFormat);
|
||||
};
|
||||
const dateRangeToString = (range: [Date, Date], isUTC: boolean) => {
|
||||
return `${formatDate(range[0], isUTC)} - ${formatDate(range[1], isUTC)}`;
|
||||
};
|
||||
|
||||
const HistogramBarChartTooltip = (props: any) => {
|
||||
const tsFormat = 'MMM d HH:mm:ss.SSS';
|
||||
const { active, payload, label } = props;
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-grey px-3 py-2 rounded fs-8">
|
||||
<div className="mb-2">
|
||||
{formatDate(new Date(label * 1000), props.isUTC, tsFormat)}
|
||||
<FormatTime value={label * 1000} format="withMs" />
|
||||
</div>
|
||||
{payload.map((p: any) => (
|
||||
<div key={p.name} style={{ color: p.color }}>
|
||||
|
|
@ -99,10 +84,6 @@ const HDXHistogram = memo(
|
|||
onTimeRangeSelect: (start: Date, end: Date) => void;
|
||||
isLive: boolean;
|
||||
}) => {
|
||||
const {
|
||||
userPreferences: { isUTC },
|
||||
} = useUserPreferences();
|
||||
|
||||
const { data: histogramResults, isLoading: isHistogramResultsLoading } =
|
||||
api.useLogHistogram(
|
||||
where,
|
||||
|
|
@ -133,6 +114,8 @@ const HDXHistogram = memo(
|
|||
const [highlightStart, setHighlightStart] = useState<string | undefined>();
|
||||
const [highlightEnd, setHighlightEnd] = useState<string | undefined>();
|
||||
|
||||
const formatTime = useFormatTime();
|
||||
|
||||
return isHistogramResultsLoading ? (
|
||||
<div className="w-100 h-100 d-flex align-items-center justify-content-center">
|
||||
Loading Graph...
|
||||
|
|
@ -202,9 +185,7 @@ const HDXHistogram = memo(
|
|||
interval="preserveStartEnd"
|
||||
scale="time"
|
||||
type="number"
|
||||
tickFormatter={tick =>
|
||||
formatDate(new Date(tick * 1000), isUTC, 'MMM d HH:mm')
|
||||
}
|
||||
tickFormatter={tick => formatTime(tick * 1000, { format: 'short' })}
|
||||
minTickGap={50}
|
||||
tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }}
|
||||
/>
|
||||
|
|
@ -459,10 +440,6 @@ function SearchPage() {
|
|||
[searchInput],
|
||||
);
|
||||
|
||||
const {
|
||||
userPreferences: { isUTC },
|
||||
} = useUserPreferences();
|
||||
|
||||
const [saveSearchModalMode, setSaveSearchModalMode] = useState<
|
||||
'update' | 'save' | 'hidden'
|
||||
>('hidden');
|
||||
|
|
@ -566,6 +543,7 @@ function SearchPage() {
|
|||
[setDisplayedSearchQuery, doSearch, displayedTimeInputValue],
|
||||
);
|
||||
|
||||
const formatTime = useFormatTime();
|
||||
const generateSearchUrl = useCallback(
|
||||
(newQuery?: string, newTimeRange?: [Date, Date], lid?: string) => {
|
||||
const fromDate = newTimeRange ? newTimeRange[0] : searchedTimeRange[0];
|
||||
|
|
@ -574,14 +552,14 @@ function SearchPage() {
|
|||
q: newQuery ?? searchedQuery,
|
||||
from: fromDate.getTime().toString(),
|
||||
to: toDate.getTime().toString(),
|
||||
tq: dateRangeToString([fromDate, toDate], isUTC),
|
||||
tq: `${formatTime(fromDate)} - ${formatTime(toDate)}`,
|
||||
...(lid ? { lid } : {}),
|
||||
});
|
||||
return `/search${
|
||||
selectedSavedSearch != null ? `/${selectedSavedSearch._id}` : ''
|
||||
}?${qparams.toString()}`;
|
||||
},
|
||||
[searchedQuery, searchedTimeRange, selectedSavedSearch, isUTC],
|
||||
[searchedQuery, searchedTimeRange, selectedSavedSearch, formatTime],
|
||||
);
|
||||
|
||||
const generateChartUrl = useCallback(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
|
@ -11,7 +10,9 @@ import Playbar from './Playbar';
|
|||
import SearchInput from './SearchInput';
|
||||
import { useSessionEvents } from './sessionUtils';
|
||||
import TabBar from './TabBar';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import { getShortUrl, usePrevious } from './utils';
|
||||
|
||||
function SessionEventList({
|
||||
config: { where, dateRange },
|
||||
onClick,
|
||||
|
|
@ -229,7 +230,7 @@ function SessionEventList({
|
|||
onClick={() => onTimeClick(row.timestamp)}
|
||||
>
|
||||
<i className="bi bi-play-fill me-1" />
|
||||
{format(new Date(row.timestamp), 'hh:mm:ss a')}
|
||||
<FormatTime value={row.timestamp} format="short" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,8 @@ import SearchInput from './SearchInput';
|
|||
import SearchTimeRangePicker from './SearchTimeRangePicker';
|
||||
import SessionSidePanel from './SessionSidePanel';
|
||||
import { parseTimeQuery, useTimeQuery } from './timeQuery';
|
||||
import {
|
||||
formatDistanceToNowStrictShort,
|
||||
formatHumanReadableDate,
|
||||
} from './utils';
|
||||
import { FormatTime } from './useFormatTime';
|
||||
import { formatDistanceToNowStrictShort } from './utils';
|
||||
|
||||
function SessionCard({
|
||||
email,
|
||||
|
|
@ -74,7 +72,7 @@ function SessionCard({
|
|||
<div className="text-end">
|
||||
<div>Last active {timeAgo} ago</div>
|
||||
<div className="text-muted fs-8 mt-1">
|
||||
Started on {formatHumanReadableDate(minTime)}
|
||||
Started on <FormatTime value={minTime} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -113,10 +113,7 @@ export const UserPreferencesModal = ({
|
|||
allowDeselect={false}
|
||||
/>
|
||||
</SettingContainer>
|
||||
<SettingContainer
|
||||
label="Use UTC time in log tables"
|
||||
description="Currently only applied to log table timestamps. Charts and other areas will be updated soon"
|
||||
>
|
||||
<SettingContainer label="Use UTC time">
|
||||
<Switch
|
||||
size="md"
|
||||
onLabel="UTC"
|
||||
|
|
|
|||
45
packages/app/src/__test__/utils.test.ts
Normal file
45
packages/app/src/__test__/utils.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { formatDate } from '../utils';
|
||||
|
||||
describe('utils', () => {
|
||||
it('12h utc', () => {
|
||||
const date = new Date('2021-01-01T12:00:00Z');
|
||||
expect(
|
||||
formatDate(date, {
|
||||
clock: '12h',
|
||||
isUTC: true,
|
||||
}),
|
||||
).toEqual('Jan 1 12:00:00 PM');
|
||||
});
|
||||
|
||||
it('24h utc', () => {
|
||||
const date = new Date('2021-01-01T12:00:00Z');
|
||||
expect(
|
||||
formatDate(date, {
|
||||
clock: '24h',
|
||||
isUTC: true,
|
||||
format: 'withMs',
|
||||
}),
|
||||
).toEqual('Jan 1 12:00:00.000');
|
||||
});
|
||||
|
||||
it('12h local', () => {
|
||||
const date = new Date('2021-01-01T12:00:00');
|
||||
expect(
|
||||
formatDate(date, {
|
||||
clock: '12h',
|
||||
isUTC: false,
|
||||
}),
|
||||
).toEqual('Jan 1 12:00:00 PM');
|
||||
});
|
||||
|
||||
it('24h local', () => {
|
||||
const date = new Date('2021-01-01T12:00:00');
|
||||
expect(
|
||||
formatDate(date, {
|
||||
clock: '24h',
|
||||
isUTC: false,
|
||||
format: 'withMs',
|
||||
}),
|
||||
).toEqual('Jan 1 12:00:00.000');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { format, sub } from 'date-fns';
|
||||
import { sub } from 'date-fns';
|
||||
import { Anchor, Badge, Group, Text, Timeline } from '@mantine/core';
|
||||
|
||||
import api from '../api';
|
||||
import { KubePhase } from '../types';
|
||||
import { FormatTime } from '../useFormatTime';
|
||||
|
||||
type KubeEvent = {
|
||||
id: string;
|
||||
|
|
@ -21,8 +22,6 @@ type AnchorEvent = {
|
|||
label: React.ReactNode;
|
||||
};
|
||||
|
||||
const FORMAT = 'MMM d hh:mm:ss a';
|
||||
|
||||
const renderKubeEvent = (event: KubeEvent) => {
|
||||
let href = '#';
|
||||
try {
|
||||
|
|
@ -39,7 +38,7 @@ const renderKubeEvent = (event: KubeEvent) => {
|
|||
<Timeline.Item key={event.id}>
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<Anchor size="11" c="gray.6" title={event.timestamp}>
|
||||
{format(new Date(event.timestamp), FORMAT)}
|
||||
<FormatTime value={event.timestamp} />
|
||||
</Anchor>
|
||||
</Link>
|
||||
<Group gap="xs" my={4}>
|
||||
|
|
@ -152,7 +151,7 @@ export const KubeTimeline = ({
|
|||
{podEventsAfterAnchor.map(renderKubeEvent)}
|
||||
<Timeline.Item key={anchorEvent.timestamp} ref={anchorRef}>
|
||||
<Text size="11" c="gray.6" title={anchorEvent.timestamp}>
|
||||
{format(new Date(anchorEvent.timestamp), FORMAT)}
|
||||
<FormatTime value={anchorEvent.timestamp} />
|
||||
</Text>
|
||||
<Group gap="xs" my={4}>
|
||||
<Text size="12" c="white" fw="bold">
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const formatDate = (
|
|||
? formatInTimeZone(date, 'Etc/UTC', strFormat)
|
||||
: format(date, strFormat);
|
||||
};
|
||||
|
||||
export const dateRangeToString = (range: [Date, Date], isUTC: boolean) => {
|
||||
return `${formatDate(range[0], isUTC)} - ${formatDate(range[1], isUTC)}`;
|
||||
};
|
||||
|
|
|
|||
54
packages/app/src/useFormatTime.tsx
Normal file
54
packages/app/src/useFormatTime.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useUserPreferences } from './useUserPreferences';
|
||||
import { formatDate } from './utils';
|
||||
|
||||
type DateLike = number | string | Date;
|
||||
|
||||
type DateFormat = 'normal' | 'short' | 'withMs';
|
||||
|
||||
const parse = (time: DateLike) => {
|
||||
if (time instanceof Date) {
|
||||
return time;
|
||||
}
|
||||
return new Date(time);
|
||||
};
|
||||
|
||||
export const useFormatTime = () => {
|
||||
const {
|
||||
userPreferences: { isUTC, timeFormat },
|
||||
} = useUserPreferences();
|
||||
|
||||
return React.useCallback(
|
||||
(time: DateLike, { format }: { format?: DateFormat } | undefined = {}) => {
|
||||
try {
|
||||
const date = parse(time);
|
||||
return formatDate(date, {
|
||||
clock: timeFormat,
|
||||
isUTC,
|
||||
format,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err, time);
|
||||
return 'Unknown date';
|
||||
}
|
||||
},
|
||||
[isUTC, timeFormat],
|
||||
);
|
||||
};
|
||||
|
||||
export const FormatTime = ({
|
||||
value,
|
||||
format,
|
||||
}: {
|
||||
value?: DateLike;
|
||||
format?: DateFormat;
|
||||
}) => {
|
||||
const formatTime = useFormatTime();
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{formatTime(value, { format })}</>;
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { format as fnsFormat, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import numbro from 'numbro';
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
|
|
@ -161,11 +162,6 @@ export const useDebounce = <T,>(
|
|||
return debouncedValue;
|
||||
};
|
||||
|
||||
export const TIME_TOKENS = {
|
||||
'12h': 'MMM d h:mm:ss a',
|
||||
'24h': 'MMM d HH:mm:ss.SSS',
|
||||
};
|
||||
|
||||
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
|
|
@ -241,10 +237,6 @@ export function formatDistanceToNowStrictShort(date: Date) {
|
|||
.replace(' seconds', 's');
|
||||
}
|
||||
|
||||
export function formatHumanReadableDate(date: Date) {
|
||||
return fnsFormat(date, 'MMMM d, h:mmaaa');
|
||||
}
|
||||
|
||||
export const getLogLevelClass = (lvl: string | undefined) => {
|
||||
const level = lvl?.toLowerCase();
|
||||
if (level == null) {
|
||||
|
|
@ -463,3 +455,38 @@ export const legacyMetricNameToNameAndDataType = (metricName?: string) => {
|
|||
dataType: mDataType as MetricsDataType,
|
||||
};
|
||||
};
|
||||
|
||||
// Date formatting
|
||||
const TIME_TOKENS = {
|
||||
normal: {
|
||||
'12h': 'MMM d h:mm:ss a',
|
||||
'24h': 'MMM d HH:mm:ss',
|
||||
},
|
||||
short: {
|
||||
'12h': 'MMM d h:mma',
|
||||
'24h': 'MMM d HH:mm',
|
||||
},
|
||||
withMs: {
|
||||
'12h': 'MMM d h:mm:ss.SSS a',
|
||||
'24h': 'MMM d HH:mm:ss.SSS',
|
||||
},
|
||||
};
|
||||
|
||||
export const formatDate = (
|
||||
date: Date,
|
||||
{
|
||||
isUTC = false,
|
||||
format = 'normal',
|
||||
clock = '12h',
|
||||
}: {
|
||||
isUTC?: boolean;
|
||||
format?: 'normal' | 'short' | 'withMs';
|
||||
clock?: '12h' | '24h';
|
||||
},
|
||||
) => {
|
||||
const formatStr = TIME_TOKENS[format][clock];
|
||||
|
||||
return isUTC
|
||||
? formatInTimeZone(date, 'Etc/UTC', formatStr)
|
||||
: fnsFormat(date, formatStr);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue