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 { ReactComponent as InitialStageIcon } from 'assets/icons/stage-initial.svg';
import { ReactComponent as PreLiveStageIcon } from 'assets/icons/stage-pre-live.svg';
import { ReactComponent as LiveStageIcon } from 'assets/icons/stage-live.svg';
import { ReactComponent as CompletedStageIcon } from 'assets/icons/stage-completed.svg';
import { ReactComponent as ArchivedStageIcon } from 'assets/icons/stage-archived.svg';
import CloudCircle from '@mui/icons-material/CloudCircle';
import { ReactComponent as UsageRate } from 'assets/icons/usage-rate.svg';
import { FeatureLifecycleStageIcon } from './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 (
);
};
const InitialStageDescription: FC = () => {
return (
<>
This feature flag is currently in the initial phase of its
lifecycle.
This means that the flag has been created, but it has not yet
been seen in any environment.
Once we detect metrics for a non-production environment it will
move into pre-live.
>
);
};
const StageTimeline: FC<{
stage: LifecycleStage;
}> = ({ stage }) => {
return (
);
};
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 (
{environments.map((environment) => {
return (
{environment.name}
);
})}
);
};
const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({
children,
}) => {
return (
<>
We've seen the feature flag in the following environments:
{children}
>
);
};
const ArchivedStageDescription = () => {
return (
Your feature has been archived, it is now safe to delete it.
);
};
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 (
<>
Is this feature complete?
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.
Mark completed
Users have been exposed to this feature in the following
production environments:
{children}
>
);
};
const SafeToArchive: FC<{
onArchive: () => void;
onUncomplete: () => void;
loading: boolean;
project: string;
}> = ({ onArchive, onUncomplete, loading, project }) => {
return (
<>
Safe to archive
We haven’t seen this feature flag in any environment for at
least two days. It’s likely that it’s safe to archive this flag.
Revert to live
Archive feature
>
);
};
const ActivelyUsed: FC<{
onUncomplete: () => void;
loading: boolean;
children?: React.ReactNode;
}> = ({ children, onUncomplete, loading }) => (
<>
This feature has been successfully completed, but we are still
seeing usage. Clean up the feature flag from your code before
archiving it:
{children}
If you think this feature was completed too early you can revert to
the live stage:
Revert to live
>
);
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 (
}
elseShow={
{children}
}
/>
);
};
const FormatTime: FC<{
time: string;
}> = ({ time }) => {
const { locationSettings } = useLocationSettings();
return {formatDateYMDHMS(time, locationSettings.locale)};
};
const FormatElapsedTime: FC<{
time: string;
}> = ({ time }) => {
const pastTime = parseISO(time);
const elapsedTime = formatDistanceToNow(pastTime, { addSuffix: false });
return {elapsedTime};
};
export const FeatureLifecycleTooltip: FC<{
children: React.ReactElement;
stage: LifecycleStage;
project: string;
onArchive: () => void;
onComplete: () => void;
onUncomplete: () => void;
loading: boolean;
}> = ({
children,
stage,
project,
onArchive,
onComplete,
onUncomplete,
loading,
}) => (
({ padding: theme.spacing(2) })}>
Lifecycle
{stage.name}
Stage entered at
Time spent in stage
{stage.name === 'initial' && }
{stage.name === 'pre-live' && (
)}
{stage.name === 'live' && (
)}
{stage.name === 'completed' && (
)}
{stage.name === 'archived' && }
}
>
{children}
);