mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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 CloudCircle from '@mui/icons-material/CloudCircle';
 | 
				
			||||||
import Flag from '@mui/icons-material/Flag';
 | 
					import Flag from '@mui/icons-material/Flag';
 | 
				
			||||||
import WarningAmberRounded from '@mui/icons-material/WarningAmberRounded';
 | 
					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 { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
				
			||||||
import { getApplicationIssues } from './ApplicationIssues/ApplicationIssues';
 | 
					import { getApplicationIssues } from './ApplicationIssues/ApplicationIssues';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -305,17 +305,9 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => {
 | 
				
			|||||||
                                        <tr>
 | 
					                                        <tr>
 | 
				
			||||||
                                            <StyledCell>Last seen:</StyledCell>
 | 
					                                            <StyledCell>Last seen:</StyledCell>
 | 
				
			||||||
                                            <StyledCell>
 | 
					                                            <StyledCell>
 | 
				
			||||||
                                                {environment.lastSeen && (
 | 
					 | 
				
			||||||
                                                <TimeAgo
 | 
					                                                <TimeAgo
 | 
				
			||||||
                                                        key={`${environment.lastSeen}`}
 | 
					                                                    date={environment.lastSeen}
 | 
				
			||||||
                                                        minPeriod={60}
 | 
					 | 
				
			||||||
                                                        date={
 | 
					 | 
				
			||||||
                                                            new Date(
 | 
					 | 
				
			||||||
                                                                environment.lastSeen,
 | 
					 | 
				
			||||||
                                                            )
 | 
					 | 
				
			||||||
                                                        }
 | 
					 | 
				
			||||||
                                                />
 | 
					                                                />
 | 
				
			||||||
                                                )}
 | 
					 | 
				
			||||||
                                            </StyledCell>
 | 
					                                            </StyledCell>
 | 
				
			||||||
                                        </tr>
 | 
					                                        </tr>
 | 
				
			||||||
                                    </tbody>
 | 
					                                    </tbody>
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,11 @@ const setupApi = (application: ApplicationOverviewSchema) => {
 | 
				
			|||||||
        '/api/admin/metrics/applications/my-app/overview',
 | 
					        '/api/admin/metrics/applications/my-app/overview',
 | 
				
			||||||
        application,
 | 
					        application,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    testServerRoute(server, '/api/admin/ui-config', {});
 | 
					    testServerRoute(server, '/api/admin/ui-config', {
 | 
				
			||||||
 | 
					        flags: {
 | 
				
			||||||
 | 
					            timeAgoRefactor: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Display application overview with environments', async () => {
 | 
					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('development environment');
 | 
				
			||||||
    await screen.findByText('999');
 | 
					    await screen.findByText('999');
 | 
				
			||||||
    await screen.findByText('unleash-client-node:5.5.0-beta.0');
 | 
					    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 () => {
 | 
					test('Display application overview without environments', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import type { VFC } from 'react';
 | 
					import type { FC } from 'react';
 | 
				
			||||||
import TimeAgo from 'react-timeago';
 | 
					import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
				
			||||||
import { Tooltip, Typography, useTheme } from '@mui/material';
 | 
					import { Tooltip, Typography, useTheme } from '@mui/material';
 | 
				
			||||||
import { formatDateYMD } from 'utils/formatDate';
 | 
					import { formatDateYMD } from 'utils/formatDate';
 | 
				
			||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
 | 
					import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
 | 
				
			||||||
@ -9,7 +9,7 @@ interface IFeatureArchivedCellProps {
 | 
				
			|||||||
    value?: string | Date | null;
 | 
					    value?: string | Date | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const FeatureArchivedCell: VFC<IFeatureArchivedCellProps> = ({
 | 
					export const FeatureArchivedCell: FC<IFeatureArchivedCellProps> = ({
 | 
				
			||||||
    value: archivedAt,
 | 
					    value: archivedAt,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const { locationSettings } = useLocationSettings();
 | 
					    const { locationSettings } = useLocationSettings();
 | 
				
			||||||
@ -37,12 +37,7 @@ export const FeatureArchivedCell: VFC<IFeatureArchivedCellProps> = ({
 | 
				
			|||||||
                arrow
 | 
					                arrow
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                <Typography noWrap variant='body2' data-loading>
 | 
					                <Typography noWrap variant='body2' data-loading>
 | 
				
			||||||
                    <TimeAgo
 | 
					                    <TimeAgo date={new Date(archivedAt)} refresh={false} />
 | 
				
			||||||
                        key={`${archivedAt}`}
 | 
					 | 
				
			||||||
                        date={new Date(archivedAt)}
 | 
					 | 
				
			||||||
                        title=''
 | 
					 | 
				
			||||||
                        live={false}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                </Typography>
 | 
					                </Typography>
 | 
				
			||||||
            </Tooltip>
 | 
					            </Tooltip>
 | 
				
			||||||
        </TextCell>
 | 
					        </TextCell>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import type { FC } from 'react';
 | 
				
			|||||||
import { Markdown } from 'component/common/Markdown/Markdown';
 | 
					import { Markdown } from 'component/common/Markdown/Markdown';
 | 
				
			||||||
import Paper from '@mui/material/Paper';
 | 
					import Paper from '@mui/material/Paper';
 | 
				
			||||||
import { Box, styled, Typography } from '@mui/material';
 | 
					import { Box, styled, Typography } from '@mui/material';
 | 
				
			||||||
import TimeAgo from 'react-timeago';
 | 
					import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
				
			||||||
import { StyledAvatar } from './StyledAvatar';
 | 
					import { StyledAvatar } from './StyledAvatar';
 | 
				
			||||||
import type { IChangeRequestComment } from '../../changeRequest.types';
 | 
					import type { IChangeRequestComment } from '../../changeRequest.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,12 +35,7 @@ export const ChangeRequestComment: FC<{ comment: IChangeRequestComment }> = ({
 | 
				
			|||||||
                <Box>
 | 
					                <Box>
 | 
				
			||||||
                    <strong>{comment.createdBy.username}</strong>{' '}
 | 
					                    <strong>{comment.createdBy.username}</strong>{' '}
 | 
				
			||||||
                    <Typography color='text.secondary' component='span'>
 | 
					                    <Typography color='text.secondary' component='span'>
 | 
				
			||||||
                        commented{' '}
 | 
					                        commented <TimeAgo date={comment.createdAt} />
 | 
				
			||||||
                        <TimeAgo
 | 
					 | 
				
			||||||
                            key={`${comment.createdAt}`}
 | 
					 | 
				
			||||||
                            minPeriod={60}
 | 
					 | 
				
			||||||
                            date={new Date(comment.createdAt)}
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    </Typography>
 | 
					                    </Typography>
 | 
				
			||||||
                </Box>
 | 
					                </Box>
 | 
				
			||||||
            </CommentHeader>
 | 
					            </CommentHeader>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Box } from '@mui/material';
 | 
					import { Box } from '@mui/material';
 | 
				
			||||||
import { type FC, useState } from 'react';
 | 
					import { type FC, useState } from 'react';
 | 
				
			||||||
import { Typography, Tooltip } from '@mui/material';
 | 
					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 type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
import { ChangeRequestStatusBadge } from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge';
 | 
					import { ChangeRequestStatusBadge } from 'component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -38,13 +38,7 @@ export const ChangeRequestHeader: FC<{ changeRequest: ChangeRequestType }> = ({
 | 
				
			|||||||
                        margin: theme.spacing('auto', 0, 'auto', 2),
 | 
					                        margin: theme.spacing('auto', 0, 'auto', 2),
 | 
				
			||||||
                    })}
 | 
					                    })}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    Created{' '}
 | 
					                    Created <TimeAgo date={changeRequest.createdAt} /> by
 | 
				
			||||||
                    <TimeAgo
 | 
					 | 
				
			||||||
                        key={`${changeRequest.createdAt}`}
 | 
					 | 
				
			||||||
                        minPeriod={60}
 | 
					 | 
				
			||||||
                        date={new Date(changeRequest.createdAt)}
 | 
					 | 
				
			||||||
                    />{' '}
 | 
					 | 
				
			||||||
                    by
 | 
					 | 
				
			||||||
                </Typography>
 | 
					                </Typography>
 | 
				
			||||||
                <Box
 | 
					                <Box
 | 
				
			||||||
                    sx={(theme) => ({
 | 
					                    sx={(theme) => ({
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ import type {
 | 
				
			|||||||
    NotificationsSchemaItemNotificationType,
 | 
					    NotificationsSchemaItemNotificationType,
 | 
				
			||||||
} from 'openapi';
 | 
					} from 'openapi';
 | 
				
			||||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
 | 
					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 ToggleOffOutlined from '@mui/icons-material/ToggleOffOutlined';
 | 
				
			||||||
import { flexRow } from 'themes/themeStyles';
 | 
					import { flexRow } from 'themes/themeStyles';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -157,11 +157,7 @@ export const Notification = ({
 | 
				
			|||||||
                    </StyledUserContainer>
 | 
					                    </StyledUserContainer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    <StyledTimeAgoTypography>
 | 
					                    <StyledTimeAgoTypography>
 | 
				
			||||||
                        <TimeAgo
 | 
					                        <TimeAgo date={notification.createdAt} />
 | 
				
			||||||
                            key={`${notification.createdAt}`}
 | 
					 | 
				
			||||||
                            date={new Date(notification.createdAt)}
 | 
					 | 
				
			||||||
                            minPeriod={60}
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    </StyledTimeAgoTypography>
 | 
					                    </StyledTimeAgoTypography>
 | 
				
			||||||
                </StyledSecondaryInfoBox>
 | 
					                </StyledSecondaryInfoBox>
 | 
				
			||||||
            </StyledNotificationMessageBox>
 | 
					            </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 { 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 type { ILastSeenEnvironments } from 'interfaces/featureToggle';
 | 
				
			||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
					import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
				
			||||||
import { useLastSeenColors } from 'component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors';
 | 
					import { useLastSeenColors } from 'component/feature/FeatureView/FeatureEnvironmentSeen/useLastSeenColors';
 | 
				
			||||||
@ -74,10 +74,10 @@ export const LastSeenTooltip = ({
 | 
				
			|||||||
    ...rest
 | 
					    ...rest
 | 
				
			||||||
}: ILastSeenTooltipProps) => {
 | 
					}: ILastSeenTooltipProps) => {
 | 
				
			||||||
    const getColor = useLastSeenColors();
 | 
					    const getColor = useLastSeenColors();
 | 
				
			||||||
    const [, defaultTextColor] = getColor();
 | 
					 | 
				
			||||||
    const environmentsHaveLastSeen = environments?.some((environment) =>
 | 
					    const environmentsHaveLastSeen = environments?.some((environment) =>
 | 
				
			||||||
        Boolean(environment.lastSeenAt),
 | 
					        Boolean(environment.lastSeenAt),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <StyledDescription {...rest} data-loading>
 | 
					        <StyledDescription {...rest} data-loading>
 | 
				
			||||||
            <StyledDescriptionHeader>
 | 
					            <StyledDescriptionHeader>
 | 
				
			||||||
@ -85,7 +85,9 @@ export const LastSeenTooltip = ({
 | 
				
			|||||||
            </StyledDescriptionHeader>
 | 
					            </StyledDescriptionHeader>
 | 
				
			||||||
            <ConditionallyRender
 | 
					            <ConditionallyRender
 | 
				
			||||||
                condition={
 | 
					                condition={
 | 
				
			||||||
                    Boolean(environments) && Boolean(environmentsHaveLastSeen)
 | 
					                    Boolean(environments) &&
 | 
				
			||||||
 | 
					                    Boolean(environmentsHaveLastSeen) &&
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                show={
 | 
					                show={
 | 
				
			||||||
                    <StyledListContainer>
 | 
					                    <StyledListContainer>
 | 
				
			||||||
@ -95,43 +97,15 @@ export const LastSeenTooltip = ({
 | 
				
			|||||||
                                    {name}
 | 
					                                    {name}
 | 
				
			||||||
                                </StyledDescriptionBlockHeader>
 | 
					                                </StyledDescriptionBlockHeader>
 | 
				
			||||||
                                <StyledValueContainer>
 | 
					                                <StyledValueContainer>
 | 
				
			||||||
                                    <ConditionallyRender
 | 
					                                    <StyledValue
 | 
				
			||||||
                                        condition={Boolean(lastSeenAt)}
 | 
					                                        color={getColor(lastSeenAt).text}
 | 
				
			||||||
                                        show={
 | 
					                                    >
 | 
				
			||||||
                                        <TimeAgo
 | 
					                                        <TimeAgo
 | 
				
			||||||
                                                key={`${lastSeenAt}`}
 | 
					                                            date={lastSeenAt}
 | 
				
			||||||
                                                date={lastSeenAt!}
 | 
					                                            refresh={false}
 | 
				
			||||||
                                                title=''
 | 
					                                            fallback='no usage'
 | 
				
			||||||
                                                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>
 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                    />
 | 
					 | 
				
			||||||
                                </StyledValueContainer>
 | 
					                                </StyledValueContainer>
 | 
				
			||||||
                                <LastSeenProgress yes={yes} no={no} />
 | 
					                                <LastSeenProgress yes={yes} no={no} />
 | 
				
			||||||
                            </StyledDescriptionBlock>
 | 
					                            </StyledDescriptionBlock>
 | 
				
			||||||
@ -139,27 +113,12 @@ export const LastSeenTooltip = ({
 | 
				
			|||||||
                    </StyledListContainer>
 | 
					                    </StyledListContainer>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                elseShow={
 | 
					                elseShow={
 | 
				
			||||||
                    <TimeAgo
 | 
					 | 
				
			||||||
                        date={featureLastSeen}
 | 
					 | 
				
			||||||
                        title=''
 | 
					 | 
				
			||||||
                        live={false}
 | 
					 | 
				
			||||||
                        formatter={(
 | 
					 | 
				
			||||||
                            value: number,
 | 
					 | 
				
			||||||
                            unit: string,
 | 
					 | 
				
			||||||
                            suffix: string,
 | 
					 | 
				
			||||||
                        ) => {
 | 
					 | 
				
			||||||
                            return (
 | 
					 | 
				
			||||||
                    <Typography
 | 
					                    <Typography
 | 
				
			||||||
                        fontWeight={'bold'}
 | 
					                        fontWeight={'bold'}
 | 
				
			||||||
                                    color={'text.primary'}
 | 
					                        color={getColor(featureLastSeen).text}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                                    {`Reported ${value} ${unit}${
 | 
					                        Reported <TimeAgo date={featureLastSeen} />
 | 
				
			||||||
                                        value !== 1 ? 's' : ''
 | 
					 | 
				
			||||||
                                    } ${suffix}`}
 | 
					 | 
				
			||||||
                    </Typography>
 | 
					                    </Typography>
 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                        }}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <StyledDescriptionSubHeader>
 | 
					            <StyledDescriptionSubHeader>
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,11 @@ const StyledWrapper = styled(Box, {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledSpan = styled('span')(() => ({
 | 
				
			||||||
 | 
					    display: 'inline-block',
 | 
				
			||||||
 | 
					    maxWidth: '100%',
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TextCell: FC<ITextCellProps> = ({
 | 
					export const TextCell: FC<ITextCellProps> = ({
 | 
				
			||||||
    value,
 | 
					    value,
 | 
				
			||||||
    children,
 | 
					    children,
 | 
				
			||||||
@ -32,8 +37,8 @@ export const TextCell: FC<ITextCellProps> = ({
 | 
				
			|||||||
    'data-testid': testid,
 | 
					    'data-testid': testid,
 | 
				
			||||||
}) => (
 | 
					}) => (
 | 
				
			||||||
    <StyledWrapper lineClamp={lineClamp} sx={sx}>
 | 
					    <StyledWrapper lineClamp={lineClamp} sx={sx}>
 | 
				
			||||||
        <span data-loading='true' data-testid={testid}>
 | 
					        <StyledSpan data-loading='true' data-testid={testid}>
 | 
				
			||||||
            {children ?? value}
 | 
					            {children ?? value}
 | 
				
			||||||
        </span>
 | 
					        </StyledSpan>
 | 
				
			||||||
    </StyledWrapper>
 | 
					    </StyledWrapper>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
 | 
				
			|||||||
import type { FC } from 'react';
 | 
					import type { FC } from 'react';
 | 
				
			||||||
import { formatDateYMD } from 'utils/formatDate';
 | 
					import { formatDateYMD } from 'utils/formatDate';
 | 
				
			||||||
import { TextCell } from '../TextCell/TextCell';
 | 
					import { TextCell } from '../TextCell/TextCell';
 | 
				
			||||||
import TimeAgo from 'react-timeago';
 | 
					import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ITimeAgoCellProps {
 | 
					interface ITimeAgoCellProps {
 | 
				
			||||||
    value?: string | number | Date;
 | 
					    value?: string | number | Date;
 | 
				
			||||||
@ -31,16 +31,15 @@ export const TimeAgoCell: FC<ITimeAgoCellProps> = ({
 | 
				
			|||||||
            <Tooltip title={title?.(date) ?? date} arrow>
 | 
					            <Tooltip title={title?.(date) ?? date} arrow>
 | 
				
			||||||
                <Typography
 | 
					                <Typography
 | 
				
			||||||
                    noWrap
 | 
					                    noWrap
 | 
				
			||||||
 | 
					                    sx={{
 | 
				
			||||||
 | 
					                        display: 'inline-block',
 | 
				
			||||||
 | 
					                        maxWidth: '100%',
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
                    component='span'
 | 
					                    component='span'
 | 
				
			||||||
                    variant='body2'
 | 
					                    variant='body2'
 | 
				
			||||||
                    data-loading
 | 
					                    data-loading
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    <TimeAgo
 | 
					                    <TimeAgo date={value} refresh={live} />
 | 
				
			||||||
                        key={`${value}`}
 | 
					 | 
				
			||||||
                        date={new Date(value)}
 | 
					 | 
				
			||||||
                        live={live}
 | 
					 | 
				
			||||||
                        title={''}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                </Typography>
 | 
					                </Typography>
 | 
				
			||||||
            </Tooltip>
 | 
					            </Tooltip>
 | 
				
			||||||
        </TextCell>
 | 
					        </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 { LastSeenTooltip } from 'component/common/Table/cells/FeatureSeenCell/LastSeenTooltip';
 | 
				
			||||||
import type React from 'react';
 | 
					import type React from 'react';
 | 
				
			||||||
import type { FC, ReactElement } from 'react';
 | 
					import type { FC, ReactElement } from 'react';
 | 
				
			||||||
@ -85,34 +84,8 @@ export const FeatureEnvironmentSeen = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const lastSeen = getLatestLastSeenAt(environments) || featureLastSeen;
 | 
					    const lastSeen = getLatestLastSeenAt(environments) || featureLastSeen;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!lastSeen) {
 | 
				
			||||||
        return (
 | 
					        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
 | 
					            <TooltipContainer
 | 
				
			||||||
                sx={sx}
 | 
					                sx={sx}
 | 
				
			||||||
                tooltip='No usage reported from connected applications'
 | 
					                tooltip='No usage reported from connected applications'
 | 
				
			||||||
@ -123,7 +96,24 @@ export const FeatureEnvironmentSeen = ({
 | 
				
			|||||||
                    </Box>
 | 
					                    </Box>
 | 
				
			||||||
                </Box>
 | 
					                </Box>
 | 
				
			||||||
            </TooltipContainer>
 | 
					            </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 { 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 theme = useTheme();
 | 
				
			||||||
 | 
					    const colorsForUnknown = {
 | 
				
			||||||
 | 
					        background: theme.palette.seen.unknown,
 | 
				
			||||||
 | 
					        text: theme.palette.grey.A400,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (unit?: string): [string, string] => {
 | 
					    return (date?: Date | number | string | null): Color => {
 | 
				
			||||||
        switch (unit) {
 | 
					        if (!date) {
 | 
				
			||||||
            case 'second':
 | 
					            return colorsForUnknown;
 | 
				
			||||||
            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];
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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 CloudCircle from '@mui/icons-material/CloudCircle';
 | 
				
			||||||
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
 | 
					import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
 | 
				
			||||||
import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
 | 
					import { FeatureLifecycleStageIcon } from './FeatureLifecycleStageIcon';
 | 
				
			||||||
import TimeAgo from 'react-timeago';
 | 
					import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
				
			||||||
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
 | 
					import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
 | 
				
			||||||
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
 | 
					import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
 | 
				
			||||||
import type { LifecycleStage } from './LifecycleStage';
 | 
					import type { LifecycleStage } from './LifecycleStage';
 | 
				
			||||||
@ -114,23 +114,13 @@ const LastSeenIcon: FC<{
 | 
				
			|||||||
    lastSeen: string;
 | 
					    lastSeen: string;
 | 
				
			||||||
}> = ({ lastSeen }) => {
 | 
					}> = ({ lastSeen }) => {
 | 
				
			||||||
    const getColor = useLastSeenColors();
 | 
					    const getColor = useLastSeenColors();
 | 
				
			||||||
 | 
					    const { text, background } = getColor(lastSeen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <TimeAgo
 | 
					        <StyledIconWrapper style={{ background }}>
 | 
				
			||||||
            key={`${lastSeen}`}
 | 
					            <UsageRate stroke={text} />
 | 
				
			||||||
            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>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const InitialStageDescription: FC = () => {
 | 
					const InitialStageDescription: FC = () => {
 | 
				
			||||||
@ -230,11 +220,7 @@ const Environments: FC<{
 | 
				
			|||||||
                            <Box>{environment.name}</Box>
 | 
					                            <Box>{environment.name}</Box>
 | 
				
			||||||
                        </CenteredBox>
 | 
					                        </CenteredBox>
 | 
				
			||||||
                        <CenteredBox>
 | 
					                        <CenteredBox>
 | 
				
			||||||
                            <TimeAgo
 | 
					                            <TimeAgo date={environment.lastSeenAt} />
 | 
				
			||||||
                                key={`${environment.lastSeenAt}`}
 | 
					 | 
				
			||||||
                                minPeriod={60}
 | 
					 | 
				
			||||||
                                date={environment.lastSeenAt}
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                            <LastSeenIcon lastSeen={environment.lastSeenAt} />
 | 
					                            <LastSeenIcon lastSeen={environment.lastSeenAt} />
 | 
				
			||||||
                        </CenteredBox>
 | 
					                        </CenteredBox>
 | 
				
			||||||
                    </EnvironmentLine>
 | 
					                    </EnvironmentLine>
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,6 @@ import { formatDateYMDHM } from 'utils/formatDate';
 | 
				
			|||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
 | 
					import { useLocationSettings } from 'hooks/useLocationSettings';
 | 
				
			||||||
import { parseISO } from 'date-fns';
 | 
					import { parseISO } from 'date-fns';
 | 
				
			||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
					import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
				
			||||||
import TimeAgo from 'react-timeago';
 | 
					 | 
				
			||||||
import { Box, Link, Tooltip } from '@mui/material';
 | 
					import { Box, Link, Tooltip } from '@mui/material';
 | 
				
			||||||
import { Link as RouterLink } from 'react-router-dom';
 | 
					import { Link as RouterLink } from 'react-router-dom';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -29,6 +28,7 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi
 | 
				
			|||||||
import Delete from '@mui/icons-material/Delete';
 | 
					import Delete from '@mui/icons-material/Delete';
 | 
				
			||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
 | 
					import { Highlighter } from 'component/common/Highlighter/Highlighter';
 | 
				
			||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
					import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
				
			||||||
 | 
					import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ProjectArchiveCardProps = {
 | 
					export type ProjectArchiveCardProps = {
 | 
				
			||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
@ -91,12 +91,8 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
 | 
				
			|||||||
                                    <p data-loading>
 | 
					                                    <p data-loading>
 | 
				
			||||||
                                        Archived:{' '}
 | 
					                                        Archived:{' '}
 | 
				
			||||||
                                        <TimeAgo
 | 
					                                        <TimeAgo
 | 
				
			||||||
                                            key={`${archivedAt}`}
 | 
					                                            date={archivedAt}
 | 
				
			||||||
                                            minPeriod={60}
 | 
					                                            refresh={false}
 | 
				
			||||||
                                            date={
 | 
					 | 
				
			||||||
                                                new Date(archivedAt as string)
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                            live={false}
 | 
					 | 
				
			||||||
                                        />
 | 
					                                        />
 | 
				
			||||||
                                    </p>
 | 
					                                    </p>
 | 
				
			||||||
                                </Box>
 | 
					                                </Box>
 | 
				
			||||||
 | 
				
			|||||||
@ -93,6 +93,7 @@ export type UiFlags = {
 | 
				
			|||||||
    newEventSearch?: boolean;
 | 
					    newEventSearch?: boolean;
 | 
				
			||||||
    archiveProjects?: boolean;
 | 
					    archiveProjects?: boolean;
 | 
				
			||||||
    projectListImprovements?: boolean;
 | 
					    projectListImprovements?: boolean;
 | 
				
			||||||
 | 
					    timeAgoRefactor?: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IVersionInfo {
 | 
					export interface IVersionInfo {
 | 
				
			||||||
 | 
				
			|||||||
@ -149,6 +149,7 @@ exports[`should create default config 1`] = `
 | 
				
			|||||||
      "showInactiveUsers": false,
 | 
					      "showInactiveUsers": false,
 | 
				
			||||||
      "signals": false,
 | 
					      "signals": false,
 | 
				
			||||||
      "strictSchemaValidation": false,
 | 
					      "strictSchemaValidation": false,
 | 
				
			||||||
 | 
					      "timeAgoRefactor": false,
 | 
				
			||||||
      "useMemoizedActiveTokens": false,
 | 
					      "useMemoizedActiveTokens": false,
 | 
				
			||||||
      "useProjectReadModel": false,
 | 
					      "useProjectReadModel": false,
 | 
				
			||||||
      "userAccessUIEnabled": false,
 | 
					      "userAccessUIEnabled": false,
 | 
				
			||||||
 | 
				
			|||||||
@ -66,7 +66,8 @@ export type IFlagKey =
 | 
				
			|||||||
    | 'projectListImprovements'
 | 
					    | 'projectListImprovements'
 | 
				
			||||||
    | 'useProjectReadModel'
 | 
					    | 'useProjectReadModel'
 | 
				
			||||||
    | 'webhookServiceNameLogging'
 | 
					    | 'webhookServiceNameLogging'
 | 
				
			||||||
    | 'addonUsageMetrics';
 | 
					    | 'addonUsageMetrics'
 | 
				
			||||||
 | 
					    | 'timeAgoRefactor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
 | 
					export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -323,6 +324,10 @@ const flags: IFlags = {
 | 
				
			|||||||
        process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS,
 | 
					        process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS,
 | 
				
			||||||
        false,
 | 
					        false,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    timeAgoRefactor: parseEnvVarBoolean(
 | 
				
			||||||
 | 
					        process.env.UNLEASH_TIMEAGO_REFACTOR,
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
 | 
					export const defaultExperimentalOptions: IExperimentalOptions = {
 | 
				
			||||||
 | 
				
			|||||||
@ -59,6 +59,7 @@ process.nextTick(async () => {
 | 
				
			|||||||
                        useProjectReadModel: true,
 | 
					                        useProjectReadModel: true,
 | 
				
			||||||
                        webhookServiceNameLogging: true,
 | 
					                        webhookServiceNameLogging: true,
 | 
				
			||||||
                        addonUsageMetrics: true,
 | 
					                        addonUsageMetrics: true,
 | 
				
			||||||
 | 
					                        timeAgoRefactor: true,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                authentication: {
 | 
					                authentication: {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user