1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-23 00:16:25 +01:00
unleash.unleash/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/LegacyFeatureLifecycleTooltip.tsx
Tymoteusz Czech 50ab2c9d61
feat: rename lifecycle stages (#9102)
Name names for "lifecycle" stages, and aligning frontend types.
2025-01-16 08:41:11 +00:00

527 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Box, styled, Typography } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import type * as React from 'react';
import type { FC } from 'react';
import CloudCircle from '@mui/icons-material/CloudCircle';
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
import { FeatureLifecycleStageIcon } from 'component/common/FeatureLifecycle/FeatureLifecycleStageIcon';
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
import { StyledIconWrapper } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import { useLastSeenColors } from '../../FeatureEnvironmentSeen/useLastSeenColors';
import type { LifecycleStage } from './LifecycleStage';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import {
DELETE_FEATURE,
UPDATE_FEATURE,
} from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { isSafeToArchive } from './isSafeToArchive';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMDHMS } from 'utils/formatDate';
import { formatDistanceToNow, parseISO } from 'date-fns';
const TimeLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const InfoText = styled('p')(({ theme }) => ({
paddingBottom: theme.spacing(1),
}));
const MainLifecycleRow = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
marginBottom: theme.spacing(2),
}));
const TimeLifecycleRow = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
marginBottom: theme.spacing(1.5),
}));
const IconsRow = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(4),
marginBottom: theme.spacing(6),
}));
const Line = styled(Box)(({ theme }) => ({
height: '1px',
background: theme.palette.divider,
flex: 1,
}));
const StageBox = styled(Box, {
shouldForwardProp: (prop) => prop !== 'active',
})<{
active?: boolean;
}>(({ theme, active }) => ({
position: 'relative',
// speech bubble triangle for active stage
...(active && {
'&:before': {
content: '""',
position: 'absolute',
display: 'block',
borderStyle: 'solid',
borderColor: `${theme.palette.primary.light} transparent`,
borderWidth: '0 6px 6px',
top: theme.spacing(3.25),
left: theme.spacing(1.75),
},
}),
// stage name text
'&:after': {
content: 'attr(data-after-content)',
display: 'block',
position: 'absolute',
top: theme.spacing(4),
left: theme.spacing(-1.25),
right: theme.spacing(-1.25),
textAlign: 'center',
whiteSpace: 'nowrap',
fontSize: theme.spacing(1.25),
padding: theme.spacing(0.25, 0),
color: theme.palette.text.secondary,
// active wrapper for stage name text
...(active && {
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.contrastText,
fontWeight: theme.typography.fontWeightBold,
borderRadius: theme.spacing(0.5),
}),
},
}));
const ColorFill = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.contrastText,
borderRadius: `0 0 ${theme.shape.borderRadiusMedium}px ${theme.shape.borderRadiusMedium}px`, // has to match the parent tooltip container
margin: theme.spacing(-1, -1.5), // has to match the parent tooltip container
padding: theme.spacing(2, 3),
}));
const LastSeenIcon: FC<{
lastSeen: string;
}> = ({ lastSeen }) => {
const getColor = useLastSeenColors();
const { text, background } = getColor(lastSeen);
return (
<StyledIconWrapper style={{ background }}>
<UsageRate stroke={text} />
</StyledIconWrapper>
);
};
const InitialStageDescription: FC = () => {
return (
<>
<InfoText>
This feature flag is currently in the initial phase of its
lifecycle.
</InfoText>
<InfoText>
This means that the flag has been created, but it has not yet
been seen in any environment.
</InfoText>
<InfoText>
Once we detect metrics for a non-production environment it will
move into pre-live.
</InfoText>
</>
);
};
const StageTimeline: FC<{
stage: LifecycleStage;
}> = ({ stage }) => {
return (
<IconsRow>
<StageBox
data-after-content='Initial'
active={stage.name === 'initial'}
>
<FeatureLifecycleStageIcon stage={{ name: 'initial' }} />
</StageBox>
<Line />
<StageBox
data-after-content='Pre-live'
active={stage.name === 'pre-live'}
>
<FeatureLifecycleStageIcon stage={{ name: 'pre-live' }} />
</StageBox>
<Line />
<StageBox data-after-content='Live' active={stage.name === 'live'}>
<FeatureLifecycleStageIcon stage={{ name: 'live' }} />
</StageBox>
<Line />
<StageBox
data-after-content='Completed'
active={stage.name === 'completed'}
>
<FeatureLifecycleStageIcon stage={{ name: 'completed' }} />
</StageBox>
<Line />
<StageBox
data-after-content='Archived'
active={stage.name === 'archived'}
>
<FeatureLifecycleStageIcon stage={{ name: 'archived' }} />
</StageBox>
</IconsRow>
);
};
const EnvironmentLine = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(2),
}));
const CenteredBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}));
const Environments: FC<{
environments: Array<{
name: string;
lastSeenAt: string;
}>;
}> = ({ environments }) => {
return (
<Box>
{environments.map((environment) => {
return (
<EnvironmentLine key={environment.name}>
<CenteredBox>
<CloudCircle />
<Box>{environment.name}</Box>
</CenteredBox>
<CenteredBox>
<TimeAgo date={environment.lastSeenAt} />
<LastSeenIcon lastSeen={environment.lastSeenAt} />
</CenteredBox>
</EnvironmentLine>
);
})}
</Box>
);
};
const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({
children,
}) => {
return (
<>
<InfoText>
We've seen the feature flag in the following environments:
</InfoText>
{children}
</>
);
};
const ArchivedStageDescription = () => {
return (
<InfoText>
Your feature has been archived, it is now safe to delete it.
</InfoText>
);
};
const BoldTitle = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
fontSize: theme.typography.body2.fontSize,
fontWeight: theme.typography.fontWeightBold,
}));
const LiveStageDescription: FC<{
onComplete: () => void;
loading: boolean;
children?: React.ReactNode;
project: string;
}> = ({ children, onComplete, loading, project }) => {
return (
<>
<BoldTitle>Is this feature complete?</BoldTitle>
<InfoText sx={{ mb: 1 }}>
Marking the feature flag as complete does not affect any
configuration; however, it moves the feature flag to its next
lifecycle stage and indicates that you have learned what you
needed in order to progress with the feature. It serves as a
reminder to start cleaning up the feature flag and removing it
from the code.
</InfoText>
<PermissionButton
color='inherit'
variant='outlined'
permission={UPDATE_FEATURE}
size='small'
onClick={onComplete}
disabled={loading}
projectId={project}
>
Mark completed
</PermissionButton>
<InfoText sx={{ mt: 3 }}>
Users have been exposed to this feature in the following
production environments:
</InfoText>
{children}
</>
);
};
const SafeToArchive: FC<{
onArchive: () => void;
onUncomplete: () => void;
loading: boolean;
project: string;
}> = ({ onArchive, onUncomplete, loading, project }) => {
return (
<>
<BoldTitle>Safe to archive</BoldTitle>
<InfoText
sx={{
mt: 2,
mb: 1,
}}
>
We havent seen this feature flag in any environment for at
least two days. Its likely that its safe to archive this flag.
</InfoText>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 2,
}}
>
<PermissionButton
color='inherit'
variant='outlined'
permission={UPDATE_FEATURE}
size='small'
onClick={onUncomplete}
disabled={loading}
projectId={project}
>
Revert to live
</PermissionButton>
<PermissionButton
color='inherit'
variant='outlined'
permission={DELETE_FEATURE}
size='small'
sx={{ mb: 2 }}
onClick={onArchive}
projectId={project}
>
Archive feature
</PermissionButton>
</Box>
</>
);
};
const ActivelyUsed: FC<{
onUncomplete: () => void;
loading: boolean;
children?: React.ReactNode;
}> = ({ children, onUncomplete, loading }) => (
<>
<InfoText
sx={{
mt: 1,
mb: 1,
}}
>
This feature has been successfully completed, but we are still
seeing usage. Clean up the feature flag from your code before
archiving it:
</InfoText>
{children}
<InfoText
sx={{
mt: 1,
mb: 1,
}}
>
If you think this feature was completed too early you can revert to
the live stage:
</InfoText>
<PermissionButton
color='inherit'
variant='outlined'
permission={UPDATE_FEATURE}
size='small'
sx={{ mb: 2 }}
onClick={onUncomplete}
disabled={loading}
>
Revert to live
</PermissionButton>
</>
);
const CompletedStageDescription: FC<{
onArchive: () => void;
onUncomplete: () => void;
loading: boolean;
environments: Array<{
name: string;
lastSeenAt: string;
}>;
children?: React.ReactNode;
project: string;
}> = ({
children,
environments,
onArchive,
onUncomplete,
loading,
project,
}) => {
return (
<ConditionallyRender
condition={isSafeToArchive(environments)}
show={
<SafeToArchive
onArchive={onArchive}
onUncomplete={onUncomplete}
loading={loading}
project={project}
/>
}
elseShow={
<ActivelyUsed onUncomplete={onUncomplete} loading={loading}>
{children}
</ActivelyUsed>
}
/>
);
};
const FormatTime: FC<{
time: string;
}> = ({ time }) => {
const { locationSettings } = useLocationSettings();
return <span>{formatDateYMDHMS(time, locationSettings.locale)}</span>;
};
const FormatElapsedTime: FC<{
time: string;
}> = ({ time }) => {
const pastTime = parseISO(time);
const elapsedTime = formatDistanceToNow(pastTime, { addSuffix: false });
return <span>{elapsedTime}</span>;
};
export const FeatureLifecycleTooltip: FC<{
children: React.ReactElement<any, any>;
stage: LifecycleStage;
project: string;
onArchive: () => void;
onComplete: () => void;
onUncomplete: () => void;
loading: boolean;
}> = ({
children,
stage,
project,
onArchive,
onComplete,
onUncomplete,
loading,
}) => (
<HtmlTooltip
maxHeight={800}
maxWidth={350}
arrow
tabIndex={0}
title={
<Box>
<Box sx={(theme) => ({ padding: theme.spacing(2) })}>
<MainLifecycleRow>
<Typography variant='h3'>Lifecycle</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<Badge sx={{ textTransform: 'capitalize' }}>
{stage.name}
</Badge>
<FeatureLifecycleStageIcon stage={stage} />
</Box>
</MainLifecycleRow>
<TimeLifecycleRow>
<TimeLabel>Stage entered at</TimeLabel>
<FormatTime time={stage.enteredStageAt} />
</TimeLifecycleRow>
<TimeLifecycleRow>
<TimeLabel>Time spent in stage</TimeLabel>
<FormatElapsedTime time={stage.enteredStageAt} />
</TimeLifecycleRow>
<StageTimeline stage={stage} />
</Box>
<ColorFill>
{stage.name === 'initial' && <InitialStageDescription />}
{stage.name === 'pre-live' && (
<PreLiveStageDescription>
<Environments environments={stage.environments} />
</PreLiveStageDescription>
)}
{stage.name === 'live' && (
<LiveStageDescription
onComplete={onComplete}
loading={loading}
project={project}
>
<Environments environments={stage.environments} />
</LiveStageDescription>
)}
{stage.name === 'completed' && (
<CompletedStageDescription
environments={stage.environments}
onArchive={onArchive}
onUncomplete={onUncomplete}
loading={loading}
project={project}
>
<Environments environments={stage.environments} />
</CompletedStageDescription>
)}
{stage.name === 'archived' && <ArchivedStageDescription />}
</ColorFill>
</Box>
}
>
<CenteredBox>{children}</CenteredBox>
</HtmlTooltip>
);