mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
refactor: fix small issues around custom strategies (#1181)
* refactor: validate strategy name on blur * refactor: remove strategy parameter type text in favor of docs * refactor: improve pie chart rendering * refactor: show icons for all feature strategies * refactor: fix list parameter add button style
This commit is contained in:
parent
79aa2d4ebe
commit
710ebe08b3
@ -1,51 +1,45 @@
|
|||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
interface IPercentageCircleProps {
|
interface IPercentageCircleProps {
|
||||||
styles?: object;
|
|
||||||
percentage: number;
|
percentage: number;
|
||||||
secondaryPieColor?: string;
|
size?: `${number}rem`;
|
||||||
className?: string;
|
|
||||||
hideNumber?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PercentageCircle = ({
|
const PercentageCircle = ({
|
||||||
styles,
|
|
||||||
percentage,
|
percentage,
|
||||||
secondaryPieColor,
|
size = '4rem',
|
||||||
hideNumber,
|
|
||||||
...rest
|
|
||||||
}: IPercentageCircleProps) => {
|
}: IPercentageCircleProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
let circle = {
|
const style: CSSProperties = {
|
||||||
height: '65px',
|
display: 'block',
|
||||||
width: '65px',
|
borderRadius: '100%',
|
||||||
borderRadius: '50%',
|
transform: 'rotate(-90deg)',
|
||||||
color: '#fff',
|
height: size,
|
||||||
backgroundColor: theme.palette.grey[200],
|
width: size,
|
||||||
backgroundImage: `conic-gradient(${
|
background: theme.palette.grey[200],
|
||||||
theme.palette.primary.light
|
|
||||||
} ${percentage}%, ${secondaryPieColor || theme.palette.grey[200]} 1%)`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (percentage === 100) {
|
// The percentage circle used to be drawn by CSS with a conic-gradient,
|
||||||
return (
|
// but the result was either jagged or blurry. SVG seems to look better.
|
||||||
<div
|
// See https://stackoverflow.com/a/70659532.
|
||||||
style={{
|
const r = 100 / (2 * Math.PI);
|
||||||
...circle,
|
const d = 2 * r;
|
||||||
...styles,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: '12px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hideNumber ? null : '100%'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div style={{ ...circle, ...styles }} {...rest} />;
|
return (
|
||||||
|
<svg viewBox={`0 0 ${d} ${d}`} style={style} aria-hidden>
|
||||||
|
<circle
|
||||||
|
r={r}
|
||||||
|
cx={r}
|
||||||
|
cy={r}
|
||||||
|
fill="none"
|
||||||
|
stroke={theme.palette.primary.light}
|
||||||
|
strokeWidth={d}
|
||||||
|
strokeDasharray={`${percentage} 100`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PercentageCircle;
|
export default PercentageCircle;
|
||||||
|
@ -13,24 +13,17 @@ export const FeatureStrategyIcons = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strategyNames = strategies.map(strategy => strategy.name);
|
|
||||||
const uniqueStrategyNames = uniqueValues(strategyNames);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledList aria-label="Feature strategies">
|
<StyledList aria-label="Feature strategies">
|
||||||
{uniqueStrategyNames.map(strategyName => (
|
{strategies.map(strategy => (
|
||||||
<StyledListItem key={strategyName}>
|
<StyledListItem key={strategy.name}>
|
||||||
<FeatureStrategyIcon strategyName={strategyName} />
|
<FeatureStrategyIcon strategyName={strategy.name} />
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
))}
|
))}
|
||||||
</StyledList>
|
</StyledList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uniqueValues = <T,>(values: T[]): T[] => {
|
|
||||||
return [...new Set(values)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledList = styled('ul')(() => ({
|
const StyledList = styled('ul')(() => ({
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -43,29 +43,25 @@ export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => {
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case 'rollout':
|
case 'rollout':
|
||||||
case 'Rollout':
|
case 'Rollout':
|
||||||
|
const percentage = parseParameterNumber(parameters[key]);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={styles.summary}
|
className={styles.summary}
|
||||||
key={key}
|
key={key}
|
||||||
sx={{ display: 'flex', alignItems: 'center' }}
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<PercentageCircle
|
<Box sx={{ mr: '1rem' }}>
|
||||||
hideNumber
|
<PercentageCircle
|
||||||
percentage={parseParameterNumber(
|
percentage={percentage}
|
||||||
parameters[key]
|
size="2rem"
|
||||||
)}
|
/>
|
||||||
styles={{
|
</Box>
|
||||||
width: '2rem',
|
|
||||||
height: '2rem',
|
|
||||||
marginRight: '1rem',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Chip
|
<Chip
|
||||||
color="success"
|
color="success"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
label={`${parameters[key]}%`}
|
label={`${percentage}%`}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
of your base{' '}
|
of your base{' '}
|
||||||
{constraints.length > 0
|
{constraints.length > 0
|
||||||
|
@ -23,6 +23,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
maxWidth: '270px',
|
maxWidth: '270px',
|
||||||
marginTop: '0.25rem',
|
marginTop: '0.25rem',
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
textAlign: 'right',
|
||||||
[theme.breakpoints.down(700)]: {
|
[theme.breakpoints.down(700)]: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
@ -33,7 +34,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
fontSize: theme.fontSizes.subHeader,
|
fontSize: theme.fontSizes.subHeader,
|
||||||
},
|
},
|
||||||
percentageCircle: {
|
percentageCircle: {
|
||||||
transform: 'scale(0.85)',
|
margin: '0 1rem',
|
||||||
[theme.breakpoints.down(500)]: {
|
[theme.breakpoints.down(500)]: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
|
@ -73,11 +73,9 @@ const FeatureOverviewEnvironmentMetrics = ({
|
|||||||
hour
|
hour
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PercentageCircle
|
<div className={styles.percentageCircle} data-loading>
|
||||||
className={styles.percentageCircle}
|
<PercentageCircle percentage={percentage} size="3rem" />
|
||||||
percentage={percentage}
|
</div>
|
||||||
data-loading
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -97,7 +97,13 @@ const StrategyInputList = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!disabled}
|
condition={!disabled}
|
||||||
show={
|
show={
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
name={`input_field`}
|
name={`input_field`}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -116,6 +122,7 @@ const StrategyInputList = ({
|
|||||||
<Button
|
<Button
|
||||||
onClick={setValue}
|
onClick={setValue}
|
||||||
data-testid={ADD_TO_STRATEGY_INPUT_LIST}
|
data-testid={ADD_TO_STRATEGY_INPUT_LIST}
|
||||||
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
>
|
>
|
||||||
|
@ -83,6 +83,7 @@ export const CreateStrategy = () => {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
strategyName={strategyName}
|
strategyName={strategyName}
|
||||||
setStrategyName={setStrategyName}
|
setStrategyName={setStrategyName}
|
||||||
|
validateStrategyName={validateStrategyName}
|
||||||
strategyDesc={strategyDesc}
|
strategyDesc={strategyDesc}
|
||||||
setStrategyDesc={setStrategyDesc}
|
setStrategyDesc={setStrategyDesc}
|
||||||
params={params}
|
params={params}
|
||||||
|
@ -12,6 +12,7 @@ interface IStrategyFormProps {
|
|||||||
strategyDesc: string;
|
strategyDesc: string;
|
||||||
params: IStrategyParameter[];
|
params: IStrategyParameter[];
|
||||||
setStrategyName: React.Dispatch<React.SetStateAction<string>>;
|
setStrategyName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
validateStrategyName?: () => void;
|
||||||
setStrategyDesc: React.Dispatch<React.SetStateAction<string>>;
|
setStrategyDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setParams: React.Dispatch<React.SetStateAction<IStrategyParameter[]>>;
|
setParams: React.Dispatch<React.SetStateAction<IStrategyParameter[]>>;
|
||||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
@ -31,6 +32,7 @@ export const StrategyForm: React.FC<IStrategyFormProps> = ({
|
|||||||
params,
|
params,
|
||||||
setParams,
|
setParams,
|
||||||
setStrategyName,
|
setStrategyName,
|
||||||
|
validateStrategyName,
|
||||||
setStrategyDesc,
|
setStrategyDesc,
|
||||||
errors,
|
errors,
|
||||||
mode,
|
mode,
|
||||||
@ -65,7 +67,8 @@ export const StrategyForm: React.FC<IStrategyFormProps> = ({
|
|||||||
onChange={e => setStrategyName(trim(e.target.value))}
|
onChange={e => setStrategyName(trim(e.target.value))}
|
||||||
error={Boolean(errors.name)}
|
error={Boolean(errors.name)}
|
||||||
errorText={errors.name}
|
errorText={errors.name}
|
||||||
onFocus={() => clearErrors()}
|
onFocus={clearErrors}
|
||||||
|
onBlur={validateStrategyName}
|
||||||
/>
|
/>
|
||||||
<p className={styles.inputDescription}>
|
<p className={styles.inputDescription}>
|
||||||
What is your strategy description?
|
What is your strategy description?
|
||||||
|
@ -29,12 +29,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
inputDescription: {
|
inputDescription: {
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
},
|
},
|
||||||
typeDescription: {
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
color: theme.palette.grey[600],
|
|
||||||
top: '-13px',
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
|
@ -17,31 +17,22 @@ const paramTypesOptions = [
|
|||||||
{
|
{
|
||||||
key: 'string',
|
key: 'string',
|
||||||
label: 'string',
|
label: 'string',
|
||||||
description: 'A string is a collection of characters',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'percentage',
|
key: 'percentage',
|
||||||
label: 'percentage',
|
label: 'percentage',
|
||||||
description:
|
|
||||||
'Percentage is used when you want to make your feature visible to a process part of your customers',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'list',
|
key: 'list',
|
||||||
label: 'list',
|
label: 'list',
|
||||||
description:
|
|
||||||
'A list is used when you want to define several parameters that must be met before your feature becomes visible to your customers',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'number',
|
key: 'number',
|
||||||
label: 'number',
|
label: 'number',
|
||||||
description:
|
|
||||||
'Number is used when you have one or more digits that must be met for your feature to be visible to your customers',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'boolean',
|
key: 'boolean',
|
||||||
label: 'boolean',
|
label: 'boolean',
|
||||||
description:
|
|
||||||
'A boolean value represents a truth value, which is either true or false',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -68,11 +59,6 @@ export const StrategyParameter = ({
|
|||||||
set({ type });
|
set({ type });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderParamTypeDescription = () => {
|
|
||||||
return paramTypesOptions.find(param => param.key === input.type)
|
|
||||||
?.description;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.paramsContainer}>
|
<div className={styles.paramsContainer}>
|
||||||
<Divider className={styles.divider} />
|
<Divider className={styles.divider} />
|
||||||
@ -80,7 +66,16 @@ export const StrategyParameter = ({
|
|||||||
condition={index === 0}
|
condition={index === 0}
|
||||||
show={
|
show={
|
||||||
<p className={styles.input}>
|
<p className={styles.input}>
|
||||||
The parameters define what the strategy will look like.
|
Parameters let you provide arguments to your strategy
|
||||||
|
that it can access for evaluation. Read more in the{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.getunleash.io/advanced/custom_activation_strategy#parameter-types"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
parameter types documentation
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -114,9 +109,6 @@ export const StrategyParameter = ({
|
|||||||
id={`prop-type-${index}-select`}
|
id={`prop-type-${index}-select`}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
/>
|
/>
|
||||||
<p className={styles.typeDescription}>
|
|
||||||
{renderParamTypeDescription()}
|
|
||||||
</p>
|
|
||||||
<Input
|
<Input
|
||||||
rows={2}
|
rows={2}
|
||||||
multiline
|
multiline
|
||||||
|
Loading…
Reference in New Issue
Block a user