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:
parent
42a81e6647
commit
f59ba567fb
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -10,8 +10,6 @@ export const useStyles = makeStyles(theme => ({
|
||||
color: 'inherit',
|
||||
},
|
||||
envName: {
|
||||
position: 'relative',
|
||||
top: '6px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}));
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
89
frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx
Normal file
89
frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx
Normal 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';
|
@ -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,
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
docs: {
|
||||
marginBottom: '2rem',
|
||||
},
|
||||
empty: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
@ -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}>
|
||||
|
@ -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]);
|
||||
};
|
2
frontend/src/utils/segmentLimits.ts
Normal file
2
frontend/src/utils/segmentLimits.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const SEGMENT_VALUES_LIMIT = 100;
|
||||
export const STRATEGY_SEGMENTS_LIMIT = 5;
|
Loading…
Reference in New Issue
Block a user