From 4b3b98f2632cc9b857a87c2a0cc63984a2ed9f35 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:53:03 +0100 Subject: [PATCH] feat: update lifecycle tooltip style (#9107) New tooltips for lifecycle indicators. - removed "timeline" lifecycle explanation - new descriptions - changed tooltip footer colors - refactored "environments" section --- .../FeatureLifecycleTooltip.test.tsx | 4 +- .../FeatureLifecycleTooltip.tsx | 438 +++++++----------- 2 files changed, 173 insertions(+), 269 deletions(-) diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx index 5ea50e5c80..e552851713 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.test.tsx @@ -54,7 +54,7 @@ test('render initial stage', async () => { await screen.findByText('Define'); await screen.findByText('2 minutes'); await screen.findByText( - 'This feature flag is currently in the initial phase of its lifecycle.', + 'Feature flag has been created, but we have not seen any metrics yet.', ); }); @@ -104,7 +104,7 @@ test('render completed stage with still active', async () => { }); await screen.findByText('Cleanup'); - await screen.findByText('production'); + await screen.findByText(/production/); await screen.findByText('2 hours ago'); expect(screen.queryByText('Archive feature')).not.toBeInTheDocument(); }); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index 11551d728c..53078c7aef 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -14,7 +14,6 @@ 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'; @@ -27,6 +26,7 @@ const TimeLabel = styled('span')(({ theme }) => ({ const InfoText = styled('p')(({ theme }) => ({ paddingBottom: theme.spacing(1), + color: theme.palette.text.primary, })); const MainLifecycleRow = styled(Box)(({ theme }) => ({ @@ -39,69 +39,21 @@ const TimeLifecycleRow = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', marginBottom: theme.spacing(1.5), + gap: theme.spacing(1), })); -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, +const StyledFooter = styled('footer')(({ theme }) => ({ + background: theme.palette.neutral.light, + color: theme.palette.text.secondary, 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), + padding: theme.spacing(2, 3.5), +})); + +const StyledEnvironmentUsageIcon = styled(StyledIconWrapper)(({ theme }) => ({ + width: theme.spacing(2), + height: theme.spacing(2), + marginRight: theme.spacing(0.75), })); const LastSeenIcon: FC<{ @@ -111,76 +63,9 @@ const LastSeenIcon: FC<{ 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 ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ); }; @@ -189,13 +74,30 @@ const EnvironmentLine = styled(Box)(({ theme }) => ({ alignItems: 'center', justifyContent: 'space-between', marginTop: theme.spacing(1), - marginBottom: theme.spacing(2), + marginBottom: theme.spacing(1), + marginLeft: theme.spacing(3.5), +})); + +const StyledEnvironmentsTitle = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + color: theme.palette.text.primary, +})); + +const StyledEnvironmentIcon = styled(CloudCircle)(({ theme }) => ({ + color: theme.palette.primary.main, + width: theme.spacing(2.5), + display: 'block', })); const CenteredBox = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - gap: theme.spacing(1), +})); + +const StyledStageAction = styled(Box)(({ theme }) => ({ + marginTop: theme.spacing(2), })); const Environments: FC<{ @@ -210,12 +112,11 @@ const Environments: FC<{ return ( - {environment.name} - + ); @@ -224,54 +125,32 @@ const Environments: FC<{ ); }; -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, +const StyledStageActionTitle = styled(Typography)(({ theme }) => ({ + paddingTop: theme.spacing(0.5), + marginBottom: theme.spacing(0.5), + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallerBody, fontWeight: theme.typography.fontWeightBold, })); -const LiveStageDescription: FC<{ +const LiveStageAction: FC<{ onComplete: () => void; loading: boolean; children?: React.ReactNode; project: string; -}> = ({ children, onComplete, loading, project }) => { +}> = ({ onComplete, loading, project }) => { return ( - <> - Is this feature complete? + + + 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. + configuration; however, it moves the flag to its next lifecycle + stage and indicates that you have learned what you needed in + order to progress. Mark completed - - Users have been exposed to this feature in the following - production environments: - - - {children} - + ); }; @@ -298,27 +171,22 @@ const SafeToArchive: FC<{ project: string; }> = ({ onArchive, onUncomplete, loading, project }) => { return ( - <> - Safe to archive - + + 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. ({ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', - gap: 2, - }} + gap: theme.spacing(2), + marginTop: theme.spacing(1), + })} > - + ); }; const ActivelyUsed: FC<{ onUncomplete: () => void; loading: boolean; - children?: React.ReactNode; -}> = ({ children, onUncomplete, loading }) => ( - <> - +}> = ({ 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: + archiving it. - {children} - + If you think this feature was completed too early you can revert to - the live stage: + the live stage. Revert to live - + ); const CompletedStageDescription: FC<{ @@ -392,34 +246,20 @@ const CompletedStageDescription: FC<{ name: string; lastSeenAt: string; }>; - children?: React.ReactNode; project: string; -}> = ({ - children, - environments, - onArchive, - onUncomplete, - loading, - project, -}) => { - return ( - - } - elseShow={ - - {children} - - } - /> - ); +}> = ({ environments, onArchive, onUncomplete, loading, project }) => { + if (isSafeToArchive(environments)) { + return ( + + ); + } + + return ; }; const FormatTime: FC<{ @@ -438,6 +278,72 @@ const FormatElapsedTime: FC<{ return {elapsedTime}; }; +const StageInfo: FC<{ stage: LifecycleStage['name'] }> = ({ stage }) => { + if (stage === 'initial') { + return ( + + Feature flag has been created, but we have not seen any metrics + yet. + + ); + } + if (stage === 'pre-live') { + return ( + + Feature is being developed and tested in controlled + environments. + + ); + } + if (stage === 'live') { + return ( + + Feature is being rolled out in production according to an + activation strategy. + + ); + } + if (stage === 'completed') { + return ( + + When a flag is no longer needed, clean up the code to minimize + technical debt and archive the flag for future reference. + + ); + } + if (stage === 'archived') { + return ( + + Flag is archived in Unleash for future reference. + + ); + } + + return null; +}; + +const EnvironmentsInfo: FC<{ + stage: { + name: LifecycleStage['name']; + environments?: Array<{ + name: string; + lastSeenAt: string; + }>; + }; +}> = ({ stage }) => ( + <> + + {' '} + {stage.environments && stage.environments.length > 0 + ? `Seen in environment${stage.environments.length > 1 ? 's' : ''}` + : 'Not seen in any environments'} + + {stage.environments && stage.environments.length > 0 ? ( + + ) : null} + +); + export const FeatureLifecycleTooltip: FC<{ children: React.ReactElement; stage: LifecycleStage; @@ -478,9 +384,11 @@ export const FeatureLifecycleTooltip: FC<{ + + + Stage entered at - @@ -488,35 +396,31 @@ export const FeatureLifecycleTooltip: FC<{ - - {stage.name === 'initial' && } - {stage.name === 'pre-live' && ( - - - - )} - {stage.name === 'live' && ( - - - - )} - {stage.name === 'completed' && ( - - - - )} - {stage.name === 'archived' && } - + {stage.name !== 'archived' ? ( + + + {stage.name === 'live' && ( + + + + )} + {stage.name === 'completed' && ( + + )} + + ) : null} } >