From f59ba567fb65e3ffd48cd0d18d1f07a7dc1a7459 Mon Sep 17 00:00:00 2001 From: olav Date: Thu, 7 Apr 2022 14:47:24 +0200 Subject: [PATCH] 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 --- .../AutocompleteBox/AutocompleteBox.styles.ts | 5 ++ .../AutocompleteBox/AutocompleteBox.tsx | 12 ++- .../FeatureStrategyEmpty.styles.ts | 2 - .../FeatureStrategySegment.tsx | 5 ++ .../FeatureStrategySegmentChip.tsx | 52 ++++++----- .../segments/CreateSegment/CreateSegment.tsx | 12 +-- .../segments/EditSegment/EditSegment.tsx | 14 +-- .../SegmentDocs/SegmentDocs.styles.ts | 18 ++++ .../segments/SegmentDocs/SegmentDocs.tsx | 89 +++++++++++++++++++ .../SegmentFormStepTwo.styles.ts | 10 ++- .../SegmentFormStepTwo/SegmentFormStepTwo.tsx | 26 ++++-- .../SegmentList/SegmentList.styles.ts | 3 + .../segments/SegmentList/SegmentList.tsx | 4 + .../segments/hooks/useSegmentValuesCount.ts | 10 +++ frontend/src/utils/segmentLimits.ts | 2 + 15 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 frontend/src/component/segments/SegmentDocs/SegmentDocs.styles.ts create mode 100644 frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx create mode 100644 frontend/src/component/segments/hooks/useSegmentValuesCount.ts create mode 100644 frontend/src/utils/segmentLimits.ts diff --git a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.styles.ts b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.styles.ts index 48a20e5f32..235b26f8eb 100644 --- a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.styles.ts +++ b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.styles.ts @@ -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', diff --git a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx index 370dbbdf2f..9fb5d8be03 100644 --- a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx +++ b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx @@ -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 (
-
+
onChange(value || [])} renderInput={renderInput} getOptionLabel={value => value.label} + disabled={disabled} multiple />
diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts index 7ceba774c4..91348b66e9 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty.styles.ts @@ -10,8 +10,6 @@ export const useStyles = makeStyles(theme => ({ color: 'inherit', }, envName: { - position: 'relative', - top: '6px', fontWeight: 'bold', }, })); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx index 8dce966001..7f35a70305 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment.tsx @@ -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 ( <>

Segmentation

+ {atSegmentsLimit && }

Add a predefined segment to constrain this feature toggle:

- } - elseShow={ - - } + show={} + elseShow={} /> ); + const previewIconTooltip = + segment === preview + ? 'Hide segment constraints' + : 'Preview segment constraints'; + return ( {segment.name} - - + + + + + + ); }; diff --git a/frontend/src/component/segments/CreateSegment/CreateSegment.tsx b/frontend/src/component/segments/CreateSegment/CreateSegment.tsx index 80e4755366..521c704ec6 100644 --- a/frontend/src/component/segments/CreateSegment/CreateSegment.tsx +++ b/frontend/src/component/segments/CreateSegment/CreateSegment.tsx @@ -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 = () => { @@ -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'; diff --git a/frontend/src/component/segments/EditSegment/EditSegment.tsx b/frontend/src/component/segments/EditSegment/EditSegment.tsx index 62c961bffd..7d0b1dd615 100644 --- a/frontend/src/component/segments/EditSegment/EditSegment.tsx +++ b/frontend/src/component/segments/EditSegment/EditSegment.tsx @@ -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 = () => { > diff --git a/frontend/src/component/segments/SegmentDocs/SegmentDocs.styles.ts b/frontend/src/component/segments/SegmentDocs/SegmentDocs.styles.ts new file mode 100644 index 0000000000..931eee86b6 --- /dev/null +++ b/frontend/src/component/segments/SegmentDocs/SegmentDocs.styles.ts @@ -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', + }, + }, + }, +})); diff --git a/frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx b/frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx new file mode 100644 index 0000000000..de281e000d --- /dev/null +++ b/frontend/src/component/segments/SegmentDocs/SegmentDocs.tsx @@ -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 ( + +

+ Segments is an experimental feature available to select users. +

+

+ This feature is currently in development. Future versions may + require to update your SDKs. +

+

+ +

+
+ ); +}; + +export const SegmentDocsValuesWarning = () => { + return ( + + Segments is an experimental feature available to select users. + Currently, segments are limited to at most {SEGMENT_VALUES_LIMIT}{' '} + values. + + ); +}; + +export const SegmentDocsValuesError = (props: { values: number }) => { + return ( + + Segments are limited to at most {SEGMENT_VALUES_LIMIT} values. This + segment currently has {props.values}{' '} + {props.values === 1 ? 'value' : 'values'}. + + ); +}; + +export const SegmentDocsStrategyWarning = () => { + return ( + + Strategies are limited to {STRATEGY_SEGMENTS_LIMIT} segments.{' '} + + + ); +}; + +const SegmentDocsLink = () => { + return ( + <> + + Read more about segments in the documentation + + . + + ); +}; + +const SegmentLimitsLink = () => { + return ( + <> + Please{' '} + + get in touch + {' '} + if you would like this limit increased. + + ); +}; + +export const segmentsDocsLink = 'https://docs.getunleash.io/reference/segments'; diff --git a/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.styles.ts b/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.styles.ts index dfbeaf045b..5bd19d7e9a 100644 --- a/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.styles.ts +++ b/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.styles.ts @@ -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, diff --git a/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.tsx b/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.tsx index 54c9d86cb9..90411120d8 100644 --- a/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.tsx +++ b/frontend/src/component/segments/SegmentFormStepTwo/SegmentFormStepTwo.tsx @@ -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 = ({ 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 = ({ }; return ( -
-
+ <> +
+
+ +

Select the context fields you want to include in the @@ -87,8 +98,14 @@ export const SegmentFormStepTwo: React.FC = ({ > Add context field + {overSegmentValuesLimit && ( +

+ +
+ )}
- = ({ />
-
-
+ ); }; diff --git a/frontend/src/component/segments/SegmentList/SegmentList.styles.ts b/frontend/src/component/segments/SegmentList/SegmentList.styles.ts index 1788c1678d..f2d69b45bb 100644 --- a/frontend/src/component/segments/SegmentList/SegmentList.styles.ts +++ b/frontend/src/component/segments/SegmentList/SegmentList.styles.ts @@ -1,6 +1,9 @@ import { makeStyles } from '@material-ui/core/styles'; export const useStyles = makeStyles(theme => ({ + docs: { + marginBottom: '2rem', + }, empty: { display: 'flex', flexDirection: 'column', diff --git a/frontend/src/component/segments/SegmentList/SegmentList.tsx b/frontend/src/component/segments/SegmentList/SegmentList.tsx index 291703a195..2f3e763c91 100644 --- a/frontend/src/component/segments/SegmentList/SegmentList.tsx +++ b/frontend/src/component/segments/SegmentList/SegmentList.tsx @@ -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 = () => { /> } > +
+ +
diff --git a/frontend/src/component/segments/hooks/useSegmentValuesCount.ts b/frontend/src/component/segments/hooks/useSegmentValuesCount.ts new file mode 100644 index 0000000000..27cfb5ddd4 --- /dev/null +++ b/frontend/src/component/segments/hooks/useSegmentValuesCount.ts @@ -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]); +}; diff --git a/frontend/src/utils/segmentLimits.ts b/frontend/src/utils/segmentLimits.ts new file mode 100644 index 0000000000..9837119c2a --- /dev/null +++ b/frontend/src/utils/segmentLimits.ts @@ -0,0 +1,2 @@ +export const SEGMENT_VALUES_LIMIT = 100; +export const STRATEGY_SEGMENTS_LIMIT = 5;