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:
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