1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +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,
color: '#fff',
},
iconDisabled: {
background: theme.palette.primary.light,
},
autocomplete: {
flex: 1,
},
inputRoot: {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
'& fieldset': {
borderColor: theme.palette,
borderLeftColor: 'transparent',

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useSegmentForm } from '../hooks/useSegmentForm';
import { SegmentForm } from '../SegmentForm/SegmentForm';
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 = () => {
const { uiConfig } = useUiConfig();
@ -34,6 +37,8 @@ export const CreateSegment = () => {
} = useSegmentForm();
const hasValidConstraints = useConstraintsValidation(constraints);
const segmentValuesCount = useSegmentValuesCount(constraints);
const atSegmentValuesLimit = segmentValuesCount >= SEGMENT_VALUES_LIMIT;
const formatApiCode = () => {
return `curl --location --request POST '${
@ -71,7 +76,7 @@ export const CreateSegment = () => {
loading={loading}
title="Create segment"
description={segmentsFormDescription}
documentationLink={segmentsFormDocsLink}
documentationLink={segmentsDocsLink}
documentationLinkLabel="More about segments"
formatApiCode={formatApiCode}
>
@ -90,7 +95,7 @@ export const CreateSegment = () => {
<CreateButton
name="segment"
permission={CREATE_SEGMENT}
disabled={!hasValidConstraints}
disabled={!hasValidConstraints || atSegmentValuesLimit}
/>
</SegmentForm>
</FormTemplate>
@ -102,6 +107,3 @@ export const segmentsFormDescription = `
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.
`;
// 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 { useSegmentForm } from '../hooks/useSegmentForm';
import { SegmentForm } from '../SegmentForm/SegmentForm';
import {
segmentsFormDocsLink,
segmentsFormDescription,
} from 'component/segments/CreateSegment/CreateSegment';
import { segmentsFormDescription } from 'component/segments/CreateSegment/CreateSegment';
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 = () => {
const segmentId = useRequiredPathParam('segmentId');
@ -44,6 +44,8 @@ export const EditSegment = () => {
);
const hasValidConstraints = useConstraintsValidation(constraints);
const segmentValuesCount = useSegmentValuesCount(constraints);
const atSegmentValuesLimit = segmentValuesCount >= SEGMENT_VALUES_LIMIT;
const formatApiCode = () => {
return `curl --location --request PUT '${
@ -77,7 +79,7 @@ export const EditSegment = () => {
loading={loading}
title="Edit segment"
description={segmentsFormDescription}
documentationLink={segmentsFormDocsLink}
documentationLink={segmentsDocsLink}
documentationLinkLabel="More about segments"
formatApiCode={formatApiCode}
>
@ -95,7 +97,7 @@ export const EditSegment = () => {
>
<UpdateButton
permission={UPDATE_SEGMENT}
disabled={!hasValidConstraints}
disabled={!hasValidConstraints || atSegmentValuesLimit}
/>
</SegmentForm>
</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';
export const useStyles = makeStyles(theme => ({
container: {},
warning: {
marginBottom: '1.5rem',
},
error: {
marginTop: '1.5rem',
},
form: {
display: 'flex',
flexDirection: 'column',
@ -21,6 +26,9 @@ export const useStyles = makeStyles(theme => ({
borderTop: `1px solid ${theme.palette.grey[300]}`,
paddingTop: 15,
},
errorsContainer: {
marginTop: '1rem',
},
cancelButton: {
marginLeft: '1.5rem',
color: theme.palette.primary.light,

View File

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

View File

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

View File

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