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 CompletedDiscardedStageIcon } from 'assets/icons/stage-completed-discarded.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 'react-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.background.application, 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.fontWeight.bold, borderRadius: theme.spacing(0.5), }), }, })); const ColorFill = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.primary.light, color: theme.palette.primary.contrastText, borderRadius: theme.spacing(0, 0, 1, 1), // 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(); return ( { const [color, textColor] = getColor(unit); return ( ); }} /> ); }; const InitialStageDescription: FC = () => { return ( <> This feature toggle is currently in the initial phase of it's life cycle. 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 ( {stage.name === 'completed' && stage.status === 'discarded' ? ( ) : ( )} ); }; 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 }) => { return ( <> We've seen the feature flag in the following non-production environments: {children} ); }; const BoldTitle = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), marginBottom: theme.spacing(1), fontSize: theme.fontSizes.smallBody, fontWeight: theme.fontWeight.bold, })); const LiveStageDescription: FC<{ onComplete: () => void; loading: boolean; }> = ({ children, onComplete, loading }) => { return ( <> Is this feature complete? Marking the feature as complete does not affect any configuration, but it moves the feature into it’s next life cycle stage and is an indication that you have learned what you needed in order to progress with the feature. It serves as a reminder to start cleaning up the 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; }> = ({ onArchive, onUncomplete, loading }) => { 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, 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, environments, onArchive, onUncomplete, loading }) => { 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; onArchive: () => void; onComplete: () => void; onUncomplete: () => void; loading: boolean; }> = ({ children, stage, 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' && ( )} } > {children} );