1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Chore(1-3390)/playground strategy execution: constraints (#9532)

Implements the new design for playground constraints. They're not in use
in segments yet, and strategy parameters have not been touched. This PR
establishes a pattern that we can follow for strategies and parameters
later.


![image](https://github.com/user-attachments/assets/c23d538d-f27f-43f2-8e29-fa8044c11d48)

The PR also includes a change in how the constraint item organizes its
children: it now takes care adding padding and spacing itself, instead
of the children doing that. It looks right most places, but segments
aren't quite right anymore. However, as this is behind a flag, I'd
rather fix that in a separate PR.

---------

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
Thomas Heartman 2025-03-17 14:30:11 +01:00 committed by GitHub
parent 37aeb62d66
commit cf1ba8fcc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 241 additions and 100 deletions

View File

@ -15,6 +15,16 @@ const StyledListItem = styled('li')(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
background: theme.palette.background.default,
padding: theme.spacing(2, 3),
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(1),
'&:has(>.MuiAccordion-root)': {
// todo: look at this later. MUI accordions rely heavily on their
// padding, but it doesn't collapse with the surrounding padding here,
// so they become super chunky otherwise.
paddingBlock: 0,
},
}));
const StyledAnd = styled('div')(({ theme }) => ({

View File

@ -12,7 +12,6 @@ const StyledContainer = styled('div')(({ theme }) => ({
gap: theme.spacing(1),
alignItems: 'center',
fontSize: theme.typography.body2.fontSize,
margin: theme.spacing(2, 3),
}));
const StyledContent = styled('div')(({ theme }) => ({

View File

@ -26,14 +26,14 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
boxShadow: 'none',
margin: 0,
padding: 0,
'::before': {
display: 'none',
},
}));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
padding: 0,
fontSize: theme.typography.body2.fontSize,
'.MuiAccordionSummary-content, .Mui-expanded.MuiAccordionSummary-content': {
margin: 0,
},
}));
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({

View File

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

View File

@ -4,11 +4,9 @@ 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/StrategyItemContainer';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
interface IFeatureStrategyItemProps {
strategy: PlaygroundStrategySchema;
@ -45,23 +43,7 @@ export const FeatureStrategyItem = ({
/>
}
>
{/* todo: use new strategy execution components */}
<ConditionallyRender
condition={Boolean(strategy.disabled)}
show={
<DisabledStrategyExecution
strategyResult={strategy}
input={input}
/>
}
elseShow={
<StrategyExecution
strategyResult={strategy}
input={input}
percentageFill={theme.palette.background.elevation2}
/>
}
/>
<StrategyExecution strategyResult={strategy} input={input} />
</StrategyItemContainer>
);
};

View File

@ -4,7 +4,7 @@ import type {
PlaygroundStrategySchema,
PlaygroundRequestSchema,
} from 'openapi';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { StrategyExecution } from './StrategyExecution/LegacyStrategyExecution';
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DisabledStrategyExecution } from './StrategyExecution/DisabledStrategyExecution';

View File

@ -1,59 +1,79 @@
import { Fragment, type VFC } from 'react';
import type { FC } from 'react';
import type {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'openapi';
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { ConstraintItem } from 'component/common/ConstraintsList/ConstraintItem/ConstraintItem';
import CheckCircle from '@mui/icons-material/CheckCircle';
import { styled } from '@mui/material';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintError } from './ConstraintError/ConstraintError';
import { ConstraintOk } from './ConstraintOk/ConstraintOk';
import Cancel from '@mui/icons-material/Cancel';
interface IConstraintExecutionProps {
constraints?: PlaygroundConstraintSchema[];
constraint?: PlaygroundConstraintSchema;
input?: PlaygroundRequestSchema;
}
export const ConstraintExecutionWrapper = styled('div')(() => ({
width: '100%',
const StyledContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'variant',
})<{ variant: 'ok' | 'error' }>(({ theme, variant }) => ({
'--font-size': theme.typography.body2.fontSize,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: theme.spacing(1),
paddingInline: theme.spacing(0.25),
color:
variant === 'ok'
? theme.palette.success.dark
: theme.palette.error.dark,
fontSize: 'var(--font-size)',
svg: {
fontSize: `calc(var(--font-size) * 1.25)`,
},
}));
export const ConstraintExecution: VFC<IConstraintExecutionProps> = ({
constraints,
input,
}) => {
if (!constraints) return null;
const ConstraintOk = () => {
return (
<ConstraintExecutionWrapper>
{constraints?.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text='AND' />}
/>
<ConstraintAccordionView
constraint={constraint}
compact
renderAfter={
<ConditionallyRender
condition={constraint.result}
show={<ConstraintOk />}
elseShow={
<ConstraintError
input={input}
constraint={constraint}
/>
}
/>
}
/>
</Fragment>
))}
</ConstraintExecutionWrapper>
<StyledContainer variant='ok'>
<CheckCircle aria-hidden='true' />
<p>Constraint met by value in context</p>
</StyledContainer>
);
};
export const ConstraintError = ({ text }: { text: string }) => {
return (
<StyledContainer variant='error'>
<Cancel aria-hidden='true' />
<p>{text}</p>
</StyledContainer>
);
};
export const ConstraintExecution: FC<IConstraintExecutionProps> = ({
constraint,
input,
}) => {
if (!constraint) return null;
const errorText = () => {
const value = input?.context[constraint.contextName];
if (value) {
return `Constraint not met the value in the context: { ${value} } is not ${constraint.operator} ${constraint.contextName}`;
}
return `Constraint not met no value was specified for ${constraint.contextName}`;
};
return (
<>
<ConstraintItem {...constraint} />
{constraint.result ? (
<ConstraintOk />
) : (
<ConstraintError text={errorText()} />
)}
</>
);
};

