From 53b32db27874f30e48ccbcd380acd09f02508728 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 15 Dec 2023 10:20:34 +0100 Subject: [PATCH] Feat/new strategy configuration targeting tab (#5643) This PR sets up the new targeting tab for strategy configuration: Skjermbilde 2023-12-14 kl 11 24 11 --- .../AutocompleteBox/AutocompleteBox.tsx | 74 ++++++++++++++- .../ConstraintAccordionList.tsx | 95 ++++++++++++++++++- .../NewFeatureStrategyForm.tsx | 50 +++++++++- .../FeatureStrategySegment.tsx | 57 ++++++++++- .../FlexibleStrategy/FlexibleStrategy.tsx | 2 +- .../RolloutSlider/RolloutSlider.tsx | 30 +++--- 6 files changed, 286 insertions(+), 22 deletions(-) diff --git a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx index 12007e48b0..b42f43269b 100644 --- a/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx +++ b/frontend/src/component/common/AutocompleteBox/AutocompleteBox.tsx @@ -1,8 +1,10 @@ import { useStyles } from 'component/common/AutocompleteBox/AutocompleteBox.styles'; -import { Search, ArrowDropDown } from '@mui/icons-material'; -import { Autocomplete, styled } from '@mui/material'; +import { Search, ArrowDropDown, Add } from '@mui/icons-material'; +import { Autocomplete, styled, InputAdornment, useTheme } from '@mui/material'; import { AutocompleteRenderInputParams } from '@mui/material/Autocomplete'; import { TextField } from '@mui/material'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { useState } from 'react'; interface IAutocompleteBoxProps { label: string; @@ -54,12 +56,80 @@ export const AutocompleteBox = ({ onChange, disabled, }: IAutocompleteBoxProps) => { + const [placeHolder, setPlaceholder] = useState('Add Segments'); const { classes: styles } = useStyles(); + const theme = useTheme(); + + const newStrategyConfiguration = useUiFlag('newStrategyConfiguration'); const renderInput = (params: AutocompleteRenderInputParams) => { return ; }; + const renderCustomInput = (params: AutocompleteRenderInputParams) => { + const { InputProps } = params; + return ( + + + + ), + }} + variant='outlined' + sx={{ + width: '215px', + '& .MuiOutlinedInput-root': { + '& .MuiInputBase-input': { + color: theme.palette.primary.main, + opacity: 1, + '&::placeholder': { + color: theme.palette.primary.main, + fontWeight: 'bold', + opacity: 1, + }, + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.primary.main, + opacity: 0.5, + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderWidth: '1px', + }, + }, + }} + placeholder={placeHolder} + onFocus={() => setPlaceholder('')} + onBlur={() => setPlaceholder('Add Segments')} + /> + ); + }; + if (newStrategyConfiguration) { + return ( + + onChange(value || [])} + renderInput={renderCustomInput} + getOptionLabel={(value) => value.label} + disabled={disabled} + size='small' + multiple + /> + + ); + } + return ( diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx index 2d4bfadb58..e6332bab54 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList.tsx @@ -1,6 +1,6 @@ import React, { forwardRef, Fragment, useImperativeHandle } from 'react'; -import { Button, styled, Tooltip } from '@mui/material'; -import { HelpOutline } from '@mui/icons-material'; +import { Box, Button, styled, Tooltip, Typography } from '@mui/material'; +import { Add, HelpOutline } from '@mui/icons-material'; import { IConstraint } from 'interfaces/strategy'; import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion'; import produce from 'immer'; @@ -10,6 +10,8 @@ import { objectId } from 'utils/objectId'; import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; interface IConstraintAccordionListProps { constraints: IConstraint[]; @@ -64,6 +66,13 @@ const StyledAddCustomLabel = styled('div')(({ theme }) => ({ display: 'flex', })); +const StyledHelpIconBox = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), +})); + export const ConstraintAccordionList = forwardRef< IConstraintAccordionListRef | undefined, IConstraintAccordionListProps @@ -78,6 +87,8 @@ export const ConstraintAccordionList = forwardRef< >(); const { context } = useUnleashContext(); + const newStrategyConfiguration = useUiFlag('newStrategyConfiguration'); + const addConstraint = setConstraints && ((contextName: string) => { @@ -135,6 +146,86 @@ export const ConstraintAccordionList = forwardRef< return null; } + if (newStrategyConfiguration) { + return ( + + + + Constraints + + + Constraints are advanced + targeting rules that you can + use to enable a feature + toggle for a subset of your + users. Read more about + constraints{' '} + + here + + + + } + /> + + {constraints.map((constraint, index) => ( + + 0} + show={ + + } + /> + + + ))} + + + } + /> + + ); + } + return ( >; } +const StyledDividerContent = styled(Box)(({ theme }) => ({ + padding: theme.spacing(0.75, 1), + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallerBody, + backgroundColor: theme.palette.background.elevation2, + borderRadius: theme.shape.borderRadius, + width: '45px', + position: 'absolute', + top: '-10px', + left: 'calc(50% - 45px)', + lineHeight: 1, +})); + const StyledForm = styled('form')(({ theme }) => ({ display: 'grid', gap: theme.spacing(2), @@ -74,6 +96,21 @@ const StyledButtons = styled('div')(({ theme }) => ({ paddingBottom: theme.spacing(10), })); +const StyledBox = styled(Box)(({ theme }) => ({ + display: 'flex', + position: 'relative', + marginTop: theme.spacing(3.5), +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + width: '100%', +})); + +const StyledTargetingHeader = styled('div')(({ theme }) => ({ + color: theme.palette.text.secondary, + marginTop: theme.spacing(1.5), +})); + export const NewFeatureStrategyForm = ({ projectId, feature, @@ -274,11 +311,22 @@ export const NewFeatureStrategyForm = ({ condition={tab === 1} show={ <> + + Segmentation and constraints allow you to set + filters on your strategies, so that they will only + be evaluated for users and applications that match + the specified preconditions. + + + + + AND + ({ fontSize: theme.fontSizes.smallBody, })); +const StyledHelpIconBox = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), +})); + export const FeatureStrategySegment = ({ segments: selectedSegments, setSegments: setSelectedSegments, @@ -28,6 +37,8 @@ export const FeatureStrategySegment = ({ const { segments: allSegments } = useSegments(); const { strategySegmentsLimit } = useSegmentLimits(); + const newStrategyConfiguration = useUiFlag('newStrategyConfiguration'); + const atStrategySegmentsLimit: boolean = Boolean( strategySegmentsLimit && selectedSegments.length >= strategySegmentsLimit, @@ -59,6 +70,49 @@ export const FeatureStrategySegment = ({ } }; + if (newStrategyConfiguration) { + return ( + <> + + Segments + + + Segments are reusable sets of constraints + that can be defined once and reused across + feature toggle configurations. You can + create a segment on the global or the + project level. Read more about segments{' '} + + here + + + + } + /> + + + {atStrategySegmentsLimit && } + + + + ); + } + return ( <> @@ -76,6 +130,7 @@ export const FeatureStrategySegment = ({ segments={selectedSegments} setSegments={setSelectedSegments} /> + ); diff --git a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx index 2802767a6c..4971c4192b 100644 --- a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx +++ b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx @@ -35,7 +35,7 @@ const StyledBox = styled(Box)(({ theme }) => ({ })); const StyledOuterBox = styled(Box)(({ theme }) => ({ - marginTop: '1rem', + marginTop: theme.spacing(1), display: 'flex', width: '100%', justifyContent: 'space-between', diff --git a/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx b/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx index 4df0425a56..a147fa8656 100644 --- a/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx +++ b/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx @@ -25,6 +25,15 @@ const StyledSlider = withStyles(Slider, (theme) => ({ }, })); +const StyledHeader = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(1), +})); + +const StyledSubheader = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(1), + marginTop: theme.spacing(1), +})); + const StyledBox = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', @@ -91,12 +100,9 @@ const RolloutSlider = ({ htmlTooltip tooltip={ - + Rollout percentage - + The rollout percentage determines the proportion of users exposed to a feature. It's based on the @@ -108,12 +114,9 @@ const RolloutSlider = ({ of the feature among users. - + Stickiness - + Stickiness refers to the value used for hashing to ensure a consistent user experience. It @@ -122,12 +125,9 @@ const RolloutSlider = ({ consistent across sessions. - + GroupId - + The groupId is used as a seed for the hash function, ensuring consistent feature exposure