mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02: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 { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
||||||
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||||
|
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@ -161,6 +162,9 @@ export const ProjectFeatureToggles = ({
|
|||||||
} = useChangeRequestToggle(projectId);
|
} = useChangeRequestToggle(projectId);
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
const showEnvironmentLastSeen = Boolean(
|
||||||
|
uiConfig.flags.lastSeenByEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
const onFavorite = useCallback(
|
const onFavorite = useCallback(
|
||||||
async (feature: IFeatureToggleListItem) => {
|
async (feature: IFeatureToggleListItem) => {
|
||||||
@ -215,8 +219,13 @@ export const ProjectFeatureToggles = ({
|
|||||||
{
|
{
|
||||||
Header: 'Seen',
|
Header: 'Seen',
|
||||||
accessor: 'lastSeenAt',
|
accessor: 'lastSeenAt',
|
||||||
Cell: FeatureSeenCell,
|
Cell: ({ value, row: { original: feature } }: any) => {
|
||||||
sortType: 'date',
|
return showEnvironmentLastSeen ? (
|
||||||
|
<FeatureEnvironmentSeenCell feature={feature} />
|
||||||
|
) : (
|
||||||
|
<FeatureSeenCell value={value} />
|
||||||
|
);
|
||||||
|
},
|
||||||
align: 'center',
|
align: 'center',
|
||||||
maxWidth: 80,
|
maxWidth: 80,
|
||||||
},
|
},
|
||||||
@ -351,6 +360,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
name: env,
|
name: env,
|
||||||
enabled: thisEnv?.enabled || false,
|
enabled: thisEnv?.enabled || false,
|
||||||
variantCount: thisEnv?.variantCount || 0,
|
variantCount: thisEnv?.variantCount || 0,
|
||||||
|
lastSeenAt: thisEnv?.lastSeenAt,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
@ -6,6 +6,7 @@ const generateEnv = (enabled: boolean, name: string): IProjectEnvironment => {
|
|||||||
name,
|
name,
|
||||||
type: 'development',
|
type: 'development',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
lastSeenAt: new Date().toISOString(),
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
protected: false,
|
protected: false,
|
||||||
enabled,
|
enabled,
|
||||||
|
@ -4,6 +4,7 @@ export const defaultEnvironment: IEnvironment = {
|
|||||||
name: '',
|
name: '',
|
||||||
type: '',
|
type: '',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
|
lastSeenAt: '',
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
protected: false,
|
protected: false,
|
||||||
|
@ -11,6 +11,7 @@ export interface IEnvironment {
|
|||||||
projectCount?: number;
|
projectCount?: number;
|
||||||
apiTokenCount?: number;
|
apiTokenCount?: number;
|
||||||
enabledToggleCount?: number;
|
enabledToggleCount?: number;
|
||||||
|
lastSeenAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectEnvironment extends IEnvironment {
|
export interface IProjectEnvironment extends IEnvironment {
|
||||||
|
@ -16,6 +16,7 @@ export interface IEnvironments {
|
|||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
variantCount: number;
|
variantCount: number;
|
||||||
|
lastSeenAt: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureToggle {
|
export interface IFeatureToggle {
|
||||||
@ -42,6 +43,7 @@ export interface IFeatureEnvironment {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
strategies: IFeatureStrategy[];
|
strategies: IFeatureStrategy[];
|
||||||
variants?: IFeatureVariant[];
|
variants?: IFeatureVariant[];
|
||||||
|
lastSeenAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureEnvironmentWithCrEnabled extends IFeatureEnvironment {
|
export interface IFeatureEnvironmentWithCrEnabled extends IFeatureEnvironment {
|
||||||
|
@ -56,6 +56,7 @@ export interface IFlags {
|
|||||||
configurableFeatureTypeLifetimes?: boolean;
|
configurableFeatureTypeLifetimes?: boolean;
|
||||||
frontendNavigationUpdate?: boolean;
|
frontendNavigationUpdate?: boolean;
|
||||||
segmentChangeRequests?: boolean;
|
segmentChangeRequests?: boolean;
|
||||||
|
lastSeenByEnvironment?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
Loading…
Reference in New Issue
Block a user