View File

@ -0,0 +1,59 @@
import { Fragment, type VFC } from 'react';
import type {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'openapi';
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintError } from './ConstraintError/LegacyConstraintError';
import { ConstraintOk } from './ConstraintOk/LegacyConstraintOk';
interface IConstraintExecutionProps {
constraints?: PlaygroundConstraintSchema[];
input?: PlaygroundRequestSchema;
}
export const ConstraintExecutionWrapper = styled('div')(() => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
}));
export const ConstraintExecution: VFC<IConstraintExecutionProps> = ({
constraints,
input,
}) => {
if (!constraints) return null;
return (
<ConstraintExecutionWrapper>
{constraints?.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text='AND' />}
/>
<ConstraintAccordionView
constraint={constraint}
compact
renderAfter={
<ConditionallyRender
condition={constraint.result}
show={<ConstraintOk />}
elseShow={
<ConstraintError
input={input}
constraint={constraint}
/>
}
/>
}
/>
</Fragment>
))}
</ConstraintExecutionWrapper>
);
};

View File

@ -0,0 +1,87 @@
import { Fragment, type VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material';
import type {
PlaygroundRequestSchema,
PlaygroundStrategySchema,
} from 'openapi';
import { ConstraintExecution } from './ConstraintExecution/LegacyConstraintExecution';
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters';
import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParams';
import { formattedStrategyNames } from 'utils/strategyNames';
import { StyledBoxSummary } from './StrategyExecution.styles';
import { Badge } from 'component/common/Badge/Badge';
interface IStrategyExecutionProps {
strategyResult: PlaygroundStrategySchema;
percentageFill?: string;
input?: PlaygroundRequestSchema;
}
const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0),
}));
export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
strategyResult,
input,
}) => {
const { name, constraints, segments, parameters } = strategyResult;
const hasSegments = Boolean(segments && segments.length > 0);
const hasConstraints = Boolean(constraints && constraints?.length > 0);
const hasExecutionParameters =
name !== 'default' &&
Object.keys(formattedStrategyNames).includes(name);
const hasCustomStrategyParameters =
Object.keys(parameters).length > 0 &&
strategyResult.result.evaluationStatus === 'incomplete'; // Use of custom strategy can be more explicit from the API
if (!parameters) {
return null;
}
const items = [
hasSegments && <SegmentExecution segments={segments} input={input} />,
hasConstraints && (
<ConstraintExecution constraints={constraints} input={input} />
),
hasExecutionParameters && (
<PlaygroundResultStrategyExecutionParameters
parameters={parameters}
constraints={constraints}
input={input}
/>
),
hasCustomStrategyParameters && (
<CustomStrategyParams strategyName={name} parameters={parameters} />
),
name === 'default' && (
<StyledBoxSummary sx={{ width: '100%' }}>
The standard strategy is <Badge color='success'>ON</Badge> for
all users.
</StyledBoxSummary>
),
].filter(Boolean);
return (
<StyledStrategyExecutionWrapper>
{items.map((item, index) => (
<Fragment key={index}>
<ConditionallyRender
condition={
index > 0 &&
(strategyResult.name === 'flexibleRollout'
? index < items.length
: index < items.length - 1)
}
show={<StrategySeparator text='AND' />}
/>
{item}
</Fragment>
))}
</StyledStrategyExecutionWrapper>
);
};

View File

@ -1,6 +1,6 @@
import { Fragment, type VFC } from 'react';
import type { PlaygroundSegmentSchema, PlaygroundRequestSchema } from 'openapi';
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
import { ConstraintExecution } from '../ConstraintExecution/LegacyConstraintExecution';
import CancelOutlined from '@mui/icons-material/CancelOutlined';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled, Typography } from '@mui/material';

View File

@ -1,7 +1,3 @@
import { Fragment, type VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
import { styled } from '@mui/material';
import type {
PlaygroundRequestSchema,
PlaygroundStrategySchema,
@ -13,6 +9,9 @@ import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParam
import { formattedStrategyNames } from 'utils/strategyNames';
import { StyledBoxSummary } from './StrategyExecution.styles';
import { Badge } from 'component/common/Badge/Badge';
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
import { objectId } from 'utils/objectId';
import type { FC } from 'react';
interface IStrategyExecutionProps {
strategyResult: PlaygroundStrategySchema;
@ -20,11 +19,7 @@ interface IStrategyExecutionProps {
input?: PlaygroundRequestSchema;
}
const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0),
}));
export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
export const StrategyExecution: FC<IStrategyExecutionProps> = ({
strategyResult,
input,
}) => {
@ -45,9 +40,15 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
const items = [
hasSegments && <SegmentExecution segments={segments} input={input} />,
hasConstraints && (
<ConstraintExecution constraints={constraints} input={input} />
),
...(hasConstraints
? constraints.map((constraint) => (
<ConstraintExecution
key={objectId(constraint)}
constraint={constraint}
input={input}
/>
))
: []),
hasExecutionParameters && (
<PlaygroundResultStrategyExecutionParameters
parameters={parameters}
@ -66,22 +67,5 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
),
].filter(Boolean);
return (
<StyledStrategyExecutionWrapper>
{items.map((item, index) => (
<Fragment key={index}>
<ConditionallyRender
condition={
index > 0 &&
(strategyResult.name === 'flexibleRollout'
? index < items.length
: index < items.length - 1)
}
show={<StrategySeparator text='AND' />}
/>
{item}
</Fragment>
))}
</StyledStrategyExecutionWrapper>
);
return <ConstraintsList>{items}</ConstraintsList>;
};