1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-18 01:18:23 +02:00

chore: Playground Strategy Lists (#9510)

Continue the implementation of Playground strategy lists. This PR also
adjusts some existing strategy container and list items to accomodate
more use cases (such as this).

The playground strategy execution component is still the old design.

After (playground results):

![image](https://github.com/user-attachments/assets/f32505ba-f040-4491-a298-6e8bf606536d)

After (env strategy list):

![image](https://github.com/user-attachments/assets/b39174c7-3ee2-4fb4-aa7c-b51134c740b8)

Before (env strategy list):

![image](https://github.com/user-attachments/assets/a0a045e5-3623-44ef-96fa-8ba2f5be6b98)
This commit is contained in:
Thomas Heartman 2025-03-13 12:01:44 +01:00 committed by GitHub
parent 4ddb8fe7d8
commit 732b7f342a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 493 additions and 200 deletions

View File

@ -4,7 +4,6 @@ import DragIndicator from '@mui/icons-material/DragIndicator';
import { Box, IconButton, Typography, styled } from '@mui/material'; import { Box, IconButton, Typography, styled } from '@mui/material';
import type { IFeatureStrategy } from 'interfaces/strategy'; import type { IFeatureStrategy } from 'interfaces/strategy';
import { formatStrategyName } from 'utils/strategyNames'; import { formatStrategyName } from 'utils/strategyNames';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { PlaygroundStrategySchema } from 'openapi'; import type { PlaygroundStrategySchema } from 'openapi';
import { Badge } from '../Badge/Badge'; import { Badge } from '../Badge/Badge';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -16,16 +15,20 @@ type StrategyItemContainerProps = {
onDragStart?: DragEventHandler<HTMLButtonElement>; onDragStart?: DragEventHandler<HTMLButtonElement>;
onDragEnd?: DragEventHandler<HTMLButtonElement>; onDragEnd?: DragEventHandler<HTMLButtonElement>;
headerItemsRight?: ReactNode; headerItemsRight?: ReactNode;
headerItemsLeft?: ReactNode;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
children?: React.ReactNode; children?: React.ReactNode;
}; };
const DragIcon = styled(IconButton)({ const inlinePadding = 3;
const DragIcon = styled(IconButton)(({ theme }) => ({
marginLeft: theme.spacing(-inlinePadding),
padding: 0, padding: 0,
cursor: 'inherit', cursor: 'inherit',
transition: 'color 0.2s ease-in-out', transition: 'color 0.2s ease-in-out',
}); }));
const StyledHeaderContainer = styled('hgroup')(({ theme }) => ({ const StyledHeaderContainer = styled('hgroup')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -38,9 +41,14 @@ const StyledHeaderContainer = styled('hgroup')(({ theme }) => ({
}, },
})); }));
const StyledContainer = styled('article')({ const StyledContainer = styled('article')(({ theme }) => ({
background: 'inherit', background: 'inherit',
}); padding: theme.spacing(inlinePadding),
paddingTop: theme.spacing(0.5),
display: 'flex',
flexDirection: 'column',
rowGap: theme.spacing(0.5),
}));
const StyledTruncator = styled(Truncator)(({ theme }) => ({ const StyledTruncator = styled(Truncator)(({ theme }) => ({
fontSize: theme.typography.body1.fontSize, fontSize: theme.typography.body1.fontSize,
@ -49,44 +57,40 @@ const StyledTruncator = styled(Truncator)(({ theme }) => ({
})); }));
const StyledHeader = styled('div', { const StyledHeader = styled('div', {
shouldForwardProp: (prop) => prop !== 'draggable' && prop !== 'disabled', shouldForwardProp: (prop) => prop !== 'disabled',
})<{ draggable: boolean; disabled: boolean }>( })<{ disabled: boolean }>(({ theme, disabled }) => ({
({ theme, draggable, disabled }) => ({
padding: theme.spacing(0.5, 2),
display: 'flex', display: 'flex',
gap: theme.spacing(1),
alignItems: 'center', alignItems: 'center',
paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
color: disabled }));
? theme.palette.text.secondary
: theme.palette.text.primary, const StyledHeaderInner = styled('div')(({ theme }) => ({
}), display: 'flex',
); alignItems: 'center',
gap: theme.spacing(1),
}));
export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({ export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({
strategy, strategy,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
headerItemsRight, headerItemsRight,
headerItemsLeft,
strategyHeaderLevel = 3, strategyHeaderLevel = 3,
children, children,
style = {}, style = {},
className,
}) => { }) => {
const StrategyHeaderLink: React.FC<{ children?: React.ReactNode }> = const StrategyHeaderLink: React.FC<{ children?: React.ReactNode }> =
'links' in strategy // todo: revisit this when we get to playground, related to flag `flagOverviewRedesign` 'links' in strategy
? ({ children }) => <Link to={strategy.links.edit}>{children}</Link> ? ({ children }) => <Link to={strategy.links.edit}>{children}</Link>
: ({ children }) => <> {children} </>; : ({ children }) => <> {children} </>;
return ( return (
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
<StyledContainer style={style}> <StyledContainer style={style} className={className}>
<StyledHeader <StyledHeader disabled={Boolean(strategy?.disabled)}>
draggable={Boolean(onDragStart)} {onDragStart ? (
disabled={Boolean(strategy?.disabled)}
>
<ConditionallyRender
condition={Boolean(onDragStart)}
show={() => (
<DragIcon <DragIcon
draggable draggable
disableRipple disableRipple
@ -101,8 +105,8 @@ export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({
sx={{ color: 'action.active' }} sx={{ color: 'action.active' }}
/> />
</DragIcon> </DragIcon>
)} ) : null}
/> <StyledHeaderInner>
<StrategyHeaderLink> <StrategyHeaderLink>
<StyledHeaderContainer> <StyledHeaderContainer>
{strategy.title ? ( {strategy.title ? (
@ -124,7 +128,9 @@ export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({
className='strategy-name' className='strategy-name'
component={`h${strategyHeaderLevel}`} component={`h${strategyHeaderLevel}`}
> >
{formatStrategyName(String(strategy.name))} {formatStrategyName(
String(strategy.name),
)}
</Typography> </Typography>
)} )}
</StyledHeaderContainer> </StyledHeaderContainer>
@ -133,18 +139,19 @@ export const StrategyItemContainer: FC<StrategyItemContainerProps> = ({
{strategy.disabled ? ( {strategy.disabled ? (
<Badge color='disabled'>Disabled</Badge> <Badge color='disabled'>Disabled</Badge>
) : null} ) : null}
{headerItemsLeft}
</StyledHeaderInner>
<Box <Box
sx={{ sx={{
marginLeft: 'auto', marginLeft: 'auto',
display: 'flex', display: 'flex',
minHeight: (theme) => theme.spacing(6),
alignItems: 'center', alignItems: 'center',
}} }}
> >
{headerItemsRight} {headerItemsRight}
</Box> </Box>
</StyledHeader> </StyledHeader>
<Box sx={{ p: 2, pt: 0 }}>{children}</Box> <Box>{children}</Box>
</StyledContainer> </StyledContainer>
</Box> </Box>
); );

View File

@ -30,9 +30,7 @@ interface IEnvironmentAccordionBodyProps {
} }
const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({ const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({
[theme.breakpoints.down(400)]: { borderBottom: `1px solid ${theme.palette.divider}`,
padding: theme.spacing(1),
},
})); }));
export const StyledContentList = styled('ol')(({ theme }) => ({ export const StyledContentList = styled('ol')(({ theme }) => ({
@ -44,6 +42,9 @@ export const StyledContentList = styled('ol')(({ theme }) => ({
paddingBlock: theme.spacing(2.5), paddingBlock: theme.spacing(2.5),
position: 'relative', position: 'relative',
}, },
'& > li + li': {
borderTop: `1px solid ${theme.palette.divider}`,
},
'&:not(li > &) > li:first-of-type': { '&:not(li > &) > li:first-of-type': {
// select first list elements in lists that are not directly nested // select first list elements in lists that are not directly nested
// within other lists. // within other lists.
@ -58,7 +59,6 @@ export const StyledContentList = styled('ol')(({ theme }) => ({
export const StyledListItem = styled('li', { export const StyledListItem = styled('li', {
shouldForwardProp: (prop) => prop !== 'type', shouldForwardProp: (prop) => prop !== 'type',
})<{ type?: 'release plan' | 'strategy' }>(({ theme, type }) => ({ })<{ type?: 'release plan' | 'strategy' }>(({ theme, type }) => ({
borderBottom: `1px solid ${theme.palette.divider}`,
background: background:
type === 'release plan' type === 'release plan'
? theme.palette.background.elevation2 ? theme.palette.background.elevation2
@ -290,16 +290,12 @@ export const EnvironmentAccordionBody = ({
}; };
return ( return (
<div>
<StyledAccordionBodyInnerContainer> <StyledAccordionBodyInnerContainer>
<StyledContentList> <StyledContentList>
{releasePlans.length > 0 ? ( {releasePlans.length > 0 ? (
<> <>
{releasePlans.map((plan) => ( {releasePlans.map((plan) => (
<StyledListItem <StyledListItem type='release plan' key={plan.id}>
type='release plan'
key={plan.id}
>
<ReleasePlan <ReleasePlan
plan={plan} plan={plan}
environmentIsDisabled={isDisabled} environmentIsDisabled={isDisabled}
@ -318,6 +314,5 @@ export const EnvironmentAccordionBody = ({
) : null} ) : null}
</StyledContentList> </StyledContentList>
</StyledAccordionBodyInnerContainer> </StyledAccordionBodyInnerContainer>
</div>
); );
}; };

View File

@ -18,22 +18,17 @@ const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
color: theme.palette.divider, color: theme.palette.divider,
})); }));
export const FeatureResultInfoPopoverCell = ({ const LegacyFeatureResultInfoPopoverCell = ({
feature, feature,
input, input,
}: FeatureResultInfoPopoverCellProps) => { }: FeatureResultInfoPopoverCellProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const ref = useRef(null); const ref = useRef(null);
const useNewStrategyDesign = useUiFlag('flagOverviewRedesign');
const togglePopover = () => { const togglePopover = () => {
setOpen(!open); setOpen(!open);
}; };
if (!feature) {
return null;
}
return ( return (
<FeatureResultPopoverWrapper> <FeatureResultPopoverWrapper>
<IconButton onClick={togglePopover}> <IconButton onClick={togglePopover}>
@ -47,7 +42,7 @@ export const FeatureResultInfoPopoverCell = ({
sx: (theme) => ({ sx: (theme) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: theme.spacing(useNewStrategyDesign ? 4 : 6), padding: theme.spacing(6),
width: 728, width: 728,
maxWidth: '100%', maxWidth: '100%',
height: 'auto', height: 'auto',
@ -65,20 +60,6 @@ export const FeatureResultInfoPopoverCell = ({
horizontal: 'left', horizontal: 'left',
}} }}
> >
{useNewStrategyDesign ? (
<>
<FeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
<PlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
</>
) : (
<>
<LegacyFeatureDetails <LegacyFeatureDetails
feature={feature} feature={feature}
input={input} input={input}
@ -88,9 +69,87 @@ export const FeatureResultInfoPopoverCell = ({
feature={feature} feature={feature}
input={input} input={input}
/> />
</>
)}
</Popover> </Popover>
</FeatureResultPopoverWrapper> </FeatureResultPopoverWrapper>
); );
}; };
const DetailsPadding = styled('div')(({ theme }) => ({
paddingInline: `var(--popover-inline-padding, ${theme.spacing(4)})`,
paddingTop: theme.spacing(2.5),
}));
export const NewFeatureResultInfoPopoverCell = ({
feature,
input,
}: FeatureResultInfoPopoverCellProps) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
const togglePopover = () => {
setOpen(!open);
};
return (
<FeatureResultPopoverWrapper>
<IconButton onClick={togglePopover}>
<InfoOutlined ref={ref} />
</IconButton>
<Popover
open={open}
onClose={() => setOpen(false)}
anchorEl={ref.current}
PaperProps={{
sx: (theme) => ({
'--popover-inline-padding': theme.spacing(4),
display: 'flex',
flexDirection: 'column',
width: 728,
maxWidth: '100%',
height: 'auto',
gap: theme.spacing(3),
overflowY: 'auto',
backgroundColor: theme.palette.background.elevation1,
borderRadius: theme.shape.borderRadius,
}),
}}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'center',
horizontal: 'left',
}}
>
<DetailsPadding>
<FeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
</DetailsPadding>
<PlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
</Popover>
</FeatureResultPopoverWrapper>
);
};
export const FeatureResultInfoPopoverCell = (
props: FeatureResultInfoPopoverCellProps,
) => {
const useNewStrategyDesign = useUiFlag('flagOverviewRedesign');
if (!props.feature) {
return null;
}
return useNewStrategyDesign ? (
<NewFeatureResultInfoPopoverCell {...props} />
) : (
<LegacyFeatureResultInfoPopoverCell {...props} />
);
};

View File

@ -1,7 +1,7 @@
import { import {
PlaygroundResultStrategyLists, PlaygroundResultStrategyLists,
WrappedPlaygroundResultStrategyList, WrappedPlaygroundResultStrategyList,
} from './StrategyList/playgroundResultStrategyLists'; } from './StrategyList/LegacyPlaygroundResultStrategyLists';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi'; import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';

View File

@ -24,7 +24,7 @@ const testCases = [
hasUnsatisfiedDependency: true, hasUnsatisfiedDependency: true,
} as PlaygroundFeatureSchema, } as PlaygroundFeatureSchema,
expectedText: expectedText:
'If environment was enabled and parent dependencies were satisfied, then this feature flag would be TRUE with strategies evaluated like so:', 'If the environment was enabled and parent dependencies were satisfied, then this feature flag would be TRUE with strategies evaluated like this:',
}, },
{ {
name: 'Environment enabled and parent dependency not satisfied', name: 'Environment enabled and parent dependency not satisfied',
@ -44,7 +44,7 @@ const testCases = [
hasUnsatisfiedDependency: true, hasUnsatisfiedDependency: true,
} as PlaygroundFeatureSchema, } as PlaygroundFeatureSchema,
expectedText: expectedText:
'If parent dependencies were satisfied, then this feature flag would be TRUE with strategies evaluated like so:', 'If parent dependencies were satisfied, then this feature flag would be TRUE with strategies evaluated like this:',
}, },
{ {
name: 'Environment not enabled and parent dependency satisfied', name: 'Environment not enabled and parent dependency satisfied',
@ -64,7 +64,7 @@ const testCases = [
hasUnsatisfiedDependency: false, hasUnsatisfiedDependency: false,
} as PlaygroundFeatureSchema, } as PlaygroundFeatureSchema,
expectedText: expectedText:
'If environment was enabled, then this feature flag would be TRUE with strategies evaluated like so:', 'If the environment was enabled, then this feature flag would be TRUE with strategies evaluated like this:',
}, },
{ {
name: 'Has disabled strategies and is enabled in environment', name: 'Has disabled strategies and is enabled in environment',

View File

@ -50,7 +50,7 @@ export const PlaygroundResultFeatureStrategyList = ({
<PlaygroundResultStrategyLists <PlaygroundResultStrategyLists
strategies={enabledStrategies || []} strategies={enabledStrategies || []}
input={input} input={input}
titlePrefix={showDisabledStrategies ? 'Enabled' : ''} titlePrefix={showDisabledStrategies ? 'Enabled' : undefined}
/> />
{showDisabledStrategies ? ( {showDisabledStrategies ? (
<PlaygroundResultStrategyLists <PlaygroundResultStrategyLists

View File

@ -0,0 +1,152 @@
import { Fragment } from 'react';
import { Alert, Box, styled, Typography } from '@mui/material';
import type {
PlaygroundStrategySchema,
PlaygroundRequestSchema,
PlaygroundFeatureSchema,
} from 'openapi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyItem } from './StrategyItem/LegacyFeatureStrategyItem';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
const StyledAlertWrapper = styled('div')(({ theme }) => ({
display: 'flex',
padding: `0, 4px`,
flexDirection: 'column',
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.warning.border}`,
}));
const StyledListWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(1, 0.5),
}));
const StyledAlert = styled(Alert)(({ theme }) => ({
border: '0!important',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderBottom: `1px solid ${theme.palette.warning.border}!important`,
}));
interface PlaygroundResultStrategyListProps {
strategies: PlaygroundStrategySchema[];
input?: PlaygroundRequestSchema;
titlePrefix?: string;
infoText?: string;
}
const StyledSubtitle = styled(Typography)(({ theme }) => ({
margin: theme.spacing(2, 1, 2, 0),
color: 'text.secondary',
}));
export const PlaygroundResultStrategyLists = ({
strategies,
input,
titlePrefix,
infoText,
}: PlaygroundResultStrategyListProps) => (
<ConditionallyRender
condition={strategies.length > 0}
show={
<>
<StyledSubtitle variant={'subtitle1'}>{`${
titlePrefix
? titlePrefix.concat(' strategies')
: 'Strategies'
} (${strategies?.length})`}</StyledSubtitle>
<ConditionallyRender
condition={Boolean(infoText)}
show={
<StyledSubtitle variant={'subtitle2'}>
{infoText}
</StyledSubtitle>
}
/>
<Box sx={{ width: '100%' }}>
{strategies?.map((strategy, index) => (
<Fragment key={strategy.id}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text='OR' />}
/>
<FeatureStrategyItem
key={strategy.id}
strategy={strategy}
index={index}
input={input}
/>
</Fragment>
))}
</Box>
</>
}
/>
);
interface IWrappedPlaygroundResultStrategyListProps {
feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
}
const resolveHintText = (feature: PlaygroundFeatureSchema) => {
if (
feature.hasUnsatisfiedDependency &&
!feature.isEnabledInCurrentEnvironment
) {
return 'If the environment was enabled and parent dependencies were satisfied';
}
if (feature.hasUnsatisfiedDependency) {
return 'If parent dependencies were satisfied';
}
if (!feature.isEnabledInCurrentEnvironment) {
return 'If the environment was enabled';
}
return '';
};
export const WrappedPlaygroundResultStrategyList = ({
feature,
input,
}: IWrappedPlaygroundResultStrategyListProps) => {
const enabledStrategies = feature.strategies?.data?.filter(
(strategy) => !strategy.disabled,
);
const disabledStrategies = feature.strategies?.data?.filter(
(strategy) => strategy.disabled,
);
const showDisabledStrategies = disabledStrategies?.length > 0;
return (
<StyledAlertWrapper sx={{ pb: 1, mt: 2 }}>
<StyledAlert severity={'info'} color={'warning'}>
{resolveHintText(feature)}, then this feature flag would be{' '}
{feature.strategies?.result ? 'TRUE' : 'FALSE'} with strategies
evaluated like this:{' '}
</StyledAlert>
<StyledListWrapper sx={{ p: 2.5 }}>
<PlaygroundResultStrategyLists
strategies={enabledStrategies || []}
input={input}
titlePrefix={showDisabledStrategies ? 'Enabled' : ''}
/>
</StyledListWrapper>
<ConditionallyRender
condition={showDisabledStrategies}
show={
<StyledListWrapper sx={{ p: 2.5 }}>
<PlaygroundResultStrategyLists
strategies={disabledStrategies}
input={input}
titlePrefix={'Disabled'}
infoText={
'Disabled strategies are not evaluated for the overall result.'
}
/>
</StyledListWrapper>
}
/>
</StyledAlertWrapper>
);
};

View File

@ -8,18 +8,18 @@ import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { objectId } from 'utils/objectId'; import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DisabledStrategyExecution } from './StrategyExecution/DisabledStrategyExecution'; import { DisabledStrategyExecution } from './StrategyExecution/DisabledStrategyExecution';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer'; import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
interface IFeatureStrategyItemProps { interface IFeatureStrategyItemProps {
strategy: PlaygroundStrategySchema; strategy: PlaygroundStrategySchema;
index: number;
input?: PlaygroundRequestSchema; input?: PlaygroundRequestSchema;
className?: string;
} }
export const FeatureStrategyItem = ({ export const FeatureStrategyItem = ({
strategy, strategy,
input, input,
index, className,
}: IFeatureStrategyItemProps) => { }: IFeatureStrategyItemProps) => {
const { result } = strategy; const { result } = strategy;
const theme = useTheme(); const theme = useTheme();
@ -33,22 +33,19 @@ export const FeatureStrategyItem = ({
return ( return (
<StrategyItemContainer <StrategyItemContainer
style={{
borderColor:
result.enabled && result.evaluationStatus === 'complete'
? theme.palette.success.main
: 'none',
}}
strategy={{ ...strategy, id: `${objectId(strategy)}` }} strategy={{ ...strategy, id: `${objectId(strategy)}` }}
orderNumber={index + 1} strategyHeaderLevel={4}
actions={ className={className}
headerItemsLeft={
<PlaygroundResultChip <PlaygroundResultChip
tabindex={-1}
showIcon={false} showIcon={false}
enabled={result.enabled} enabled={result.enabled}
label={label} label={label}
/> />
} }
> >
{/* todo: use new strategy execution components */}
<ConditionallyRender <ConditionallyRender
condition={Boolean(strategy.disabled)} condition={Boolean(strategy.disabled)}
show={ show={

View File

@ -0,0 +1,70 @@
import { useTheme } from '@mui/material';
import { PlaygroundResultChip } from '../../../../PlaygroundResultChip/PlaygroundResultChip';
import type {
PlaygroundStrategySchema,
PlaygroundRequestSchema,
} from 'openapi';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DisabledStrategyExecution } from './StrategyExecution/DisabledStrategyExecution';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/LegacyStrategyItemContainer';
interface IFeatureStrategyItemProps {
strategy: PlaygroundStrategySchema;
index: number;
input?: PlaygroundRequestSchema;
}
export const FeatureStrategyItem = ({
strategy,
input,
index,
}: IFeatureStrategyItemProps) => {
const { result } = strategy;
const theme = useTheme();
const label =
result.evaluationStatus === 'incomplete' ||
result.evaluationStatus === 'unevaluated'
? 'Unevaluated'
: result.enabled
? 'True'
: 'False';
return (
<StrategyItemContainer
style={{
borderColor:
result.enabled && result.evaluationStatus === 'complete'
? theme.palette.success.main
: 'none',
}}
strategy={{ ...strategy, id: `${objectId(strategy)}` }}
orderNumber={index + 1}
actions={
<PlaygroundResultChip
showIcon={false}
enabled={result.enabled}
label={label}
/>
}
>
<ConditionallyRender
condition={Boolean(strategy.disabled)}
show={
<DisabledStrategyExecution
strategyResult={strategy}
input={input}
/>
}
elseShow={
<StrategyExecution
strategyResult={strategy}
input={input}
percentageFill={theme.palette.background.elevation2}
/>
}
/>
</StrategyItemContainer>
);
};

View File

@ -1,13 +1,16 @@
import { Fragment } from 'react'; import { Alert, styled } from '@mui/material';
import { Alert, Box, styled, Typography } from '@mui/material';
import type { import type {
PlaygroundStrategySchema, PlaygroundStrategySchema,
PlaygroundRequestSchema, PlaygroundRequestSchema,
PlaygroundFeatureSchema, PlaygroundFeatureSchema,
} from 'openapi'; } from 'openapi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
StyledContentList,
StyledListItem,
} from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
const StyledAlertWrapper = styled('div')(({ theme }) => ({ const StyledAlertWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -31,13 +34,28 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
interface PlaygroundResultStrategyListProps { interface PlaygroundResultStrategyListProps {
strategies: PlaygroundStrategySchema[]; strategies: PlaygroundStrategySchema[];
input?: PlaygroundRequestSchema; input?: PlaygroundRequestSchema;
titlePrefix?: string; titlePrefix?: 'Enabled' | 'Disabled';
infoText?: string; infoText?: string;
} }
const StyledHeaderGroup = styled('hgroup')(({ theme }) => ({
paddingInline: `var(--popover-inline-padding, ${theme.spacing(4)})`,
paddingBottom: theme.spacing(2),
borderBottom: `1px solid ${theme.palette.divider}`,
}));
const StyledSubtitle = styled(Typography)(({ theme }) => ({ const StyledListTitle = styled('h4')(({ theme }) => ({
margin: theme.spacing(2, 1, 2, 0), fontWeight: 'normal',
color: 'text.secondary', fontSize: theme.typography.body1.fontSize,
margin: 0,
}));
const StyledListTitleDescription = styled('p')(({ theme }) => ({
fontWeight: 'bold',
fontSize: theme.typography.body2.fontSize,
}));
const StyledFeatureStrategyItem = styled(FeatureStrategyItem)(({ theme }) => ({
paddingInline: `var(--popover-inline-padding, ${theme.spacing(4)})`,
})); }));
export const PlaygroundResultStrategyLists = ({ export const PlaygroundResultStrategyLists = ({
@ -45,44 +63,39 @@ export const PlaygroundResultStrategyLists = ({
input, input,
titlePrefix, titlePrefix,
infoText, infoText,
}: PlaygroundResultStrategyListProps) => ( }: PlaygroundResultStrategyListProps) => {
<ConditionallyRender if (strategies.length === 0) {
condition={strategies.length > 0} return null;
show={ }
<>
<StyledSubtitle variant={'subtitle1'}>{`${ return (
<div>
<StyledHeaderGroup>
<StyledListTitle>{`${
titlePrefix titlePrefix
? titlePrefix.concat(' strategies') ? titlePrefix.concat(' strategies')
: 'Strategies' : 'Strategies'
} (${strategies?.length})`}</StyledSubtitle> } (${strategies?.length})`}</StyledListTitle>
<ConditionallyRender {infoText ? (
condition={Boolean(infoText)} <StyledListTitleDescription>
show={
<StyledSubtitle variant={'subtitle2'}>
{infoText} {infoText}
</StyledSubtitle> </StyledListTitleDescription>
} ) : null}
/> </StyledHeaderGroup>
<Box sx={{ width: '100%' }}> <StyledContentList>
{strategies?.map((strategy, index) => ( {strategies?.map((strategy, index) => (
<Fragment key={strategy.id}> <StyledListItem key={strategy.id}>
<ConditionallyRender {index > 0 ? <StrategySeparator /> : ''}
condition={index > 0} <StyledFeatureStrategyItem
show={<StrategySeparator text='OR' />}
/>
<FeatureStrategyItem
key={strategy.id}
strategy={strategy} strategy={strategy}
index={index}
input={input} input={input}
/> />
</Fragment> </StyledListItem>
))} ))}
</Box> </StyledContentList>
</> </div>
}
/>
); );
};
interface IWrappedPlaygroundResultStrategyListProps { interface IWrappedPlaygroundResultStrategyListProps {
feature: PlaygroundFeatureSchema; feature: PlaygroundFeatureSchema;
@ -94,13 +107,13 @@ const resolveHintText = (feature: PlaygroundFeatureSchema) => {
feature.hasUnsatisfiedDependency && feature.hasUnsatisfiedDependency &&
!feature.isEnabledInCurrentEnvironment !feature.isEnabledInCurrentEnvironment
) { ) {
return 'If environment was enabled and parent dependencies were satisfied'; return 'If the environment was enabled and parent dependencies were satisfied';
} }
if (feature.hasUnsatisfiedDependency) { if (feature.hasUnsatisfiedDependency) {
return 'If parent dependencies were satisfied'; return 'If parent dependencies were satisfied';
} }
if (!feature.isEnabledInCurrentEnvironment) { if (!feature.isEnabledInCurrentEnvironment) {
return 'If environment was enabled'; return 'If the environment was enabled';
} }
return ''; return '';
}; };
@ -123,19 +136,19 @@ export const WrappedPlaygroundResultStrategyList = ({
<StyledAlert severity={'info'} color={'warning'}> <StyledAlert severity={'info'} color={'warning'}>
{resolveHintText(feature)}, then this feature flag would be{' '} {resolveHintText(feature)}, then this feature flag would be{' '}
{feature.strategies?.result ? 'TRUE' : 'FALSE'} with strategies {feature.strategies?.result ? 'TRUE' : 'FALSE'} with strategies
evaluated like so:{' '} evaluated like this:{' '}
</StyledAlert> </StyledAlert>
<StyledListWrapper sx={{ p: 2.5 }}> <StyledListWrapper>
<PlaygroundResultStrategyLists <PlaygroundResultStrategyLists
strategies={enabledStrategies || []} strategies={enabledStrategies || []}
input={input} input={input}
titlePrefix={showDisabledStrategies ? 'Enabled' : ''} titlePrefix={showDisabledStrategies ? 'Enabled' : undefined}
/> />
</StyledListWrapper> </StyledListWrapper>
<ConditionallyRender <ConditionallyRender
condition={showDisabledStrategies} condition={showDisabledStrategies}
show={ show={
<StyledListWrapper sx={{ p: 2.5 }}> <StyledListWrapper>
<PlaygroundResultStrategyLists <PlaygroundResultStrategyLists
strategies={disabledStrategies} strategies={disabledStrategies}
input={input} input={input}