From a064672635d0ce749535e246e5ee2dfc95b989fc Mon Sep 17 00:00:00 2001
From: Thomas Heartman
Date: Tue, 11 Mar 2025 11:36:14 +0100
Subject: [PATCH] chore(1-3422): playground strategies list (#9504)
Initial rough work on adapting the playground strategies to the new
designs. This PR primarily splits components into Legacy files and adds
new replacements. There are *some* updates (including spacing and text
color), but nothing juicy yet. However, I wanted to get this in now,
before this PR grows even bigger.
---
.../FeatureDetails/FeatureDetails.test.tsx | 34 +++-
.../FeatureDetails/FeatureDetails.tsx | 107 ++++------
.../FeatureDetails/LegacyFeatureDetails.tsx | 184 ++++++++++++++++++
.../FeatureResultInfoPopoverCell.tsx | 42 ++--
...cyPlaygroundResultFeatureStrategyList.tsx} | 0
...aygroundResultFeatureStrategyList.test.tsx | 18 +-
.../PlaygroundResultsFeatureStrategyList.tsx | 67 +++++++
.../PlaygroundResultChip.tsx | 11 +-
8 files changed, 373 insertions(+), 90 deletions(-)
create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/LegacyFeatureDetails.tsx
rename frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/{PlaygroundResultFeatureStrategyList.tsx => LegacyPlaygroundResultFeatureStrategyList.tsx} (100%)
create mode 100644 frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultsFeatureStrategyList.tsx
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.test.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.test.tsx
index 00d35947cf..ed4ee5bcfe 100644
--- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.test.tsx
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.test.tsx
@@ -1,7 +1,8 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
-import { FeatureDetails } from './FeatureDetails';
+import { FeatureDetails as LegacyFeatureDetails } from './LegacyFeatureDetails';
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
+import { FeatureDetails } from './FeatureDetails';
const testCases = [
{
@@ -10,7 +11,7 @@ const testCases = [
hasUnsatisfiedDependency: true,
isEnabledInCurrentEnvironment: false,
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is False in development because',
+ expectedText1: /This feature flag is False in development because/,
expectedText2:
'parent dependency is not satisfied and the environment is disabled',
},
@@ -20,7 +21,7 @@ const testCases = [
hasUnsatisfiedDependency: true,
isEnabledInCurrentEnvironment: true,
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is False in development because',
+ expectedText1: /This feature flag is False in development because/,
expectedText2: 'parent dependency is not satisfied',
},
{
@@ -29,7 +30,7 @@ const testCases = [
hasUnsatisfiedDependency: false,
isEnabledInCurrentEnvironment: false,
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is False in development because',
+ expectedText1: /This feature flag is False in development because/,
expectedText2: 'the environment is disabled',
},
{
@@ -37,7 +38,7 @@ const testCases = [
feature: {
isEnabled: true,
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is True in development because',
+ expectedText1: /This feature flag is True in development because/,
expectedText2: 'at least one strategy is True',
},
{
@@ -48,7 +49,7 @@ const testCases = [
data: [{ name: 'custom' }],
},
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is Unknown in development because',
+ expectedText1: /This feature flag is Unknown in development because/,
expectedText2: 'no strategies could be fully evaluated',
},
{
@@ -59,7 +60,7 @@ const testCases = [
data: [{ name: 'custom' }, { name: 'default' }],
},
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is Unknown in development because',
+ expectedText1: /This feature flag is Unknown in development because/,
expectedText2: 'not all strategies could be fully evaluated',
},
{
@@ -70,12 +71,29 @@ const testCases = [
data: [{ name: 'default' }],
},
} as PlaygroundFeatureSchema,
- expectedText1: 'This feature flag is False in development because',
+ expectedText1: /This feature flag is False in development because/,
expectedText2:
'all strategies are either False or could not be fully evaluated',
},
];
+testCases.forEach(({ name, feature, expectedText1, expectedText2 }) => {
+ test(`${name} (legacy)`, async () => {
+ render(
+ {}}
+ />,
+ );
+
+ await screen.findByText(expectedText1);
+ await screen.findByText(expectedText2);
+ });
+});
+
testCases.forEach(({ name, feature, expectedText1, expectedText2 }) => {
test(name, async () => {
render(
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx
index 4ebf9e60cc..e56eefb55f 100644
--- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/FeatureDetails.tsx
@@ -1,45 +1,32 @@
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
-import { Alert, IconButton, Typography, useTheme, styled } from '@mui/material';
+import { Alert, Typography, useTheme, styled, IconButton } from '@mui/material';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import CloseOutlined from '@mui/icons-material/CloseOutlined';
import type React from 'react';
-import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
checkForEmptyValues,
hasCustomStrategies,
hasOnlyCustomStrategies,
} from './helpers';
-const StyledDivWrapper = styled('div')({
+const HeaderRow = styled('div')({
display: 'flex',
justifyContent: 'space-between',
width: '100%',
});
-const StyledDivTitleRow = styled('div')(({ theme }) => ({
+const HeaderGroup = styled('hgroup')(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
gap: theme.spacing(1.5),
- marginTop: theme.spacing(1.5),
}));
-const StyledDivAlertRow = styled('div')(({ theme }) => ({
- margin: theme.spacing(1, 0),
+const StyledTypographyName = styled('h3')(({ theme }) => ({
+ fontWeight: 'bold',
+ fontSize: theme.typography.subtitle1.fontSize,
+ margin: 0,
}));
-const StyledDivDescriptionRow = styled('div')(({ theme }) => ({
- margin: theme.spacing(1, 0.5),
-}));
-
-const StyledTypographyName = styled(Typography)(({ theme }) => ({
- fontWeight: 600,
- padding: theme.spacing(0.5),
-}));
-
-const StyledIconButton = styled(IconButton)({
- textAlign: 'right',
-});
-
interface PlaygroundFeatureResultDetailsProps {
feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
@@ -57,7 +44,7 @@ export const FeatureDetails = ({
return [
`This feature flag is True in ${input?.environment} because `,
'at least one strategy is True',
- theme.palette.success.main,
+ theme.palette.success.contrastText,
];
if (
@@ -67,7 +54,7 @@ export const FeatureDetails = ({
return [
`This feature flag is False in ${input?.environment} because `,
'parent dependency is not satisfied and the environment is disabled',
- theme.palette.error.main,
+ theme.palette.error.contrastText,
];
}
@@ -75,35 +62,35 @@ export const FeatureDetails = ({
return [
`This feature flag is False in ${input?.environment} because `,
'the environment is disabled',
- theme.palette.error.main,
+ theme.palette.error.contrastText,
];
if (hasOnlyCustomStrategies(feature))
return [
`This feature flag is Unknown in ${input?.environment} because `,
'no strategies could be fully evaluated',
- theme.palette.warning.main,
+ theme.palette.warning.contrastText,
];
if (hasCustomStrategies(feature))
return [
`This feature flag is Unknown in ${input?.environment} because `,
'not all strategies could be fully evaluated',
- theme.palette.warning.main,
+ theme.palette.warning.contrastText,
];
if (feature.hasUnsatisfiedDependency) {
return [
`This feature flag is False in ${input?.environment} because `,
'parent dependency is not satisfied',
- theme.palette.error.main,
+ theme.palette.error.contrastText,
];
}
return [
`This feature flag is False in ${input?.environment} because `,
'all strategies are either False or could not be fully evaluated',
- theme.palette.error.main,
+ theme.palette.error.contrastText,
];
})();
@@ -124,61 +111,43 @@ export const FeatureDetails = ({
return (
<>
-
-
-
- {feature.name}
-
- (
+
+
+ {feature.name}
+
+ {feature?.strategies?.result !== 'unknown' ? (
- )}
- elseShow={() => (
+ ) : (
)}
- />
-
-
+
+
+
-
-
-
-
- {description}
-
-
+
+
+
+ {description}
+
{reason}
-
- .
-
-
-
- {noValueTxt}
-
- }
- />
-
-
- {customStrategiesTxt}
-
-
- }
- />
+ .
+
+ {noValueTxt ? {noValueTxt} : null}
+ {customStrategiesTxt ? (
+
+ {customStrategiesTxt}
+
+ ) : null}
>
);
};
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/LegacyFeatureDetails.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/LegacyFeatureDetails.tsx
new file mode 100644
index 0000000000..4ebf9e60cc
--- /dev/null
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureDetails/LegacyFeatureDetails.tsx
@@ -0,0 +1,184 @@
+import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
+import { Alert, IconButton, Typography, useTheme, styled } from '@mui/material';
+import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
+import CloseOutlined from '@mui/icons-material/CloseOutlined';
+import type React from 'react';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import {
+ checkForEmptyValues,
+ hasCustomStrategies,
+ hasOnlyCustomStrategies,
+} from './helpers';
+
+const StyledDivWrapper = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ width: '100%',
+});
+
+const StyledDivTitleRow = styled('div')(({ theme }) => ({
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: theme.spacing(1.5),
+ marginTop: theme.spacing(1.5),
+}));
+
+const StyledDivAlertRow = styled('div')(({ theme }) => ({
+ margin: theme.spacing(1, 0),
+}));
+
+const StyledDivDescriptionRow = styled('div')(({ theme }) => ({
+ margin: theme.spacing(1, 0.5),
+}));
+
+const StyledTypographyName = styled(Typography)(({ theme }) => ({
+ fontWeight: 600,
+ padding: theme.spacing(0.5),
+}));
+
+const StyledIconButton = styled(IconButton)({
+ textAlign: 'right',
+});
+
+interface PlaygroundFeatureResultDetailsProps {
+ feature: PlaygroundFeatureSchema;
+ input?: PlaygroundRequestSchema;
+ onClose: () => void;
+}
+export const FeatureDetails = ({
+ feature,
+ input,
+ onClose,
+}: PlaygroundFeatureResultDetailsProps) => {
+ const theme = useTheme();
+
+ const [description, reason, color] = (() => {
+ if (feature.isEnabled)
+ return [
+ `This feature flag is True in ${input?.environment} because `,
+ 'at least one strategy is True',
+ theme.palette.success.main,
+ ];
+
+ if (
+ feature.hasUnsatisfiedDependency &&
+ !feature.isEnabledInCurrentEnvironment
+ ) {
+ return [
+ `This feature flag is False in ${input?.environment} because `,
+ 'parent dependency is not satisfied and the environment is disabled',
+ theme.palette.error.main,
+ ];
+ }
+
+ if (!feature.isEnabledInCurrentEnvironment)
+ return [
+ `This feature flag is False in ${input?.environment} because `,
+ 'the environment is disabled',
+ theme.palette.error.main,
+ ];
+
+ if (hasOnlyCustomStrategies(feature))
+ return [
+ `This feature flag is Unknown in ${input?.environment} because `,
+ 'no strategies could be fully evaluated',
+ theme.palette.warning.main,
+ ];
+
+ if (hasCustomStrategies(feature))
+ return [
+ `This feature flag is Unknown in ${input?.environment} because `,
+ 'not all strategies could be fully evaluated',
+ theme.palette.warning.main,
+ ];
+
+ if (feature.hasUnsatisfiedDependency) {
+ return [
+ `This feature flag is False in ${input?.environment} because `,
+ 'parent dependency is not satisfied',
+ theme.palette.error.main,
+ ];
+ }
+
+ return [
+ `This feature flag is False in ${input?.environment} because `,
+ 'all strategies are either False or could not be fully evaluated',
+ theme.palette.error.main,
+ ];
+ })();
+
+ const noValueTxt = checkForEmptyValues(input?.context)
+ ? 'You did not provide a value for your context field in step 2 of the configuration'
+ : undefined;
+
+ const customStrategiesTxt = hasCustomStrategies(feature)
+ ? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked accordingly.`
+ : undefined;
+
+ const onCloseClick =
+ onClose &&
+ ((event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ onClose();
+ });
+
+ return (
+ <>
+
+
+
+ {feature.name}
+
+ (
+
+ )}
+ elseShow={() => (
+
+ )}
+ />
+
+
+
+
+
+
+
+ {description}
+
+
+ {reason}
+
+
+ .
+
+
+
+ {noValueTxt}
+
+ }
+ />
+
+
+ {customStrategiesTxt}
+
+
+ }
+ />
+ >
+ );
+};
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx
index 7cb05e44f2..dc5b418e9f 100644
--- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell.tsx
@@ -2,8 +2,11 @@ import { useRef, useState } from 'react';
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
import { IconButton, Popover, styled } from '@mui/material';
import InfoOutlined from '@mui/icons-material/InfoOutlined';
+import { FeatureDetails as LegacyFeatureDetails } from './FeatureDetails/LegacyFeatureDetails';
+import { PlaygroundResultFeatureStrategyList as LegacyPlaygroundResultFeatureStrategyList } from './FeatureStrategyList/LegacyPlaygroundResultFeatureStrategyList';
+import { useUiFlag } from 'hooks/useUiFlag';
import { FeatureDetails } from './FeatureDetails/FeatureDetails';
-import { PlaygroundResultFeatureStrategyList } from './FeatureStrategyList/PlaygroundResultFeatureStrategyList';
+import { PlaygroundResultFeatureStrategyList } from './FeatureStrategyList/PlaygroundResultsFeatureStrategyList';
interface FeatureResultInfoPopoverCellProps {
feature: PlaygroundFeatureSchema;
@@ -21,6 +24,7 @@ export const FeatureResultInfoPopoverCell = ({
}: FeatureResultInfoPopoverCellProps) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
+ const useNewStrategyDesign = useUiFlag('flagOverviewRedesign');
const togglePopover = () => {
setOpen(!open);
@@ -43,7 +47,7 @@ export const FeatureResultInfoPopoverCell = ({
sx: (theme) => ({
display: 'flex',
flexDirection: 'column',
- padding: theme.spacing(6),
+ padding: theme.spacing(useNewStrategyDesign ? 4 : 6),
width: 728,
maxWidth: '100%',
height: 'auto',
@@ -61,15 +65,31 @@ export const FeatureResultInfoPopoverCell = ({
horizontal: 'left',
}}
>
- setOpen(false)}
- />
-
+ {useNewStrategyDesign ? (
+ <>
+ setOpen(false)}
+ />
+
+ >
+ ) : (
+ <>
+ setOpen(false)}
+ />
+
+ >
+ )}
);
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/LegacyPlaygroundResultFeatureStrategyList.tsx
similarity index 100%
rename from frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx
rename to frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/LegacyPlaygroundResultFeatureStrategyList.tsx
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx
index 24340f967c..b64be1ee17 100644
--- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.test.tsx
@@ -1,8 +1,9 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
-import { PlaygroundResultFeatureStrategyList } from './PlaygroundResultFeatureStrategyList';
+import { PlaygroundResultFeatureStrategyList as LegacyPlaygroundResultFeatureStrategyList } from './LegacyPlaygroundResultFeatureStrategyList';
import { vi } from 'vitest';
+import { PlaygroundResultFeatureStrategyList } from './PlaygroundResultsFeatureStrategyList';
const testCases = [
{
@@ -135,6 +136,21 @@ afterAll(() => {
vi.clearAllMocks();
});
+testCases.forEach(({ name, feature, expectedText }) => {
+ test(`${name} (legacy)`, async () => {
+ render(
+ ,
+ );
+
+ await screen.findByText(expectedText);
+ });
+});
+
testCases.forEach(({ name, feature, expectedText }) => {
test(name, async () => {
render(
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultsFeatureStrategyList.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultsFeatureStrategyList.tsx
new file mode 100644
index 0000000000..2a3e1b25b3
--- /dev/null
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultsFeatureStrategyList.tsx
@@ -0,0 +1,67 @@
+import {
+ PlaygroundResultStrategyLists,
+ WrappedPlaygroundResultStrategyList,
+} from './StrategyList/playgroundResultStrategyLists';
+import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
+import { Alert } from '@mui/material';
+
+interface PlaygroundResultFeatureStrategyListProps {
+ feature: PlaygroundFeatureSchema;
+ input?: PlaygroundRequestSchema;
+}
+
+export const PlaygroundResultFeatureStrategyList = ({
+ feature,
+ input,
+}: PlaygroundResultFeatureStrategyListProps) => {
+ const enabledStrategies = feature.strategies?.data?.filter(
+ (strategy) => !strategy.disabled,
+ );
+ const disabledStrategies = feature.strategies?.data?.filter(
+ (strategy) => strategy.disabled,
+ );
+
+ const showDisabledStrategies = disabledStrategies?.length > 0;
+
+ if ((feature?.strategies?.data.length ?? 0) === 0) {
+ return (
+
+ There are no strategies added to this feature flag in the
+ selected environment.
+
+ );
+ }
+
+ if (
+ (feature.hasUnsatisfiedDependency ||
+ !feature.isEnabledInCurrentEnvironment) &&
+ Boolean(feature?.strategies?.data)
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+ {showDisabledStrategies ? (
+
+ ) : null}
+ >
+ );
+};
diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx
index c5bd63cce7..f7ef9883c9 100644
--- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx
+++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/PlaygroundResultChip/PlaygroundResultChip.tsx
@@ -11,12 +11,14 @@ interface IResultChipProps {
label: string;
// Result icon - defaults to true
showIcon?: boolean;
+ tabindex?: number;
}
export const PlaygroundResultChip: VFC = ({
enabled,
label,
showIcon = true,
+ tabindex,
}) => {
const theme = useTheme();
const icon = (
@@ -28,12 +30,14 @@ export const PlaygroundResultChip: VFC = ({
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
}
elseShow={
@@ -56,6 +60,7 @@ export const PlaygroundResultChip: VFC = ({
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
@@ -63,7 +68,11 @@ export const PlaygroundResultChip: VFC = ({
}
elseShow={
-
+
{label}
}