1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

refactor: add segment limit warnings (#851)

* refactor: fix environment name text alignment

* refactor: use rounded corners for AutocompleteBox

* refactor: add tooltips to the strategy segment icons

* refactor: add segment limit warnings

* refactor: improve segments warning text
This commit is contained in:
olav 2022-04-07 14:47:24 +02:00 committed by GitHub
parent 42a81e6647
commit f59ba567fb
15 changed files with 220 additions and 44 deletions

View File

@ -18,12 +18,17 @@ export const useStyles = makeStyles(theme => ({
borderBottomLeftRadius: 50, borderBottomLeftRadius: 50,
color: '#fff', color: '#fff',
}, },
iconDisabled: {
background: theme.palette.primary.light,
},
autocomplete: { autocomplete: {
flex: 1, flex: 1,
}, },
inputRoot: { inputRoot: {
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
borderBottomLeftRadius: 0, borderBottomLeftRadius: 0,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
'& fieldset': { '& fieldset': {
borderColor: theme.palette, borderColor: theme.palette,
borderLeftColor: 'transparent', borderLeftColor: 'transparent',

View File

@ -2,12 +2,14 @@ import { useStyles } from 'component/common/AutocompleteBox/AutocompleteBox.styl
import { Search, ArrowDropDown } from '@material-ui/icons'; import { Search, ArrowDropDown } from '@material-ui/icons';
import { Autocomplete, AutocompleteRenderInputParams } from '@material-ui/lab'; import { Autocomplete, AutocompleteRenderInputParams } from '@material-ui/lab';
import { TextField } from '@material-ui/core'; import { TextField } from '@material-ui/core';
import classNames from 'classnames';
interface IAutocompleteBoxProps { interface IAutocompleteBoxProps {
label: string; label: string;
options: IAutocompleteBoxOption[]; options: IAutocompleteBoxOption[];
value?: IAutocompleteBoxOption[]; value?: IAutocompleteBoxOption[];
onChange: (value: IAutocompleteBoxOption[]) => void; onChange: (value: IAutocompleteBoxOption[]) => void;
disabled?: boolean;
} }
export interface IAutocompleteBoxOption { export interface IAutocompleteBoxOption {
@ -20,6 +22,7 @@ export const AutocompleteBox = ({
options, options,
value = [], value = [],
onChange, onChange,
disabled,
}: IAutocompleteBoxProps) => { }: IAutocompleteBoxProps) => {
const styles = useStyles(); const styles = useStyles();
@ -29,7 +32,13 @@ export const AutocompleteBox = ({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.icon} aria-hidden> <div
className={classNames(
styles.icon,
disabled && styles.iconDisabled
)}
aria-hidden
>
<Search /> <Search />
</div> </div>
<Autocomplete <Autocomplete
@ -41,6 +50,7 @@ export const AutocompleteBox = ({
onChange={(event, value) => onChange(value || [])} onChange={(event, value) => onChange(value || [])}
renderInput={renderInput} renderInput={renderInput}
getOptionLabel={value => value.label} getOptionLabel={value => value.label}
disabled={disabled}
multiple multiple
/> />
</div> </div>

View File

@ -10,8 +10,6 @@ export const useStyles = makeStyles(theme => ({
color: 'inherit', color: 'inherit',
}, },
envName: { envName: {
position: 'relative',
top: '6px',
fontWeight: 'bold', fontWeight: 'bold',
}, },
})); }));

View File

@ -7,6 +7,8 @@ import {
} from 'component/common/AutocompleteBox/AutocompleteBox'; } from 'component/common/AutocompleteBox/AutocompleteBox';
import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList'; import { FeatureStrategySegmentList } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentList';
import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.styles'; import { useStyles } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.styles';
import { SegmentDocsStrategyWarning } from 'component/segments/SegmentDocs/SegmentDocs';
import { STRATEGY_SEGMENTS_LIMIT } from 'utils/segmentLimits';
interface IFeatureStrategySegmentProps { interface IFeatureStrategySegmentProps {
segments: ISegment[]; segments: ISegment[];
@ -17,6 +19,7 @@ export const FeatureStrategySegment = ({
segments: selectedSegments, segments: selectedSegments,
setSegments: setSelectedSegments, setSegments: setSelectedSegments,
}: IFeatureStrategySegmentProps) => { }: IFeatureStrategySegmentProps) => {
const atSegmentsLimit = selectedSegments.length >= STRATEGY_SEGMENTS_LIMIT;
const { segments: allSegments } = useSegments(); const { segments: allSegments } = useSegments();
const styles = useStyles(); const styles = useStyles();
@ -45,11 +48,13 @@ export const FeatureStrategySegment = ({
return ( return (
<> <>
<h3 className={styles.title}>Segmentation</h3> <h3 className={styles.title}>Segmentation</h3>
{atSegmentsLimit && <SegmentDocsStrategyWarning />}
<p>Add a predefined segment to constrain this feature toggle:</p> <p>Add a predefined segment to constrain this feature toggle:</p>
<AutocompleteBox <AutocompleteBox
label="Select segments" label="Select segments"
options={autocompleteOptions} options={autocompleteOptions}
onChange={onChange} onChange={onChange}
disabled={atSegmentsLimit}
/> />
<FeatureStrategySegmentList <FeatureStrategySegmentList
segments={selectedSegments} segments={selectedSegments}

View File

@ -5,6 +5,7 @@ import { Clear, VisibilityOff, Visibility } from '@material-ui/icons';
import { useStyles } from './FeatureStrategySegmentChip.styles'; import { useStyles } from './FeatureStrategySegmentChip.styles';
import ConditionallyRender from 'component/common/ConditionallyRender'; import ConditionallyRender from 'component/common/ConditionallyRender';
import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList'; import { constraintAccordionListId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
import { Tooltip } from '@material-ui/core';
interface IFeatureStrategySegmentListProps { interface IFeatureStrategySegmentListProps {
segment: ISegment; segment: ISegment;
@ -39,21 +40,16 @@ export const FeatureStrategySegmentChip = ({
const togglePreviewIcon = ( const togglePreviewIcon = (
<ConditionallyRender <ConditionallyRender
condition={segment === preview} condition={segment === preview}
show={ show={<VisibilityOff titleAccess="Hide" className={styles.icon} />}
<VisibilityOff elseShow={<Visibility titleAccess="Show" className={styles.icon} />}
titleAccess="Hide preview"
className={styles.icon}
/>
}
elseShow={
<Visibility
titleAccess="Show preview"
className={styles.icon}
/>
}
/> />
); );
const previewIconTooltip =
segment === preview
? 'Hide segment constraints'
: 'Preview segment constraints';
return ( return (
<span className={styles.chip}> <span className={styles.chip}>
<Link <Link
@ -63,6 +59,7 @@ export const FeatureStrategySegmentChip = ({
> >
{segment.name} {segment.name}
</Link> </Link>
<Tooltip title={previewIconTooltip}>
<button <button
type="button" type="button"
onClick={onTogglePreview} onClick={onTogglePreview}
@ -72,9 +69,16 @@ export const FeatureStrategySegmentChip = ({
> >
{togglePreviewIcon} {togglePreviewIcon}
</button> </button>
<button type="button" onClick={onRemove} className={styles.button}> </Tooltip>
<Tooltip title="Remove segment">
<button
type="button"
onClick={onRemove}
className={styles.button}
>
<Clear titleAccess="Remove" className={styles.icon} /> <Clear titleAccess="Remove" className={styles.icon} />
</button> </button>
</Tooltip>
</span> </span>
); );
}; };

View File

@ -12,6 +12,9 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useSegmentForm } from '../hooks/useSegmentForm'; import { useSegmentForm } from '../hooks/useSegmentForm';
import { SegmentForm } from '../SegmentForm/SegmentForm'; import { SegmentForm } from '../SegmentForm/SegmentForm';
import { feedbackCESContext } from 'component/feedback/FeedbackCESContext/FeedbackCESContext'; import { feedbackCESContext } from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
import { segmentsDocsLink } from 'component/segments/SegmentDocs/SegmentDocs';
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import { SEGMENT_VALUES_LIMIT } from 'utils/segmentLimits';
export const CreateSegment = () => { export const CreateSegment = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -34,6 +37,8 @@ export const CreateSegment = () => {
} = useSegmentForm(); } = useSegmentForm();
const hasValidConstraints = useConstraintsValidation(constraints); const hasValidConstraints = useConstraintsValidation(constraints);
const segmentValuesCount = useSegmentValuesCount(constraints);
const atSegmentValuesLimit = segmentValuesCount >= SEGMENT_VALUES_LIMIT;
const formatApiCode = () => { const formatApiCode = () => {
return `curl --location --request POST '${ return `curl --location --request POST '${
@ -71,7 +76,7 @@ export const CreateSegment = () => {
loading={loading} loading={loading}
title="Create segment" title="Create segment"
description={segmentsFormDescription} description={segmentsFormDescription}
documentationLink={segmentsFormDocsLink} documentationLink={segmentsDocsLink}
documentationLinkLabel="More about segments" documentationLinkLabel="More about segments"
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
@ -90,7 +95,7 @@ export const CreateSegment = () => {
<CreateButton <CreateButton
name="segment" name="segment"
permission={CREATE_SEGMENT} permission={CREATE_SEGMENT}
disabled={!hasValidConstraints} disabled={!hasValidConstraints || atSegmentValuesLimit}
/> />
</SegmentForm> </SegmentForm>
</FormTemplate> </FormTemplate>
@ -102,6 +107,3 @@ export const segmentsFormDescription = `
A segment is a reusable collection of constraints. A segment is a reusable collection of constraints.
You can create and apply a segment when configuring activation strategies for a feature toggle or at any time from the segments page in the navigation menu. You can create and apply a segment when configuring activation strategies for a feature toggle or at any time from the segments page in the navigation menu.
`; `;
// TODO(olav): Update link when the segments docs are ready.
export const segmentsFormDocsLink = 'https://docs.getunleash.io';

View File

@ -12,11 +12,11 @@ import { useHistory } from 'react-router-dom';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useSegmentForm } from '../hooks/useSegmentForm'; import { useSegmentForm } from '../hooks/useSegmentForm';
import { SegmentForm } from '../SegmentForm/SegmentForm'; import { SegmentForm } from '../SegmentForm/SegmentForm';
import { import { segmentsFormDescription } from 'component/segments/CreateSegment/CreateSegment';
segmentsFormDocsLink,
segmentsFormDescription,
} from 'component/segments/CreateSegment/CreateSegment';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton'; import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
import { segmentsDocsLink } from 'component/segments/SegmentDocs/SegmentDocs';
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import { SEGMENT_VALUES_LIMIT } from 'utils/segmentLimits';
export const EditSegment = () => { export const EditSegment = () => {
const segmentId = useRequiredPathParam('segmentId'); const segmentId = useRequiredPathParam('segmentId');
@ -44,6 +44,8 @@ export const EditSegment = () => {
); );
const hasValidConstraints = useConstraintsValidation(constraints); const hasValidConstraints = useConstraintsValidation(constraints);
const segmentValuesCount = useSegmentValuesCount(constraints);
const atSegmentValuesLimit = segmentValuesCount >= SEGMENT_VALUES_LIMIT;
const formatApiCode = () => { const formatApiCode = () => {
return `curl --location --request PUT '${ return `curl --location --request PUT '${
@ -77,7 +79,7 @@ export const EditSegment = () => {
loading={loading} loading={loading}
title="Edit segment" title="Edit segment"
description={segmentsFormDescription} description={segmentsFormDescription}
documentationLink={segmentsFormDocsLink} documentationLink={segmentsDocsLink}
documentationLinkLabel="More about segments" documentationLinkLabel="More about segments"
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
@ -95,7 +97,7 @@ export const EditSegment = () => {
> >
<UpdateButton <UpdateButton
permission={UPDATE_SEGMENT} permission={UPDATE_SEGMENT}
disabled={!hasValidConstraints} disabled={!hasValidConstraints || atSegmentValuesLimit}
/> />
</SegmentForm> </SegmentForm>
</FormTemplate> </FormTemplate>

View File

@ -0,0 +1,18 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
paragraph: {
[theme.breakpoints.down('md')]: {
display: 'inline',
'&:after': {
content: '" "',
},
},
[theme.breakpoints.up('md')]: {
display: 'block',
'& + &': {
marginTop: '0.25rem',
},
},
},
}));

View File

@ -0,0 +1,89 @@
import { Alert } from '@material-ui/lab';
import { useStyles } from 'component/segments/SegmentDocs/SegmentDocs.styles';
import {
STRATEGY_SEGMENTS_LIMIT,
SEGMENT_VALUES_LIMIT,
} from 'utils/segmentLimits';
export const SegmentDocsWarning = () => {
const styles = useStyles();
return (
<Alert severity="warning">
<p className={styles.paragraph}>
Segments is an experimental feature available to select users.
</p>
<p className={styles.paragraph}>
This feature is currently in development. Future versions may
require to update your SDKs.
</p>
<p className={styles.paragraph}>
<SegmentDocsLink />
</p>
</Alert>
);
};
export const SegmentDocsValuesWarning = () => {
return (
<Alert severity="warning">
Segments is an experimental feature available to select users.
Currently, segments are limited to at most {SEGMENT_VALUES_LIMIT}{' '}
values. <SegmentLimitsLink />
</Alert>
);
};
export const SegmentDocsValuesError = (props: { values: number }) => {
return (
<Alert severity="error">
Segments are limited to at most {SEGMENT_VALUES_LIMIT} values. This
segment currently has {props.values}{' '}
{props.values === 1 ? 'value' : 'values'}.
</Alert>
);
};
export const SegmentDocsStrategyWarning = () => {
return (
<Alert severity="warning">
Strategies are limited to {STRATEGY_SEGMENTS_LIMIT} segments.{' '}
<SegmentLimitsLink />
</Alert>
);
};
const SegmentDocsLink = () => {
return (
<>
<a
href={segmentsDocsLink}
target="_blank"
rel="noreferrer"
style={{ color: 'inherit' }}
>
Read more about segments in the documentation
</a>
.
</>
);
};
const SegmentLimitsLink = () => {
return (
<>
Please{' '}
<a
href="https://slack.unleash.run"
target="_blank"
rel="noreferrer"
style={{ color: 'inherit' }}
>
get in touch
</a>{' '}
if you would like this limit increased.
</>
);
};
export const segmentsDocsLink = 'https://docs.getunleash.io/reference/segments';

View File

@ -1,7 +1,12 @@
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
container: {}, warning: {
marginBottom: '1.5rem',
},
error: {
marginTop: '1.5rem',
},
form: { form: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -21,6 +26,9 @@ export const useStyles = makeStyles(theme => ({
borderTop: `1px solid ${theme.palette.grey[300]}`, borderTop: `1px solid ${theme.palette.grey[300]}`,
paddingTop: 15, paddingTop: 15,
}, },
errorsContainer: {
marginTop: '1rem',
},
cancelButton: { cancelButton: {
marginLeft: '1.5rem', marginLeft: '1.5rem',
color: theme.palette.primary.light, color: theme.palette.primary.light,

View File

@ -19,6 +19,12 @@ import {
AutocompleteBox, AutocompleteBox,
IAutocompleteBoxOption, IAutocompleteBoxOption,
} from 'component/common/AutocompleteBox/AutocompleteBox'; } from 'component/common/AutocompleteBox/AutocompleteBox';
import {
SegmentDocsValuesWarning,
SegmentDocsValuesError,
} from 'component/segments/SegmentDocs/SegmentDocs';
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
import { SEGMENT_VALUES_LIMIT } from 'utils/segmentLimits';
interface ISegmentFormPartTwoProps { interface ISegmentFormPartTwoProps {
constraints: IConstraint[]; constraints: IConstraint[];
@ -37,6 +43,8 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
const styles = useStyles(); const styles = useStyles();
const { context = [] } = useUnleashContext(); const { context = [] } = useUnleashContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const segmentValuesCount = useSegmentValuesCount(constraints);
const overSegmentValuesLimit = segmentValuesCount > SEGMENT_VALUES_LIMIT;
const autocompleteOptions = context.map(c => ({ const autocompleteOptions = context.map(c => ({
value: c.name, value: c.name,
@ -48,8 +56,11 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
}; };
return ( return (
<>
<div className={styles.form}> <div className={styles.form}>
<div className={styles.container}> <div className={styles.warning}>
<SegmentDocsValuesWarning />
</div>
<div> <div>
<p className={styles.inputDescription}> <p className={styles.inputDescription}>
Select the context fields you want to include in the Select the context fields you want to include in the
@ -87,8 +98,14 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
> >
Add context field Add context field
</PermissionButton> </PermissionButton>
{overSegmentValuesLimit && (
<div className={styles.error}>
<SegmentDocsValuesError
values={segmentValuesCount}
/>
</div>
)}
</div> </div>
<ConditionallyRender <ConditionallyRender
condition={constraints.length === 0} condition={constraints.length === 0}
show={ show={
@ -109,7 +126,6 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
/> />
</div> </div>
</div> </div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<Button <Button
type="button" type="button"
@ -129,6 +145,6 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
Cancel Cancel
</Button> </Button>
</div> </div>
</div> </>
); );
}; };

View File

@ -1,6 +1,9 @@
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({ export const useStyles = makeStyles(theme => ({
docs: {
marginBottom: '2rem',
},
empty: { empty: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@ -27,6 +27,7 @@ import HeaderTitle from 'component/common/HeaderTitle';
import PageContent from 'component/common/PageContent'; import PageContent from 'component/common/PageContent';
import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { SegmentDelete } from '../SegmentDelete/SegmentDelete'; import { SegmentDelete } from '../SegmentDelete/SegmentDelete';
import { SegmentDocsWarning } from 'component/segments/SegmentDocs/SegmentDocs';
export const SegmentsList = () => { export const SegmentsList = () => {
const history = useHistory(); const history = useHistory();
@ -107,6 +108,9 @@ export const SegmentsList = () => {
/> />
} }
> >
<div className={styles.docs}>
<SegmentDocsWarning />
</div>
<Table> <Table>
<TableHead> <TableHead>
<TableRow className={styles.tableRow}> <TableRow className={styles.tableRow}>

View File

@ -0,0 +1,10 @@
import { IConstraint } from 'interfaces/strategy';
import { useMemo } from 'react';
export const useSegmentValuesCount = (constraints: IConstraint[]): number => {
return useMemo(() => {
return constraints
.map(constraint => constraint.values)
.reduce((acc, values) => acc + (values?.length ?? 0), 0);
}, [constraints]);
};

View File

@ -0,0 +1,2 @@
export const SEGMENT_VALUES_LIMIT = 100;
export const STRATEGY_SEGMENTS_LIMIT = 5;