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

fix: proper spacing and dividers between strategies (#1187)

* fix: proper spacing and dividers between strategies

* fix: improve strategy execution list logic

* update custom strategy execution styles

* interpret not defined custom strategy parameters
This commit is contained in:
Tymoteusz Czech 2022-08-03 09:23:57 +02:00 committed by GitHub
parent 6bf0211140
commit 537bcdc1b7
4 changed files with 199 additions and 192 deletions

View File

@ -2,17 +2,11 @@ import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({ export const useStyles = makeStyles()(theme => ({
valueContainer: { valueContainer: {
display: 'flex', padding: theme.spacing(2, 3),
alignItems: 'center', border: `1px solid ${theme.palette.dividerAlternative}`,
gap: '1ch', borderRadius: theme.shape.borderRadius,
}, },
valueSeparator: { valueSeparator: {
color: theme.palette.grey[700], color: theme.palette.grey[700],
}, },
summary: {
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
},
})); }));

View File

@ -1,56 +1,62 @@
import { Fragment } from 'react'; import { Fragment, useMemo, VFC } from 'react';
import { Box, Chip } from '@mui/material'; import { Box, Chip, Tooltip } from '@mui/material';
import { IFeatureStrategy } from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { ConstraintItem } from './ConstraintItem/ConstraintItem'; import { ConstraintItem } from './ConstraintItem/ConstraintItem';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment'; import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { useStyles } from './StrategyExecution.styles'; import { useStyles } from './StrategyExecution.styles';
import { import {
parseParameterString,
parseParameterNumber, parseParameterNumber,
parseParameterString,
parseParameterStrings, parseParameterStrings,
} from 'utils/parseParameter'; } from 'utils/parseParameter';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
interface IStrategyExecutionProps { interface IStrategyExecutionProps {
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
percentageFill?: string; percentageFill?: string;
} }
export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => { const NoItems: VFC = () => (
<Box sx={{ px: 3, color: 'text.disabled' }}>
This strategy does not have constraints or parameters.
</Box>
);
export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
strategy,
}) => {
const { parameters, constraints = [] } = strategy; const { parameters, constraints = [] } = strategy;
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const { strategies } = useStrategies(); const { strategies } = useStrategies();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { segments } = useSegments(strategy.id);
if (!parameters) {
return null;
}
const definition = strategies.find(strategyDefinition => { const definition = strategies.find(strategyDefinition => {
return strategyDefinition.name === strategy.name; return strategyDefinition.name === strategy.name;
}); });
const renderParameters = () => { const parametersList = useMemo(() => {
if (definition?.editable) return null; if (!parameters || definition?.editable) return null;
return Object.keys(parameters).map(key => { return Object.keys(parameters).map(key => {
switch (key) { switch (key) {
case 'rollout': case 'rollout':
case 'Rollout': case 'Rollout':
const percentage = parseParameterNumber(parameters[key]); const percentage = parseParameterNumber(parameters[key]);
return ( return (
<Box <Box
className={styles.summary} className={styles.valueContainer}
key={key}
sx={{ display: 'flex', alignItems: 'center' }} sx={{ display: 'flex', alignItems: 'center' }}
> >
<Box sx={{ mr: '1rem' }}> <Box sx={{ mr: 2 }}>
<PercentageCircle <PercentageCircle
percentage={percentage} percentage={percentage}
size="2rem" size="2rem"
@ -93,191 +99,195 @@ export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => {
return null; return null;
} }
}); });
}; }, [parameters, definition, constraints, styles]);
const customStrategyList = useMemo(() => {
if (!parameters || !definition?.editable) return null;
const isSetTo = (
<span className={styles.valueSeparator}>{' is set to '}</span>
);
return definition?.parameters.map(param => {
const { type, name } = { ...param };
if (!type || !name || parameters[name] === undefined) {
return null;
}
const nameItem = (
<StringTruncator maxLength={15} maxWidth="150" text={name} />
);
const renderCustomStrategy = () => {
if (!definition?.editable) return null;
return definition?.parameters.map((param: any, index: number) => {
const notLastItem = index !== definition?.parameters?.length - 1;
switch (param?.type) { switch (param?.type) {
case 'list': case 'list':
const values = parseParameterStrings( const values = parseParameterStrings(parameters[name]);
strategy?.parameters[param.name]
); return values.length > 0 ? (
return ( <div className={styles.valueContainer}>
<Fragment key={param?.name}> {nameItem}{' '}
<ConstraintItem value={values} text={param.name} /> <span className={styles.valueSeparator}>
<ConditionallyRender has {values.length}{' '}
condition={notLastItem} {values.length > 1 ? `items` : 'item'}:{' '}
show={<StrategySeparator text="AND" />} {values.map((item: string) => (
/> <Chip
</Fragment> key={item}
); label={
case 'percentage': <StringTruncator
return ( maxWidth="300"
<Fragment key={param?.name}> text={item}
<div> maxLength={50}
<Chip />
size="small" }
variant="outlined" sx={{ mr: 0.5 }}
color="success"
label={`${
strategy?.parameters[param.name]
}%`}
/>{' '}
of your base{' '}
{constraints?.length > 0
? 'who match constraints'
: ''}{' '}
is included.
</div>
<PercentageCircle
percentage={parseParameterNumber(
strategy.parameters[param.name]
)}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'boolean':
return (
<Fragment key={param.name}>
<p key={param.name}>
<StringTruncator
maxLength={15}
maxWidth="150"
text={param.name}
/>{' '}
{strategy.parameters[param.name]}
</p>
<ConditionallyRender
condition={
typeof strategy.parameters[param.name] !==
'undefined'
}
show={
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/> />
} ))}
</span>
</div>
) : null;
case 'percentage':
const percentage = parseParameterNumber(parameters[name]);
return parameters[name] !== '' ? (
<Box
className={styles.valueContainer}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: 2 }}>
<PercentageCircle
percentage={percentage}
size="2rem"
/>
</Box>
<div>
{nameItem}
{isSetTo}
<Chip
color="success"
variant="outlined"
size="small"
label={`${percentage}%`}
/>
</div>
</Box>
) : null;
case 'boolean':
return parameters[name] === 'true' ||
parameters[name] === 'false' ? (
<div className={styles.valueContainer}>
<StringTruncator
maxLength={15}
maxWidth="150"
text={name}
/> />
</Fragment> {isSetTo}
); <Chip
color={
parameters[name] === 'true'
? 'success'
: 'error'
}
variant="outlined"
size="small"
label={parameters[name]}
/>
</div>
) : null;
case 'string': case 'string':
const value = parseParameterString( const value = parseParameterString(parameters[name]);
strategy.parameters[param.name] return typeof parameters[name] !== 'undefined' ? (
); <div className={styles.valueContainer}>
return ( {nameItem}
<ConditionallyRender <ConditionallyRender
condition={ condition={value === ''}
typeof strategy.parameters[param.name] !== show={
'undefined' <span className={styles.valueSeparator}>
} {' is an empty string'}
key={param.name} </span>
show={ }
<> elseShow={
<p className={styles.valueContainer}> <>
<StringTruncator {isSetTo}
maxWidth="150"
maxLength={15}
text={param.name}
/>
<span className={styles.valueSeparator}>
is set to
</span>
<StringTruncator <StringTruncator
maxWidth="300" maxWidth="300"
text={value} text={value}
maxLength={50} maxLength={50}
/> />
</p> </>
<ConditionallyRender }
condition={notLastItem} />
show={<StrategySeparator text="AND" />} </div>
/> ) : null;
</>
}
/>
);
case 'number': case 'number':
const number = parseParameterNumber( const number = parseParameterNumber(parameters[name]);
strategy.parameters[param.name] return parameters[name] !== '' && number !== undefined ? (
); <div className={styles.valueContainer}>
return ( {nameItem}
<ConditionallyRender {isSetTo}
condition={number !== undefined} <StringTruncator
key={param.name} maxWidth="300"
show={ text={String(number)}
<> maxLength={50}
<p className={styles.valueContainer}> />
<StringTruncator </div>
maxLength={15} ) : null;
maxWidth="150"
text={param.name}
/>
<span className={styles.valueSeparator}>
is set to
</span>
<StringTruncator
maxWidth="300"
text={String(number)}
maxLength={50}
/>
</p>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</>
}
/>
);
case 'default': case 'default':
return null; return null;
} }
return null; return null;
}); });
}; }, [parameters, definition, styles]);
if (!parameters) {
return <NoItems />;
}
const listItems = [
Boolean(uiConfig.flags.SE) && segments && segments.length > 0 && (
<FeatureOverviewSegment strategyId={strategy.id} />
),
constraints.length > 0 && (
<ConstraintAccordionList
constraints={constraints}
showLabel={false}
/>
),
strategy.name === 'default' && (
<>
<Box sx={{ width: '100%' }} className={styles.valueContainer}>
The standard strategy is{' '}
<Chip
variant="outlined"
size="small"
color="success"
label="ON"
/>{' '}
for all users.
</Box>
</>
),
...(parametersList ?? []),
...(customStrategyList ?? []),
].filter(Boolean);
return ( return (
<> <ConditionallyRender
<ConditionallyRender condition={listItems.length > 0}
condition={Boolean(uiConfig.flags.SE)} show={
show={<FeatureOverviewSegment strategyId={strategy.id} />} <>
/> {listItems.map((item, index) => (
<ConditionallyRender <Fragment key={index}>
condition={constraints.length > 0} <ConditionallyRender
show={ condition={index > 0}
<> show={<StrategySeparator text="AND" />}
<ConstraintAccordionList />
constraints={constraints} {item}
showLabel={false} </Fragment>
/> ))}
<StrategySeparator text="AND" /> </>
</> }
} elseShow={<NoItems />}
/> />
<ConditionallyRender
condition={strategy.name === 'default'}
show={
<Box sx={{ width: '100%' }} className={styles.summary}>
The standard strategy is{' '}
<Chip
variant="outlined"
size="small"
color="success"
label="ON"
/>{' '}
for all users.
</Box>
}
/>
{renderParameters()}
{renderCustomStrategy()}
</>
); );
}; };

View File

@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
import { DonutLarge } from '@mui/icons-material'; import { DonutLarge } from '@mui/icons-material';
import { useStyles } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment.styles'; import { useStyles } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment.styles';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
interface IFeatureOverviewSegmentProps { interface IFeatureOverviewSegmentProps {
strategyId: string; strategyId: string;
@ -32,7 +31,6 @@ export const FeatureOverviewSegment = ({
{segment.name} {segment.name}
</Link> </Link>
</div> </div>
<StrategySeparator text="AND" />
</Fragment> </Fragment>
))} ))}
</> </>

View File

@ -309,6 +309,11 @@ export default createTheme({
...(ownerState.color === 'default' && { ...(ownerState.color === 'default' && {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
}), }),
...(ownerState.color === 'error' && {
color: theme.palette.error.dark,
background: theme.palette.error.light,
borderColor: theme.palette.error.border,
}),
}), }),
}), }),
}, },