Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/components/shared/HoverPopper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { PopperProps } from '@mui/material';
import type { MouseEvent, ReactNode } from 'react';

import { useEffect, useRef, useState } from 'react';

import { Box } from '@mui/material';

import PopperWrapper from 'src/components/shared/PopperWrapper';

interface Props {
children: ReactNode;
popperContent: ReactNode;
popperProps?: Partial<PopperProps>;
}

// Delay before opening so quick mouse-overs don't flash the popper.
const OPEN_DELAY_MS = 35;

// Delay before closing so the mouse can travel from the anchor into the popper
// without it disappearing. The popper renders in a portal so there is no DOM
// parent/child relationship between the two.
const CLOSE_DELAY_MS = 100;

function HoverPopper({ children, popperContent, popperProps }: Props) {
const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

const clearOpenTimer = () => {
if (openTimer.current !== null) {
clearTimeout(openTimer.current);
openTimer.current = null;
}
};

const clearCloseTimer = () => {
if (closeTimer.current !== null) {
clearTimeout(closeTimer.current);
closeTimer.current = null;
}
};

const scheduleClose = () => {
clearOpenTimer();
clearCloseTimer();
closeTimer.current = setTimeout(() => setOpen(false), CLOSE_DELAY_MS);
};

const handleAnchorEnter = (event: MouseEvent<HTMLElement>) => {
clearCloseTimer();
const target = event.currentTarget;
openTimer.current = setTimeout(() => {
setAnchorEl(target);
setOpen(true);
}, OPEN_DELAY_MS);
};

useEffect(
() => () => {
clearOpenTimer();
clearCloseTimer();
},
[]
);

return (
<>
<Box
component="span"
onMouseEnter={handleAnchorEnter}
onMouseLeave={scheduleClose}
>
{children}
</Box>
<PopperWrapper
anchorEl={anchorEl}
open={open}
setOpen={setOpen}
popperProps={popperProps}
>
<Box
onMouseEnter={clearCloseTimer}
onMouseLeave={scheduleClose}
>
{popperContent}
</Box>
</PopperWrapper>
</>
);
}

export default HoverPopper;
1 change: 1 addition & 0 deletions src/components/shared/PopperWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function PopperWrapper({
open={open}
anchorEl={anchorEl}
transition
modifiers={popperProps?.modifiers ?? []}
sx={
popperProps?.sx
? { ...popperProps.sx, zIndex: popperIndex }
Expand Down
85 changes: 85 additions & 0 deletions src/components/shared/TimestampPopperContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { DateTime } from 'luxon';

import { Fragment } from 'react';

import { Box, Divider, Typography } from '@mui/material';

import { useIntl } from 'react-intl';

import SingleLineCode from 'src/components/content/SingleLineCode';
import { LOGS_DATE_FORMAT } from 'src/components/tables/cells/logs/shared';

interface Props {
dateTime: DateTime;
showRelative?: boolean;
}

const labelSx = {
color: 'text.secondary',
fontWeight: 500,
textTransform: 'uppercase',
} as const;

interface Row {
labelId: string;
getValue: (dt: DateTime) => string;
}

const rows: Row[] = [
{
labelId: 'common.timestamp.local.label',
getValue: (dt) => dt.toLocal().toFormat(LOGS_DATE_FORMAT),
},
{
labelId: 'common.timestamp.utc.label',
getValue: (dt) => dt.toUTC().toFormat(LOGS_DATE_FORMAT),
},
];

function TimestampPopperContent({ dateTime, showRelative }: Props) {
const intl = useIntl();

return (
<Box
onClick={(e) => e.stopPropagation()} // Make sure we stop otherwise the row is opened
component="dl"
sx={{
'alignItems': 'baseline',
'display': 'grid',
'gap': '2px 12px',
'gridTemplateColumns': 'auto 1fr',
'm': 0,
'& dd': { m: 0 },
}}
>
{rows.map(({ labelId, getValue }) => (
<Fragment key={labelId}>
<Typography component="dt" variant="caption" sx={labelSx}>
{intl.formatMessage({ id: labelId })}
</Typography>

<dd>
<SingleLineCode compact value={getValue(dateTime)} />
</dd>
</Fragment>
))}
{showRelative ? (
<>
<Divider sx={{ gridColumn: '1 / -1', my: 0.5 }} />
<Typography component="dt" />
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't perfect - but makes this line sliiiiightly more semantic

<Typography
component="dd"
sx={{
m: 0,
textAlign: 'right',
}}
>
{dateTime.toRelative({ style: 'narrow' })}
</Typography>
</>
) : null}
</Box>
);
}

export default TimestampPopperContent;
31 changes: 24 additions & 7 deletions src/components/tables/cells/logs/TimestampCell.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import { TableCell, Typography } from '@mui/material';
import { Chip, TableCell } from '@mui/material';

import { DateTime } from 'luxon';

import HoverPopper from 'src/components/shared/HoverPopper';
import TimestampPopperContent from 'src/components/shared/TimestampPopperContent';
import {
BaseCellSx,
BaseTypographySx,
LOGS_DATE_FORMAT,
} from 'src/components/tables/cells/logs/shared';

interface Props {
ts: string;
}

function TimestampCell({ ts }: Props) {
const formattedDateTime = DateTime.fromISO(ts, {
zone: 'UTC',
}).toFormat('yyyy-LL-dd HH:mm:ss.SSS ZZZZ');
const dt = DateTime.fromISO(ts, { zone: 'UTC' });

if (!dt.isValid) {
return <TableCell sx={BaseCellSx} component="div" />;
}

const formattedDateTime = dt.toFormat(LOGS_DATE_FORMAT);

return (
<TableCell sx={BaseCellSx} component="div">
<Typography noWrap sx={BaseTypographySx}>
{formattedDateTime.includes('Invalid') ? '' : formattedDateTime}
</Typography>
<HoverPopper
popperContent={
<TimestampPopperContent dateTime={dt} showRelative />
}
popperProps={{ placement: 'right' }}
>
<Chip
sx={BaseTypographySx}
label={formattedDateTime}
size="small"
variant="outlined"
/>
Comment on lines +34 to +39
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch to chip to maybe drive people to view timestamp as interactable

</HoverPopper>
</TableCell>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/tables/cells/logs/shared.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export const BaseTypographySx = { fontFamily: 'Monospace', textWrap: 'nowrap' };
export const BaseCellSx = { maxWidth: 'min-content', py: 0 };

export const LOGS_DATE_FORMAT = 'yyyy-LL-dd HH:mm:ss.SSS ZZZZ';
4 changes: 4 additions & 0 deletions src/lang/en-US/CommonMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const CommonMessages: Record<string, string> = {
'common.upToDate': `Up-to-date`,
'common.unknown': `Unknown`,

// Timestamp popper labels
'common.timestamp.utc.label': `UTC`,
'common.timestamp.local.label': `Local`,

// Aria
'aria.openExpand': `show more`,
'aria.closeExpand': `show less`,
Expand Down
Loading