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:
parent
6bf0211140
commit
537bcdc1b7
@ -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}`,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
@ -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()}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user