mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
parent
43100f9561
commit
6c5ce52470
@ -13,7 +13,7 @@ import CheckCircle from '@mui/icons-material/CheckCircle';
|
||||
import CloudCircle from '@mui/icons-material/CloudCircle';
|
||||
import Flag from '@mui/icons-material/Flag';
|
||||
import WarningAmberRounded from '@mui/icons-material/WarningAmberRounded';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { getApplicationIssues } from './ApplicationIssues/ApplicationIssues';
|
||||
|
||||
@ -305,17 +305,9 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
||||
<tr>
|
||||
<StyledCell>Last seen:</StyledCell>
|
||||
<StyledCell>
|
||||
{environment.lastSeen && (
|
||||
<TimeAgo
|
||||
key={`${environment.lastSeen}`}
|
||||
minPeriod={60}
|
||||
date={
|
||||
new Date(
|
||||
environment.lastSeen,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TimeAgo
|
||||
date={environment.lastSeen}
|
||||
/>
|
||||
</StyledCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -13,7 +13,11 @@ const setupApi = (application: ApplicationOverviewSchema) => {
|
||||
'/api/admin/metrics/applications/my-app/overview',
|
||||
application,
|
||||
);
|
||||
testServerRoute(server, '/api/admin/ui-config', {});
|
||||
testServerRoute(server, '/api/admin/ui-config', {
|
||||
flags: {
|
||||
timeAgoRefactor: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('Display application overview with environments', async () => {
|
||||
@ -51,7 +55,7 @@ test('Display application overview with environments', async () => {
|
||||
await screen.findByText('development environment');
|
||||
await screen.findByText('999');
|
||||
await screen.findByText('unleash-client-node:5.5.0-beta.0');
|
||||
await screen.findByText('0 seconds ago');
|
||||
await screen.findByText('< 1 minute ago');
|
||||
});
|
||||
|
||||
test('Display application overview without environments', async () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { VFC } from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import type { FC } from 'react';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { Tooltip, Typography, useTheme } from '@mui/material';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
@ -9,7 +9,7 @@ interface IFeatureArchivedCellProps {
|
||||
value?: string | Date | null;
|
||||
}
|
||||
|
||||
export const FeatureArchivedCell: VFC<IFeatureArchivedCellProps> = ({
|
||||
export const FeatureArchivedCell: FC<IFeatureArchivedCellProps> = ({
|
||||
value: archivedAt,
|
||||
}) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
@ -37,12 +37,7 @@ export const FeatureArchivedCell: VFC<IFeatureArchivedCellProps> = ({
|
||||
arrow
|
||||
>
|
||||
<Typography noWrap variant='body2' data-loading>
|
||||
<TimeAgo
|
||||
key={`${archivedAt}`}
|
||||
date={new Date(archivedAt)}
|
||||
title=''
|
||||
live={false}
|
||||
/>
|
||||
<TimeAgo date={new Date(archivedAt)} refresh={false} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TextCell>
|
||||
|
@ -2,7 +2,7 @@ import type { FC } from 'react';
|
||||
import { Markdown } from 'component/common/Markdown/Markdown';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { Box, styled, Typography } from '@mui/material';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { StyledAvatar } from './StyledAvatar';
|
||||
import type { IChangeRequestComment } from '../../changeRequest.types';
|
||||
|
||||
@ -35,12 +35,7 @@ export const ChangeRequestComment: FC<{ comment: IChangeRequestComment }> = ({
|
||||
<Box>
|
||||
<strong>{comment.createdBy.username}</strong>{' '}
|
||||
<Typography color='text.secondary' component='span'>
|
||||
commented{' '}
|
||||
<TimeAgo
|
||||
key={`${comment.createdAt}`}
|
||||
minPeriod={60}
|
||||
date={new Date(comment.createdAt)}
|
||||
/>
|
||||
commented <TimeAgo date={comment.createdAt} />
|
||||
</Typography>
|
||||
</Box>
|
||||
</CommentHeader>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { type FC, useState } from 'react';
|
||||
import { Typography, Tooltip } from '@mui/material';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
|
||||
import { ChangeRequestStatusBadge } from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge';
|
||||
import {
|
||||
@ -38,13 +38,7 @@ export const ChangeRequestHeader: FC<{ changeRequest: ChangeRequestType }> = ({
|
||||
margin: theme.spacing('auto', 0, 'auto', 2),
|
||||
})}
|
||||
>
|
||||
Created{' '}
|
||||
<TimeAgo
|
||||
key={`${changeRequest.createdAt}`}
|
||||
minPeriod={60}
|
||||
date={new Date(changeRequest.createdAt)}
|
||||
/>{' '}
|
||||
by
|
||||
Created <TimeAgo date={changeRequest.createdAt} /> by
|
||||
</Typography>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
|
@ -11,7 +11,7 @@ import type {
|
||||
NotificationsSchemaItemNotificationType,
|
||||
} from 'openapi';
|
||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import ToggleOffOutlined from '@mui/icons-material/ToggleOffOutlined';
|
||||
import { flexRow } from 'themes/themeStyles';
|
||||
|
||||
@ -157,11 +157,7 @@ export const Notification = ({
|
||||
</StyledUserContainer>
|
||||
|
||||
<StyledTimeAgoTypography>
|
||||
<TimeAgo
|
||||
key={`${notification.createdAt}`}
|
||||
date={new Date(notification.createdAt)}
|
||||
minPeriod={60}
|
||||
/>
|
||||
<TimeAgo date={notification.createdAt} />
|
||||
</StyledTimeAgoTypography>
|
||||
</StyledSecondaryInfoBox>
|
||||
</StyledNotificationMessageBox>
|
||||
|
@ -1,131 +0,0 @@
|
||||
import type React from 'react';
|
||||
import type { FC, VFC } from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { styled, Tooltip, useTheme } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
function shortenUnitName(unit?: string): string {
|
||||
switch (unit) {
|
||||
case 'second':
|
||||
return 's';
|
||||
case 'minute':
|
||||
return 'm';
|
||||
case 'hour':
|
||||
return 'h';
|
||||
case 'day':
|
||||
return 'D';
|
||||
case 'week':
|
||||
return 'W';
|
||||
case 'month':
|
||||
return 'M';
|
||||
case 'year':
|
||||
return 'Y';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const useFeatureColor = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (unit?: string): string => {
|
||||
switch (unit) {
|
||||
case 'second':
|
||||
return theme.palette.seen.recent;
|
||||
case 'minute':
|
||||
return theme.palette.seen.recent;
|
||||
case 'hour':
|
||||
return theme.palette.seen.recent;
|
||||
case 'day':
|
||||
return theme.palette.seen.recent;
|
||||
case 'week':
|
||||
return theme.palette.seen.inactive;
|
||||
case 'month':
|
||||
return theme.palette.seen.abandoned;
|
||||
case 'year':
|
||||
return theme.palette.seen.abandoned;
|
||||
default:
|
||||
return theme.palette.seen.unknown;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
padding: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledBox = styled('div')(({ theme }) => ({
|
||||
width: '38px',
|
||||
height: '38px',
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
margin: '0 auto',
|
||||
}));
|
||||
|
||||
interface IFeatureSeenCellProps {
|
||||
value?: string | Date | null;
|
||||
}
|
||||
|
||||
const Wrapper: FC<{
|
||||
unit?: string;
|
||||
tooltip: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ unit, tooltip, children }) => {
|
||||
const getColor = useFeatureColor();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Tooltip title={tooltip} arrow describeChild>
|
||||
<StyledBox style={{ background: getColor(unit) }} data-loading>
|
||||
{children}
|
||||
</StyledBox>
|
||||
</Tooltip>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeatureSeenCell: VFC<IFeatureSeenCellProps> = ({
|
||||
value: lastSeenAt,
|
||||
}) => {
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(lastSeenAt)}
|
||||
show={
|
||||
<TimeAgo
|
||||
key={`${lastSeenAt}`}
|
||||
date={lastSeenAt!}
|
||||
title=''
|
||||
live={false}
|
||||
formatter={(
|
||||
value: number,
|
||||
unit: string,
|
||||
suffix: string,
|
||||
) => {
|
||||
return (
|
||||
<Wrapper
|
||||
tooltip={`Last usage reported ${value} ${unit}${
|
||||
value !== 1 ? 's' : ''
|
||||
} ${suffix}`}
|
||||
unit={unit}
|
||||
>
|
||||
{value}
|
||||
{shortenUnitName(unit)}
|
||||
</Wrapper>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<Wrapper tooltip='No usage reported from connected applications'>
|
||||
–
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { styled, type SxProps, type Theme, Typography } from '@mui/material';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import type { ILastSeenEnvironments } from 'interfaces/featureToggle';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useLastSeenColors } from 'component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors';
|
||||
@ -74,10 +74,10 @@ export const LastSeenTooltip = ({
|
||||
...rest
|
||||
}: ILastSeenTooltipProps) => {
|
||||
const getColor = useLastSeenColors();
|
||||
const [, defaultTextColor] = getColor();
|
||||
const environmentsHaveLastSeen = environments?.some((environment) =>
|
||||
Boolean(environment.lastSeenAt),
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDescription {...rest} data-loading>
|
||||
<StyledDescriptionHeader>
|
||||
@ -85,7 +85,9 @@ export const LastSeenTooltip = ({
|
||||
</StyledDescriptionHeader>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
Boolean(environments) && Boolean(environmentsHaveLastSeen)
|
||||
Boolean(environments) &&
|
||||
Boolean(environmentsHaveLastSeen) &&
|
||||
false
|
||||
}
|
||||
show={
|
||||
<StyledListContainer>
|
||||
@ -95,43 +97,15 @@ export const LastSeenTooltip = ({
|
||||
{name}
|
||||
</StyledDescriptionBlockHeader>
|
||||
<StyledValueContainer>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(lastSeenAt)}
|
||||
show={
|
||||
<TimeAgo
|
||||
key={`${lastSeenAt}`}
|
||||
date={lastSeenAt!}
|
||||
title=''
|
||||
live={false}
|
||||
formatter={(
|
||||
value: number,
|
||||
unit: string,
|
||||
suffix: string,
|
||||
) => {
|
||||
const [, textColor] =
|
||||
getColor(unit);
|
||||
return (
|
||||
<StyledValue
|
||||
color={textColor}
|
||||
>
|
||||
{`${value} ${unit}${
|
||||
value !== 1
|
||||
? 's'
|
||||
: ''
|
||||
} ${suffix}`}
|
||||
</StyledValue>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<StyledValue
|
||||
color={defaultTextColor}
|
||||
>
|
||||
no usage
|
||||
</StyledValue>
|
||||
}
|
||||
/>
|
||||
<StyledValue
|
||||
color={getColor(lastSeenAt).text}
|
||||
>
|
||||
<TimeAgo
|
||||
date={lastSeenAt}
|
||||
refresh={false}
|
||||
fallback='no usage'
|
||||
/>
|
||||
</StyledValue>
|
||||
</StyledValueContainer>
|
||||
<LastSeenProgress yes={yes} no={no} />
|
||||
</StyledDescriptionBlock>
|
||||
@ -139,27 +113,12 @@ export const LastSeenTooltip = ({
|
||||
</StyledListContainer>
|
||||
}
|
||||
elseShow={
|
||||
<TimeAgo
|
||||
date={featureLastSeen}
|
||||
title=''
|
||||
live={false}
|
||||
formatter={(
|
||||
value: number,
|
||||
unit: string,
|
||||
suffix: string,
|
||||
) => {
|
||||
return (
|
||||
<Typography
|
||||
fontWeight={'bold'}
|
||||
color={'text.primary'}
|
||||
>
|
||||
{`Reported ${value} ${unit}${
|
||||
value !== 1 ? 's' : ''
|
||||
} ${suffix}`}
|
||||
</Typography>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
fontWeight={'bold'}
|
||||
color={getColor(featureLastSeen).text}
|
||||
>
|
||||
Reported <TimeAgo date={featureLastSeen} />
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<StyledDescriptionSubHeader>
|
||||
|
@ -24,6 +24,11 @@ const StyledWrapper = styled(Box, {
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledSpan = styled('span')(() => ({
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%',
|
||||
}));
|
||||
|
||||
export const TextCell: FC<ITextCellProps> = ({
|
||||
value,
|
||||
children,
|
||||
@ -32,8 +37,8 @@ export const TextCell: FC<ITextCellProps> = ({
|
||||
'data-testid': testid,
|
||||
}) => (
|
||||
<StyledWrapper lineClamp={lineClamp} sx={sx}>
|
||||
<span data-loading='true' data-testid={testid}>
|
||||
<StyledSpan data-loading='true' data-testid={testid}>
|
||||
{children ?? value}
|
||||
</span>
|
||||
</StyledSpan>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import type { FC } from 'react';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { TextCell } from '../TextCell/TextCell';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
|
||||
interface ITimeAgoCellProps {
|
||||
value?: string | number | Date;
|
||||
@ -31,16 +31,15 @@ export const TimeAgoCell: FC<ITimeAgoCellProps> = ({
|
||||
<Tooltip title={title?.(date) ?? date} arrow>
|
||||
<Typography
|
||||
noWrap
|
||||
sx={{
|
||||
display: 'inline-block',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
component='span'
|
||||
variant='body2'
|
||||
data-loading
|
||||
>
|
||||
<TimeAgo
|
||||
key={`${value}`}
|
||||
date={new Date(value)}
|
||||
live={live}
|
||||
title={''}
|
||||
/>
|
||||
<TimeAgo date={value} refresh={live} />
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TextCell>
|
||||
|
139
frontend/src/component/common/TimeAgo/TimeAgo.test.tsx
Normal file
139
frontend/src/component/common/TimeAgo/TimeAgo.test.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { vi } from 'vitest';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { NewTimeAgo as TimeAgo } from './TimeAgo';
|
||||
|
||||
const h = 3_600_000 as const;
|
||||
const min = 60_000 as const;
|
||||
const s = 1_000 as const;
|
||||
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('renders fallback when date is null or undefined', () => {
|
||||
render(<TimeAgo date={null} fallback='N/A' />);
|
||||
expect(screen.getByText('N/A')).toBeInTheDocument();
|
||||
|
||||
render(<TimeAgo date={undefined} fallback='unknown' />);
|
||||
expect(screen.getByText('unknown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('formats date correctly', () => {
|
||||
const date = new Date();
|
||||
render(<TimeAgo date={date} />);
|
||||
expect(screen.getByText('< 1 minute ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('updates time periodically', () => {
|
||||
const date = new Date();
|
||||
render(<TimeAgo date={date} />);
|
||||
|
||||
expect(screen.getByText('< 1 minute ago')).toBeInTheDocument();
|
||||
|
||||
act(() => vi.advanceTimersByTime(61 * s));
|
||||
|
||||
expect(screen.getByText('1 minute ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('stops updating when live is false', () => {
|
||||
const date = new Date();
|
||||
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
||||
render(<TimeAgo date={date} refresh={false} />);
|
||||
|
||||
expect(screen.getByText('< 1 minute ago')).toBeInTheDocument();
|
||||
|
||||
act(() => vi.advanceTimersByTime(61 * s));
|
||||
|
||||
expect(screen.getByText('< 1 minute ago')).toBeInTheDocument();
|
||||
|
||||
expect(setIntervalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles string dates', () => {
|
||||
const dateString = '2024-01-01T00:00:00Z';
|
||||
vi.setSystemTime(new Date('2024-01-01T01:01:00Z'));
|
||||
|
||||
render(<TimeAgo date={dateString} />);
|
||||
expect(screen.getByText('1 hour ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('cleans up interval on unmount', () => {
|
||||
const date = new Date();
|
||||
const { unmount } = render(<TimeAgo date={date} refresh />);
|
||||
|
||||
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
||||
unmount();
|
||||
expect(clearIntervalSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders fallback for invalid date', () => {
|
||||
render(<TimeAgo date='invalid-date' fallback='Invalid date' />);
|
||||
expect(screen.getByText('Invalid date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('on date change, current time should be updated', () => {
|
||||
const start = new Date().getTime();
|
||||
|
||||
vi.advanceTimersByTime(60_000);
|
||||
|
||||
const Component = ({ date }: { date: number }) => (
|
||||
<>
|
||||
<TimeAgo date={date} />
|
||||
</>
|
||||
);
|
||||
|
||||
const { rerender } = render(<Component date={start} />);
|
||||
expect(screen.getByText('1 minute ago')).toBeInTheDocument();
|
||||
|
||||
act(() => vi.advanceTimersByTime(2 * min));
|
||||
rerender(<Component date={start} />);
|
||||
|
||||
expect(screen.getByText('3 minutes ago')).toBeInTheDocument();
|
||||
|
||||
rerender(<Component date={start + min} />);
|
||||
|
||||
expect(screen.getByText('2 minutes ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should refresh on fallback change', () => {
|
||||
const date = null;
|
||||
const { rerender } = render(
|
||||
<TimeAgo date={date} fallback='Initial fallback' />,
|
||||
);
|
||||
expect(screen.getByText('Initial fallback')).toBeInTheDocument();
|
||||
|
||||
rerender(<TimeAgo date={date} fallback='Updated fallback' />);
|
||||
expect(screen.getByText('Updated fallback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should create `time` element', () => {
|
||||
const now = 1724222592978;
|
||||
vi.setSystemTime(now);
|
||||
const { container } = render(<TimeAgo date={now - 3 * min} />);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<time
|
||||
datetime="2024-08-21T06:40:12.978Z"
|
||||
>
|
||||
3 minutes ago
|
||||
</time>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('should not create `time` element if `timeElement` is false', () => {
|
||||
const now = 1724222592978;
|
||||
vi.setSystemTime(now);
|
||||
const { container } = render(
|
||||
<TimeAgo date={now - 5 * h} timeElement={false} />,
|
||||
);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
5 hours ago
|
||||
</div>
|
||||
`);
|
||||
});
|
73
frontend/src/component/common/TimeAgo/TimeAgo.tsx
Normal file
73
frontend/src/component/common/TimeAgo/TimeAgo.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { formatDistanceToNow, secondsToMilliseconds } from 'date-fns';
|
||||
import { default as LegacyTimeAgo } from 'react-timeago';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
type TimeAgoProps = {
|
||||
date: Date | number | string | null | undefined;
|
||||
fallback?: string;
|
||||
refresh?: boolean;
|
||||
timeElement?: boolean;
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: string | number | Date) =>
|
||||
formatDistanceToNow(new Date(date), {
|
||||
addSuffix: true,
|
||||
})
|
||||
.replace('about ', '')
|
||||
.replace('less than a minute ago', '< 1 minute ago');
|
||||
|
||||
export const TimeAgo: FC<TimeAgoProps> = ({ ...props }) => {
|
||||
const { date, fallback, refresh } = props;
|
||||
const timeAgoRefactorEnabled = useUiFlag('timeAgoRefactor');
|
||||
|
||||
if (timeAgoRefactorEnabled) return <NewTimeAgo {...props} />;
|
||||
if (!date) return fallback;
|
||||
return (
|
||||
<LegacyTimeAgo key={`${date}`} date={new Date(date)} live={refresh} />
|
||||
);
|
||||
};
|
||||
|
||||
export const NewTimeAgo: FC<TimeAgoProps> = ({
|
||||
date,
|
||||
fallback = '',
|
||||
refresh = true,
|
||||
timeElement = true,
|
||||
}) => {
|
||||
const getValue = (): { description: string; dateTime?: Date } => {
|
||||
try {
|
||||
if (!date) return { description: fallback };
|
||||
return {
|
||||
description: formatTimeAgo(date),
|
||||
dateTime: timeElement ? new Date(date) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return { description: fallback };
|
||||
}
|
||||
};
|
||||
const [state, setState] = useState(getValue);
|
||||
|
||||
useEffect(() => {
|
||||
setState(getValue);
|
||||
}, [date, fallback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!date || !refresh) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setState(getValue);
|
||||
}, secondsToMilliseconds(12));
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [refresh]);
|
||||
|
||||
if (!state.dateTime) {
|
||||
return state.description;
|
||||
}
|
||||
|
||||
return (
|
||||
<time dateTime={state.dateTime.toISOString()}>{state.description}</time>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeAgo;
|
@ -1,4 +1,3 @@
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { LastSeenTooltip } from 'component/common/Table/cells/FeatureSeenCell/LastSeenTooltip';
|
||||
import type React from 'react';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
@ -85,45 +84,36 @@ export const FeatureEnvironmentSeen = ({
|
||||
|
||||
const lastSeen = getLatestLastSeenAt(environments) || featureLastSeen;
|
||||
|
||||
return (
|
||||
<>
|
||||
{lastSeen ? (
|
||||
<TimeAgo
|
||||
key={`${lastSeen}`}
|
||||
date={lastSeen}
|
||||
title=''
|
||||
live={false}
|
||||
formatter={(value: number, unit: string) => {
|
||||
const [color, textColor] = getColor(unit);
|
||||
return (
|
||||
<TooltipContainer
|
||||
sx={sx}
|
||||
tooltip={
|
||||
<LastSeenTooltip
|
||||
featureLastSeen={lastSeen}
|
||||
environments={environments}
|
||||
{...rest}
|
||||
/>
|
||||
}
|
||||
color={color}
|
||||
>
|
||||
<UsageRate stroke={textColor} />
|
||||
</TooltipContainer>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TooltipContainer
|
||||
sx={sx}
|
||||
tooltip='No usage reported from connected applications'
|
||||
>
|
||||
<Box data-loading>
|
||||
<Box>
|
||||
<UsageLine />
|
||||
</Box>
|
||||
if (!lastSeen) {
|
||||
return (
|
||||
<TooltipContainer
|
||||
sx={sx}
|
||||
tooltip='No usage reported from connected applications'
|
||||
>
|
||||
<Box data-loading>
|
||||
<Box>
|
||||
<UsageLine />
|
||||
</Box>
|
||||
</TooltipContainer>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
</TooltipContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const { background, text } = getColor(lastSeen);
|
||||
|
||||
return (
|
||||
<TooltipContainer
|
||||
sx={sx}
|
||||
tooltip={
|
||||
<LastSeenTooltip
|
||||
featureLastSeen={lastSeen}
|
||||
environments={environments}
|
||||
{...rest}
|
||||
/>
|
||||
}
|
||||
color={background}
|
||||
>
|
||||
<UsageRate stroke={text} />
|
||||
</TooltipContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,25 +1,47 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
export const useLastSeenColors = () => {
|
||||
type Color = {
|
||||
background: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const useLastSeenColors = (): ((
|
||||
date?: Date | number | string | null,
|
||||
) => Color) => {
|
||||
const theme = useTheme();
|
||||
const colorsForUnknown = {
|
||||
background: theme.palette.seen.unknown,
|
||||
text: theme.palette.grey.A400,
|
||||
};
|
||||
|
||||
return (unit?: string): [string, string] => {
|
||||
switch (unit) {
|
||||
case 'second':
|
||||
case 'minute':
|
||||
case 'hour':
|
||||
case 'day':
|
||||
return [theme.palette.seen.recent, theme.palette.success.main];
|
||||
case 'week':
|
||||
return [
|
||||
theme.palette.seen.inactive,
|
||||
theme.palette.warning.main,
|
||||
];
|
||||
case 'month':
|
||||
case 'year':
|
||||
return [theme.palette.seen.abandoned, theme.palette.error.main];
|
||||
default:
|
||||
return [theme.palette.seen.unknown, theme.palette.grey.A400];
|
||||
return (date?: Date | number | string | null): Color => {
|
||||
if (!date) {
|
||||
return colorsForUnknown;
|
||||
}
|
||||
|
||||
try {
|
||||
const days = differenceInDays(Date.now(), new Date(date));
|
||||
|
||||
if (days < 1) {
|
||||
return {
|
||||
background: theme.palette.seen.recent,
|
||||
text: theme.palette.success.main,
|
||||
};
|
||||
}
|
||||
if (days <= 7) {
|
||||
return {
|
||||
background: theme.palette.seen.inactive,
|
||||
text: theme.palette.warning.main,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
background: theme.palette.seen.abandoned,
|
||||
text: theme.palette.error.main,
|
||||
};
|
||||
} catch {}
|
||||
|
||||
return colorsForUnknown;
|
||||
};
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ import { ReactComponent as ArchivedStageIcon } from 'assets/icons/stage-archived
|
||||
import CloudCircle from '@mui/icons-material/CloudCircle';
|
||||
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
|
||||
import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
|
||||
import type { LifecycleStage } from './LifecycleStage';
|
||||
@ -114,22 +114,12 @@ const LastSeenIcon: FC<{
|
||||
lastSeen: string;
|
||||
}> = ({ lastSeen }) => {
|
||||
const getColor = useLastSeenColors();
|
||||
const { text, background } = getColor(lastSeen);
|
||||
|
||||
return (
|
||||
<TimeAgo
|
||||
key={`${lastSeen}`}
|
||||
date={lastSeen}
|
||||
title=''
|
||||
live={false}
|
||||
formatter={(value: number, unit: string) => {
|
||||
const [color, textColor] = getColor(unit);
|
||||
return (
|
||||
<StyledIconWrapper style={{ background: color }}>
|
||||
<UsageRate stroke={textColor} />
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<StyledIconWrapper style={{ background }}>
|
||||
<UsageRate stroke={text} />
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -230,11 +220,7 @@ const Environments: FC<{
|
||||
<Box>{environment.name}</Box>
|
||||
</CenteredBox>
|
||||
<CenteredBox>
|
||||
<TimeAgo
|
||||
key={`${environment.lastSeenAt}`}
|
||||
minPeriod={60}
|
||||
date={environment.lastSeenAt}
|
||||
/>
|
||||
<TimeAgo date={environment.lastSeenAt} />
|
||||
<LastSeenIcon lastSeen={environment.lastSeenAt} />
|
||||
</CenteredBox>
|
||||
</EnvironmentLine>
|
||||
|
@ -17,7 +17,6 @@ import { formatDateYMDHM } from 'utils/formatDate';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { Box, Link, Tooltip } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import {
|
||||
@ -29,6 +28,7 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
|
||||
export type ProjectArchiveCardProps = {
|
||||
id: string;
|
||||
@ -91,12 +91,8 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||
<p data-loading>
|
||||
Archived:{' '}
|
||||
<TimeAgo
|
||||
key={`${archivedAt}`}
|
||||
minPeriod={60}
|
||||
date={
|
||||
new Date(archivedAt as string)
|
||||
}
|
||||
live={false}
|
||||
date={archivedAt}
|
||||
refresh={false}
|
||||
/>
|
||||
</p>
|
||||
</Box>
|
||||
|
@ -93,6 +93,7 @@ export type UiFlags = {
|
||||
newEventSearch?: boolean;
|
||||
archiveProjects?: boolean;
|
||||
projectListImprovements?: boolean;
|
||||
timeAgoRefactor?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -149,6 +149,7 @@ exports[`should create default config 1`] = `
|
||||
"showInactiveUsers": false,
|
||||
"signals": false,
|
||||
"strictSchemaValidation": false,
|
||||
"timeAgoRefactor": false,
|
||||
"useMemoizedActiveTokens": false,
|
||||
"useProjectReadModel": false,
|
||||
"userAccessUIEnabled": false,
|
||||
|
@ -66,7 +66,8 @@ export type IFlagKey =
|
||||
| 'projectListImprovements'
|
||||
| 'useProjectReadModel'
|
||||
| 'webhookServiceNameLogging'
|
||||
| 'addonUsageMetrics';
|
||||
| 'addonUsageMetrics'
|
||||
| 'timeAgoRefactor';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -323,6 +324,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS,
|
||||
false,
|
||||
),
|
||||
timeAgoRefactor: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_TIMEAGO_REFACTOR,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -59,6 +59,7 @@ process.nextTick(async () => {
|
||||
useProjectReadModel: true,
|
||||
webhookServiceNameLogging: true,
|
||||
addonUsageMetrics: true,
|
||||
timeAgoRefactor: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user