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 => ({
valueContainer: {
display: 'flex',
alignItems: 'center',
gap: '1ch',
padding: theme.spacing(2, 3),
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadius,
},
valueSeparator: {
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 { Box, Chip } from '@mui/material';
import { Fragment, useMemo, VFC } from 'react';
import { Box, Chip, Tooltip } from '@mui/material';
import { IFeatureStrategy } from 'interfaces/strategy';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { ConstraintItem } from './ConstraintItem/ConstraintItem';
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 { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { useStyles } from './StrategyExecution.styles';
import {
parseParameterString,
parseParameterNumber,
parseParameterString,
parseParameterStrings,
} from 'utils/parseParameter';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
interface IStrategyExecutionProps {
strategy: IFeatureStrategy;
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 { classes: styles } = useStyles();
const { strategies } = useStrategies();
const { uiConfig } = useUiConfig();
if (!parameters) {
return null;
}
const { segments } = useSegments(strategy.id);
const definition = strategies.find(strategyDefinition => {
return strategyDefinition.name === strategy.name;
});
const renderParameters = () => {
if (definition?.editable) return null;
const parametersList = useMemo(() => {
if (!parameters || definition?.editable) return null;
return Object.keys(parameters).map(key => {
switch (key) {
case 'rollout':
case 'Rollout':
const percentage = parseParameterNumber(parameters[key]);
return (
<Box
className={styles.summary}
key={key}
className={styles.valueContainer}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: '1rem' }}>
<Box sx={{ mr: 2 }}>
<PercentageCircle
percentage={percentage}
size="2rem"
@ -93,191 +99,195 @@ export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => {
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) {
case 'list':
const values = parseParameterStrings(
strategy?.parameters[param.name]
);
return (
<Fragment key={param?.name}>
<ConstraintItem value={values} text={param.name} />
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'percentage':
return (
<Fragment key={param?.name}>
<div>
<Chip
size="small"
variant="outlined"
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" />}
const values = parseParameterStrings(parameters[name]);
return values.length > 0 ? (
<div className={styles.valueContainer}>
{nameItem}{' '}
<span className={styles.valueSeparator}>
has {values.length}{' '}
{values.length > 1 ? `items` : 'item'}:{' '}
{values.map((item: string) => (
<Chip
key={item}
label={
<StringTruncator
maxWidth="300"
text={item}
maxLength={50}
/>
}
sx={{ mr: 0.5 }}
/>
}
))}
</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':
const value = parseParameterString(
strategy.parameters[param.name]
);
return (
<ConditionallyRender
condition={
typeof strategy.parameters[param.name] !==
'undefined'
}
key={param.name}
show={
<>
<p className={styles.valueContainer}>
<StringTruncator
maxWidth="150"
maxLength={15}
text={param.name}
/>
<span className={styles.valueSeparator}>
is set to
</span>
const value = parseParameterString(parameters[name]);
return typeof parameters[name] !== 'undefined' ? (
<div className={styles.valueContainer}>
{nameItem}
<ConditionallyRender
condition={value === ''}
show={
<span className={styles.valueSeparator}>
{' is an empty string'}
</span>
}
elseShow={
<>
{isSetTo}
<StringTruncator
maxWidth="300"
text={value}
maxLength={50}
/>
</p>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</>
}
/>
);
</>
}
/>
</div>
) : null;
case 'number':
const number = parseParameterNumber(
strategy.parameters[param.name]
);
return (
<ConditionallyRender
condition={number !== undefined}
key={param.name}
show={
<>
<p className={styles.valueContainer}>
<StringTruncator
maxLength={15}
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" />}
/>
</>
}
/>
);
const number = parseParameterNumber(parameters[name]);
return parameters[name] !== '' && number !== undefined ? (
<div className={styles.valueContainer}>
{nameItem}
{isSetTo}
<StringTruncator
maxWidth="300"
text={String(number)}
maxLength={50}
/>
</div>
) : null;
case 'default':
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 (
<>
<ConditionallyRender
condition={Boolean(uiConfig.flags.SE)}
show={<FeatureOverviewSegment strategyId={strategy.id} />}
/>
<ConditionallyRender
condition={constraints.length > 0}
show={
<>
<ConstraintAccordionList
constraints={constraints}
showLabel={false}
/>
<StrategySeparator text="AND" />
</>
}
/>
<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()}
</>
<ConditionallyRender
condition={listItems.length > 0}
show={
<>
{listItems.map((item, index) => (
<Fragment key={index}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="AND" />}
/>
{item}
</Fragment>
))}
</>
}
elseShow={<NoItems />}
/>
);
};

View File

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

View File

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