1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00

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.
This commit is contained in:
Thomas Heartman 2025-03-11 11:36:14 +01:00 committed by GitHub
parent 9547fd962f
commit a064672635
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 373 additions and 90 deletions

View File

@ -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(
<LegacyFeatureDetails
feature={feature}
input={
{ environment: 'development' } as PlaygroundRequestSchema
}
onClose={() => {}}
/>,
);
await screen.findByText(expectedText1);
await screen.findByText(expectedText2);
});
});
testCases.forEach(({ name, feature, expectedText1, expectedText2 }) => {
test(name, async () => {
render(

View File

@ -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 (
<>
<StyledDivWrapper>
<StyledDivTitleRow>
<StyledTypographyName variant={'subtitle1'}>
{feature.name}
</StyledTypographyName>
<ConditionallyRender
condition={feature?.strategies?.result !== 'unknown'}
show={() => (
<HeaderRow>
<HeaderGroup>
<StyledTypographyName>{feature.name}</StyledTypographyName>
<p>
{feature?.strategies?.result !== 'unknown' ? (
<PlaygroundResultChip
tabindex={-1}
enabled={feature.isEnabled}
label={feature.isEnabled ? 'True' : 'False'}
/>
)}
elseShow={() => (
) : (
<PlaygroundResultChip
tabindex={-1}
enabled='unknown'
label={'Unknown'}
showIcon={false}
/>
)}
/>
</StyledDivTitleRow>
<StyledIconButton onClick={onCloseClick}>
</p>
</HeaderGroup>
<IconButton aria-label='Close' onClick={onCloseClick}>
<CloseOutlined />
</StyledIconButton>
</StyledDivWrapper>
<StyledDivDescriptionRow>
<Typography variant='body1' component='span'>
{description}
</Typography>
<Typography variant='subtitle1' color={color} component='span'>
</IconButton>
</HeaderRow>
<p>
{description}
<Typography color={color} component='span'>
{reason}
</Typography>
<Typography variant='body1' component='span'>
.
</Typography>
</StyledDivDescriptionRow>
<ConditionallyRender
condition={Boolean(noValueTxt)}
show={
<StyledDivAlertRow>
<Alert color={'info'}>{noValueTxt}</Alert>
</StyledDivAlertRow>
}
/>
<ConditionallyRender
condition={Boolean(customStrategiesTxt)}
show={
<StyledDivAlertRow>
<Alert severity='warning' color='info'>
{customStrategiesTxt}
</Alert>
</StyledDivAlertRow>
}
/>
.
</p>
{noValueTxt ? <Alert color={'info'}>{noValueTxt}</Alert> : null}
{customStrategiesTxt ? (
<Alert severity='warning' color='info'>
{customStrategiesTxt}
</Alert>
) : null}
</>
);
};

View File

@ -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 (
<>
<StyledDivWrapper>
<StyledDivTitleRow>
<StyledTypographyName variant={'subtitle1'}>
{feature.name}
</StyledTypographyName>
<ConditionallyRender
condition={feature?.strategies?.result !== 'unknown'}
show={() => (
<PlaygroundResultChip
enabled={feature.isEnabled}
label={feature.isEnabled ? 'True' : 'False'}
/>
)}
elseShow={() => (
<PlaygroundResultChip
enabled='unknown'
label={'Unknown'}
showIcon={false}
/>
)}
/>
</StyledDivTitleRow>
<StyledIconButton onClick={onCloseClick}>
<CloseOutlined />
</StyledIconButton>
</StyledDivWrapper>
<StyledDivDescriptionRow>
<Typography variant='body1' component='span'>
{description}
</Typography>
<Typography variant='subtitle1' color={color} component='span'>
{reason}
</Typography>
<Typography variant='body1' component='span'>
.
</Typography>
</StyledDivDescriptionRow>
<ConditionallyRender
condition={Boolean(noValueTxt)}
show={
<StyledDivAlertRow>
<Alert color={'info'}>{noValueTxt}</Alert>
</StyledDivAlertRow>
}
/>
<ConditionallyRender
condition={Boolean(customStrategiesTxt)}
show={
<StyledDivAlertRow>
<Alert severity='warning' color='info'>
{customStrategiesTxt}
</Alert>
</StyledDivAlertRow>
}
/>
</>
);
};

View File

@ -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',
}}
>
<FeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
<PlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
{useNewStrategyDesign ? (
<>
<FeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
<PlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
</>
) : (
<>
<LegacyFeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
<LegacyPlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
</>
)}
</Popover>
</FeatureResultPopoverWrapper>
);

View File

@ -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(
<LegacyPlaygroundResultFeatureStrategyList
feature={feature}
input={
{ environment: 'development' } as PlaygroundRequestSchema
}
/>,
);
await screen.findByText(expectedText);
});
});
testCases.forEach(({ name, feature, expectedText }) => {
test(name, async () => {
render(

View File

@ -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 (
<Alert severity='warning' sx={{ mt: 2 }}>
There are no strategies added to this feature flag in the
selected environment.
</Alert>
);
}
if (
(feature.hasUnsatisfiedDependency ||
!feature.isEnabledInCurrentEnvironment) &&
Boolean(feature?.strategies?.data)
) {
return (
<WrappedPlaygroundResultStrategyList
feature={feature}
input={input}
/>
);
}
return (
<>
<PlaygroundResultStrategyLists
strategies={enabledStrategies || []}
input={input}
titlePrefix={showDisabledStrategies ? 'Enabled' : ''}
/>
{showDisabledStrategies ? (
<PlaygroundResultStrategyLists
strategies={disabledStrategies}
input={input}
titlePrefix={'Disabled'}
infoText={
'Disabled strategies are not evaluated for the overall result.'
}
/>
) : null}
</>
);
};

View File

@ -11,12 +11,14 @@ interface IResultChipProps {
label: string;
// Result icon - defaults to true
showIcon?: boolean;
tabindex?: number;
}
export const PlaygroundResultChip: VFC<IResultChipProps> = ({
enabled,
label,
showIcon = true,
tabindex,
}) => {
const theme = useTheme();
const icon = (
@ -28,12 +30,14 @@ export const PlaygroundResultChip: VFC<IResultChipProps> = ({
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
<FeatureEnabledIcon
aria-hidden
color={theme.palette.success.main}
strokeWidth='0.25'
/>
}
elseShow={
<FeatureDisabledIcon
aria-hidden
color={theme.palette.error.main}
strokeWidth='0.25'
/>
@ -56,6 +60,7 @@ export const PlaygroundResultChip: VFC<IResultChipProps> = ({
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
<Badge
tabIndex={tabindex}
color='success'
icon={showIcon ? icon : undefined}
>
@ -63,7 +68,11 @@ export const PlaygroundResultChip: VFC<IResultChipProps> = ({
</Badge>
}
elseShow={
<Badge color='error' icon={showIcon ? icon : undefined}>
<Badge
color='error'
icon={showIcon ? icon : undefined}
tabIndex={tabindex}
>
{label}
</Badge>
}