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} );