mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Feat: last seen by env UI (#4439)
Implements last seen by environment UI Closes # [1-1182](https://linear.app/unleash/issue/1-1182/ui-for-last-seen-per-environment)   --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									b55d677d1e
								
							
						
					
					
						commit
						f3b11b89bb
					
				
							
								
								
									
										3
									
								
								frontend/src/assets/icons/usage-line.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/assets/icons/usage-line.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="current" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<path d="M7 10H13" stroke="#6E6E70" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 211 B  | 
							
								
								
									
										3
									
								
								frontend/src/assets/icons/usage-rate.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/assets/icons/usage-rate.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<path d="M15 10H12.8571L11.4286 14L8.57143 6L7.14286 10H5" stroke="current" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 248 B  | 
@ -0,0 +1,108 @@
 | 
			
		||||
import React, { FC, ReactElement, VFC } from 'react';
 | 
			
		||||
import { Box, styled } from '@mui/material';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
 | 
			
		||||
import { LastSeenTooltip } from './LastSeenTooltip';
 | 
			
		||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
 | 
			
		||||
import { ReactComponent as UsageLine } from 'assets/icons/usage-line.svg';
 | 
			
		||||
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
 | 
			
		||||
import TimeAgo from 'react-timeago';
 | 
			
		||||
import { useLastSeenColors } from './useLastSeenColors';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    padding: theme.spacing(1.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    width: '28px',
 | 
			
		||||
    height: '28px',
 | 
			
		||||
    background: 'transparent',
 | 
			
		||||
    borderRadius: `${theme.shape.borderRadius}px`,
 | 
			
		||||
    textAlign: 'center',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    fontSize: theme.typography.body2.fontSize,
 | 
			
		||||
    margin: '0 auto',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledIconWrapper = styled('div')(({ theme }) => ({
 | 
			
		||||
    width: '20px',
 | 
			
		||||
    height: '20px',
 | 
			
		||||
    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 {
 | 
			
		||||
    feature: IFeatureToggleListItem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TooltipContainer: FC<{
 | 
			
		||||
    color?: string;
 | 
			
		||||
    tooltip: ReactElement | string;
 | 
			
		||||
}> = ({ tooltip, color, children }) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledContainer>
 | 
			
		||||
            <TooltipResolver
 | 
			
		||||
                variant="custom"
 | 
			
		||||
                titleComponent={tooltip}
 | 
			
		||||
                arrow
 | 
			
		||||
                describeChild
 | 
			
		||||
            >
 | 
			
		||||
                <StyledBox sx={{ '&:hover': { background: color } }}>
 | 
			
		||||
                    <StyledIconWrapper style={{ background: color }}>
 | 
			
		||||
                        {children}
 | 
			
		||||
                    </StyledIconWrapper>
 | 
			
		||||
                </StyledBox>
 | 
			
		||||
            </TooltipResolver>
 | 
			
		||||
        </StyledContainer>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
 | 
			
		||||
    feature,
 | 
			
		||||
}) => {
 | 
			
		||||
    const getColor = useLastSeenColors();
 | 
			
		||||
    const environments = Object.values(feature.environments);
 | 
			
		||||
    return (
 | 
			
		||||
        <ConditionallyRender
 | 
			
		||||
            condition={Boolean(feature.lastSeenAt)}
 | 
			
		||||
            show={
 | 
			
		||||
                feature.lastSeenAt && (
 | 
			
		||||
                    <TimeAgo
 | 
			
		||||
                        date={feature.lastSeenAt}
 | 
			
		||||
                        title=""
 | 
			
		||||
                        live={false}
 | 
			
		||||
                        formatter={(value: number, unit: string) => {
 | 
			
		||||
                            const [color, textColor] = getColor(unit);
 | 
			
		||||
                            return (
 | 
			
		||||
                                <TooltipContainer
 | 
			
		||||
                                    tooltip={
 | 
			
		||||
                                        <LastSeenTooltip
 | 
			
		||||
                                            environments={environments}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    }
 | 
			
		||||
                                    color={color}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <UsageRate stroke={textColor} />
 | 
			
		||||
                                </TooltipContainer>
 | 
			
		||||
                            );
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            elseShow={
 | 
			
		||||
                <TooltipContainer tooltip="No usage reported from connected applications">
 | 
			
		||||
                    <UsageLine />
 | 
			
		||||
                </TooltipContainer>
 | 
			
		||||
            }
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,120 @@
 | 
			
		||||
import { styled, SxProps, Theme } from '@mui/material';
 | 
			
		||||
import TimeAgo from 'react-timeago';
 | 
			
		||||
import { IEnvironments } from 'interfaces/featureToggle';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { useLastSeenColors } from './useLastSeenColors';
 | 
			
		||||
 | 
			
		||||
const StyledDescription = styled(
 | 
			
		||||
    'div',
 | 
			
		||||
    {}
 | 
			
		||||
)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    maxWidth: theme.spacing(50),
 | 
			
		||||
    padding: theme.spacing(1),
 | 
			
		||||
    backgroundColor: theme.palette.background.paper,
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    borderRadius: theme.shape.borderRadiusMedium,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDescriptionBlock = styled('div')({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledDescriptionHeader = styled('p')(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.text.primary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    fontWeight: theme.fontWeight.bold,
 | 
			
		||||
    marginBottom: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDescriptionBlockHeader = styled('p')(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.text.primary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    fontWeight: theme.fontWeight.bold,
 | 
			
		||||
    marginBottom: theme.spacing(1),
 | 
			
		||||
    width: '50%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDescriptionSubHeader = styled('p')(({ theme }) => ({
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    margin: theme.spacing(2, 0),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledValueContainer = styled('div')({
 | 
			
		||||
    width: '50%',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledValue = styled('div', {
 | 
			
		||||
    shouldForwardProp: prop => prop !== 'color',
 | 
			
		||||
})(({ color }) => ({
 | 
			
		||||
    textAlign: 'left',
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    color: color,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface ILastSeenTooltipProps {
 | 
			
		||||
    environments?: IEnvironments[];
 | 
			
		||||
    className?: string;
 | 
			
		||||
    sx?: SxProps<Theme>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const LastSeenTooltip = ({
 | 
			
		||||
    environments,
 | 
			
		||||
    ...rest
 | 
			
		||||
}: ILastSeenTooltipProps) => {
 | 
			
		||||
    const getColor = useLastSeenColors();
 | 
			
		||||
    const [defaultColor] = getColor();
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledDescription {...rest}>
 | 
			
		||||
            <StyledDescriptionHeader sx={{ mb: 0 }}>
 | 
			
		||||
                Last usage reported
 | 
			
		||||
            </StyledDescriptionHeader>
 | 
			
		||||
            <StyledDescriptionSubHeader>
 | 
			
		||||
                Usage is reported from connected applications through metrics
 | 
			
		||||
            </StyledDescriptionSubHeader>
 | 
			
		||||
            {environments &&
 | 
			
		||||
                environments.map(({ name, lastSeenAt }) => (
 | 
			
		||||
                    <StyledDescriptionBlock key={name}>
 | 
			
		||||
                        <StyledDescriptionBlockHeader>
 | 
			
		||||
                            {name}
 | 
			
		||||
                        </StyledDescriptionBlockHeader>
 | 
			
		||||
                        <StyledValueContainer>
 | 
			
		||||
                            <ConditionallyRender
 | 
			
		||||
                                condition={Boolean(lastSeenAt)}
 | 
			
		||||
                                show={
 | 
			
		||||
                                    <TimeAgo
 | 
			
		||||
                                        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={defaultColor}>
 | 
			
		||||
                                        no usage
 | 
			
		||||
                                    </StyledValue>
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        </StyledValueContainer>
 | 
			
		||||
                    </StyledDescriptionBlock>
 | 
			
		||||
                ))}
 | 
			
		||||
        </StyledDescription>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { useTheme } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
export const useLastSeenColors = () => {
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
    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];
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@ -64,6 +64,7 @@ import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
 | 
			
		||||
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
 | 
			
		||||
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
 | 
			
		||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
 | 
			
		||||
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
 | 
			
		||||
 | 
			
		||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
 | 
			
		||||
    whiteSpace: 'nowrap',
 | 
			
		||||
@ -161,6 +162,9 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
    } = useChangeRequestToggle(projectId);
 | 
			
		||||
    const [showExportDialog, setShowExportDialog] = useState(false);
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const showEnvironmentLastSeen = Boolean(
 | 
			
		||||
        uiConfig.flags.lastSeenByEnvironment
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onFavorite = useCallback(
 | 
			
		||||
        async (feature: IFeatureToggleListItem) => {
 | 
			
		||||
@ -215,8 +219,13 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
            {
 | 
			
		||||
                Header: 'Seen',
 | 
			
		||||
                accessor: 'lastSeenAt',
 | 
			
		||||
                Cell: FeatureSeenCell,
 | 
			
		||||
                sortType: 'date',
 | 
			
		||||
                Cell: ({ value, row: { original: feature } }: any) => {
 | 
			
		||||
                    return showEnvironmentLastSeen ? (
 | 
			
		||||
                        <FeatureEnvironmentSeenCell feature={feature} />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <FeatureSeenCell value={value} />
 | 
			
		||||
                    );
 | 
			
		||||
                },
 | 
			
		||||
                align: 'center',
 | 
			
		||||
                maxWidth: 80,
 | 
			
		||||
            },
 | 
			
		||||
@ -351,6 +360,7 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
                                name: env,
 | 
			
		||||
                                enabled: thisEnv?.enabled || false,
 | 
			
		||||
                                variantCount: thisEnv?.variantCount || 0,
 | 
			
		||||
                                lastSeenAt: thisEnv?.lastSeenAt,
 | 
			
		||||
                            },
 | 
			
		||||
                        ];
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ const generateEnv = (enabled: boolean, name: string): IProjectEnvironment => {
 | 
			
		||||
        name,
 | 
			
		||||
        type: 'development',
 | 
			
		||||
        createdAt: new Date().toISOString(),
 | 
			
		||||
        lastSeenAt: new Date().toISOString(),
 | 
			
		||||
        sortOrder: 0,
 | 
			
		||||
        protected: false,
 | 
			
		||||
        enabled,
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ export const defaultEnvironment: IEnvironment = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    type: '',
 | 
			
		||||
    createdAt: '',
 | 
			
		||||
    lastSeenAt: '',
 | 
			
		||||
    sortOrder: 0,
 | 
			
		||||
    enabled: false,
 | 
			
		||||
    protected: false,
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ export interface IEnvironment {
 | 
			
		||||
    projectCount?: number;
 | 
			
		||||
    apiTokenCount?: number;
 | 
			
		||||
    enabledToggleCount?: number;
 | 
			
		||||
    lastSeenAt: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectEnvironment extends IEnvironment {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ export interface IEnvironments {
 | 
			
		||||
    name: string;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    variantCount: number;
 | 
			
		||||
    lastSeenAt: Date | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeatureToggle {
 | 
			
		||||
@ -42,6 +43,7 @@ export interface IFeatureEnvironment {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    strategies: IFeatureStrategy[];
 | 
			
		||||
    variants?: IFeatureVariant[];
 | 
			
		||||
    lastSeenAt?: Date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeatureEnvironmentWithCrEnabled extends IFeatureEnvironment {
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,7 @@ export interface IFlags {
 | 
			
		||||
    configurableFeatureTypeLifetimes?: boolean;
 | 
			
		||||
    frontendNavigationUpdate?: boolean;
 | 
			
		||||
    segmentChangeRequests?: boolean;
 | 
			
		||||
    lastSeenByEnvironment?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IVersionInfo {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user