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