1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +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)

![Screenshot 2023-08-08 at 10 30
56](https://github.com/Unleash/unleash/assets/104830839/b70bf63e-8e58-4678-9be4-0fe40c873d29)
![Screenshot 2023-08-08 at 10 31
24](https://github.com/Unleash/unleash/assets/104830839/4ec74030-9425-4254-ad02-b8101e131774)

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2023-08-08 12:32:33 +03:00 committed by GitHub
parent b55d677d1e
commit f3b11b89bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 277 additions and 2 deletions

View 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

View 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

View File

@ -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>
}
/>
);
};

View File

@ -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>
);
};

View File

@ -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];
}
};
};

View File

@ -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,
},
];
})

View File

@ -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,

View File

@ -4,6 +4,7 @@ export const defaultEnvironment: IEnvironment = {
name: '',
type: '',
createdAt: '',
lastSeenAt: '',
sortOrder: 0,
enabled: false,
protected: false,

View File

@ -11,6 +11,7 @@ export interface IEnvironment {
projectCount?: number;
apiTokenCount?: number;
enabledToggleCount?: number;
lastSeenAt: string;
}
export interface IProjectEnvironment extends IEnvironment {

View File

@ -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 {

View File

@ -56,6 +56,7 @@ export interface IFlags {
configurableFeatureTypeLifetimes?: boolean;
frontendNavigationUpdate?: boolean;
segmentChangeRequests?: boolean;
lastSeenByEnvironment?: boolean;
}
export interface IVersionInfo {