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.  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:
parent
37aeb62d66
commit
cf1ba8fcc5
@ -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 }) => ({
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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 }) => ({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
PlaygroundResultStrategyLists,
|
||||
WrappedPlaygroundResultStrategyList,
|
||||
} from './StrategyList/playgroundResultStrategyLists';
|
||||
} from './StrategyList/PlaygroundResultStrategyLists';
|
||||
import type { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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()} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
@ -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>;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user