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

Feat/new strategy variants tab (#5649)

This PR sets up the variants tab for the new strategy configuration.
Also some minor adjustments to the new form:

* Change where padding is controlled to allow us to have more granular
control over how the buttons width and border should look and have the
tabs have full-width borders across the form
* Move the buttons to be absolutely positioned at the bottom of the page
* Move where we display banners to avoid clutter

<img width="1284" alt="Skjermbilde 2023-12-14 kl 21 17 53"
src="https://github.com/Unleash/unleash/assets/16081982/45e6a364-e4aa-47ac-b420-f3be9b39a15e">
This commit is contained in:
Fredrik Strand Oseberg 2023-12-15 12:26:13 +01:00 committed by GitHub
parent 848415c5ca
commit cbd6aa1324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 310 additions and 173 deletions

View File

@ -1,15 +1,6 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Alert,
Button,
styled,
Tabs,
Tab,
Typography,
Divider,
Box,
} from '@mui/material';
import { Alert, Button, styled, Tabs, Tab, Box, Divider } from '@mui/material';
import {
IFeatureStrategy,
IFeatureStrategyParameters,
@ -42,6 +33,7 @@ import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitl
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { formatStrategyName } from 'utils/strategyNames';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
@ -77,8 +69,15 @@ const StyledDividerContent = styled(Box)(({ theme }) => ({
}));
const StyledForm = styled('form')(({ theme }) => ({
display: 'grid',
position: 'relative',
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(6),
paddingBottom: theme.spacing(12),
paddingTop: theme.spacing(4),
overflow: 'auto',
height: '100%',
}));
const StyledHr = styled('hr')(({ theme }) => ({
@ -89,11 +88,31 @@ const StyledHr = styled('hr')(({ theme }) => ({
background: theme.palette.background.elevation2,
}));
const StyledButtons = styled('div')(({ theme }) => ({
const StyledTitle = styled('h1')(({ theme }) => ({
fontWeight: 'normal',
display: 'flex',
alignItems: 'center',
}));
const StyledButtons = styled('div')(({ theme }) => ({
bottom: 0,
right: 0,
left: 0,
position: 'absolute',
display: 'flex',
padding: theme.spacing(3),
paddingRight: theme.spacing(6),
paddingLeft: theme.spacing(6),
backgroundColor: theme.palette.common.white,
justifyContent: 'end',
gap: theme.spacing(2),
paddingBottom: theme.spacing(10),
borderTop: `1px solid ${theme.palette.divider}`,
}));
const StyledTabs = styled(Tabs)(({ theme }) => ({
borderTop: `1px solid ${theme.palette.divider}`,
borderBottom: `1px solid ${theme.palette.divider}`,
paddingLeft: theme.spacing(6),
paddingRight: theme.spacing(6),
}));
const StyledBox = styled(Box)(({ theme }) => ({
@ -111,6 +130,15 @@ const StyledTargetingHeader = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1.5),
}));
const StyledHeaderBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
paddingLeft: theme.spacing(6),
paddingRight: theme.spacing(6),
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
}));
export const NewFeatureStrategyForm = ({
projectId,
feature,
@ -225,173 +253,186 @@ export const NewFeatureStrategyForm = ({
};
return (
<StyledForm onSubmit={onSubmitWithValidation}>
<Tabs value={tab} onChange={handleChange}>
<>
<StyledHeaderBox>
<StyledTitle>
{formatStrategyName(strategy.name || '')}
</StyledTitle>
</StyledHeaderBox>
<StyledTabs value={tab} onChange={handleChange}>
<Tab label='General' />
<Tab label='Targeting' />
<Tab label='Variants' />
</Tabs>
<ConditionallyRender
condition={tab === 0}
show={
<>
<ConditionallyRender
condition={hasChangeRequestInReviewForEnvironment}
show={alert}
elseShow={
</StyledTabs>
<StyledForm onSubmit={onSubmitWithValidation}>
<ConditionallyRender
condition={tab === 0}
show={
<>
<FeatureStrategyTitle
title={strategy.title || ''}
setTitle={(title) => {
setStrategy((prev) => ({
...prev,
title,
}));
}}
/>
<FeatureStrategyEnabledDisabled
enabled={!strategy?.disabled}
onToggleEnabled={() =>
setStrategy((strategyState) => ({
...strategyState,
disabled: !strategyState.disabled,
}))
}
/>
<FeatureStrategyType
strategy={strategy}
strategyDefinition={strategyDefinition}
setStrategy={setStrategy}
validateParameter={validateParameter}
errors={errors}
hasAccess={access}
/>
<ConditionallyRender
condition={
hasChangeRequestInReviewForEnvironment
}
show={alert}
elseShow={
<ConditionallyRender
condition={Boolean(isChangeRequest)}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
}
/>
}
/>
<FeatureStrategyEnabled
projectId={feature.project}
featureId={feature.name}
environmentId={environmentId}
>
<ConditionallyRender
condition={Boolean(isChangeRequest)}
show={
<FeatureStrategyChangeRequestAlert
environment={environmentId}
/>
<Alert severity='success'>
This feature toggle is currently
enabled in the{' '}
<strong>{environmentId}</strong>{' '}
environment. Any changes made here
will be available to users as soon
as these changes are approved and
applied.
</Alert>
}
elseShow={
<Alert severity='success'>
This feature toggle is currently
enabled in the{' '}
<strong>{environmentId}</strong>{' '}
environment. Any changes made here
will be available to users as soon
as you hit <strong>save</strong>.
</Alert>
}
/>
</FeatureStrategyEnabled>
</>
}
/>
<ConditionallyRender
condition={tab === 1}
show={
<>
<StyledTargetingHeader>
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.
</StyledTargetingHeader>
<FeatureStrategySegment
segments={segments}
setSegments={setSegments}
projectId={projectId}
/>
<StyledBox>
<StyledDivider />
<StyledDividerContent>AND</StyledDividerContent>
</StyledBox>
<FeatureStrategyConstraints
projectId={feature.project}
environmentId={environmentId}
strategy={strategy}
setStrategy={setStrategy}
/>
</>
}
/>
<ConditionallyRender
condition={tab === 2}
show={
<ConditionallyRender
condition={
strategy.parameters != null &&
'stickiness' in strategy.parameters
}
show={
<StrategyVariants
strategy={strategy}
setStrategy={setStrategy}
environment={environmentId}
projectId={projectId}
/>
}
/>
<FeatureStrategyEnabled
projectId={feature.project}
featureId={feature.name}
environmentId={environmentId}
>
<ConditionallyRender
condition={Boolean(isChangeRequest)}
show={
<Alert severity='success'>
This feature toggle is currently enabled
in the <strong>{environmentId}</strong>{' '}
environment. Any changes made here will
be available to users as soon as these
changes are approved and applied.
</Alert>
}
elseShow={
<Alert severity='success'>
This feature toggle is currently enabled
in the <strong>{environmentId}</strong>{' '}
environment. Any changes made here will
be available to users as soon as you hit{' '}
<strong>save</strong>.
</Alert>
}
/>
</FeatureStrategyEnabled>
<StyledHr />
<FeatureStrategyTitle
title={strategy.title || ''}
setTitle={(title) => {
setStrategy((prev) => ({
...prev,
title,
}));
}}
/>
<FeatureStrategyEnabledDisabled
enabled={!strategy?.disabled}
onToggleEnabled={() =>
setStrategy((strategyState) => ({
...strategyState,
disabled: !strategyState.disabled,
}))
}
/>
<FeatureStrategyType
strategy={strategy}
strategyDefinition={strategyDefinition}
setStrategy={setStrategy}
validateParameter={validateParameter}
errors={errors}
hasAccess={access}
/>
</>
}
/>
<ConditionallyRender
condition={tab === 1}
show={
<>
<StyledTargetingHeader>
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.
</StyledTargetingHeader>
<FeatureStrategySegment
segments={segments}
setSegments={setSegments}
projectId={projectId}
/>
<StyledBox>
<StyledDivider />
<StyledDividerContent>AND</StyledDividerContent>
</StyledBox>
<FeatureStrategyConstraints
projectId={feature.project}
environmentId={environmentId}
strategy={strategy}
setStrategy={setStrategy}
/>
</>
}
/>
<ConditionallyRender
condition={tab === 2}
show={
<ConditionallyRender
condition={
strategy.parameters != null &&
'stickiness' in strategy.parameters
}
show={
<StrategyVariants
strategy={strategy}
setStrategy={setStrategy}
environment={environmentId}
projectId={projectId}
/>
}
/>
}
/>
<StyledButtons>
<PermissionButton
permission={permission}
projectId={feature.project}
environmentId={environmentId}
variant='contained'
color='primary'
type='submit'
disabled={
loading ||
!hasValidConstraints ||
errors.hasFormErrors()
}
data-testid={STRATEGY_FORM_SUBMIT_ID}
>
{isChangeRequest
? changeRequestButtonText
: 'Save strategy'}
</PermissionButton>
<Button
type='button'
color='primary'
onClick={onCancel ? onCancel : onDefaultCancel}
disabled={loading}
>
Cancel
</Button>
<FeatureStrategyProdGuard
open={showProdGuard}
onClose={() => setShowProdGuard(false)}
onClick={onSubmit}
loading={loading}
label='Save strategy'
/>
</StyledButtons>
</StyledForm>
<StyledButtons>
<PermissionButton
permission={permission}
projectId={feature.project}
environmentId={environmentId}
variant='contained'
color='primary'
type='submit'
disabled={
loading ||
!hasValidConstraints ||
errors.hasFormErrors()
}
data-testid={STRATEGY_FORM_SUBMIT_ID}
>
{isChangeRequest
? changeRequestButtonText
: 'Save strategy'}
</PermissionButton>
<Button
type='button'
color='primary'
onClick={onCancel ? onCancel : onDefaultCancel}
disabled={loading}
>
Cancel
</Button>
<FeatureStrategyProdGuard
open={showProdGuard}
onClose={() => setShowProdGuard(false)}
onClick={onSubmit}
loading={loading}
label='Save strategy'
/>
</StyledButtons>
</StyledForm>
</>
);
};

View File

@ -180,10 +180,10 @@ export const NewFeatureStrategyCreate = () => {
return (
<FormTemplate
modal
title={formatStrategyName(strategyName)}
description={featureStrategyHelp}
documentationLink={featureStrategyDocsLink}
documentationLinkLabel={featureStrategyDocsLinkLabel}
disablePadding
formatApiCode={() =>
formatAddStrategyApiCode(
projectId,

View File

@ -200,7 +200,7 @@ export const NewFeatureStrategyEdit = () => {
return (
<FormTemplate
modal
title={formatStrategyName(strategy.name ?? '')}
disablePadding
description={featureStrategyHelp}
documentationLink={featureStrategyDocsLink}
documentationLinkLabel={featureStrategyDocsLinkLabel}

View File

@ -6,18 +6,32 @@ import PermissionButton from '../../common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE_ENVIRONMENT_VARIANTS } from '../../providers/AccessProvider/permissions';
import { v4 as uuidv4 } from 'uuid';
import { WeightType } from '../../../constants/variantTypes';
import { Link, styled, Typography, useTheme } from '@mui/material';
import { Box, Link, styled, Typography, useTheme } from '@mui/material';
import { IFeatureStrategy } from 'interfaces/strategy';
import SplitPreviewSlider from './SplitPreviewSlider/SplitPreviewSlider';
import { HelpIcon } from '../../common/HelpIcon/HelpIcon';
import { StrategyVariantsUpgradeAlert } from '../../common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useUiFlag } from 'hooks/useUiFlag';
import { Add } from '@mui/icons-material';
const StyledVariantForms = styled('div')({
display: 'flex',
flexDirection: 'column',
});
const StyledHelpIconBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
const StyledVariantsHeader = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
marginTop: theme.spacing(1.5),
}));
export const StrategyVariants: FC<{
setStrategy: React.Dispatch<
React.SetStateAction<Partial<IFeatureStrategy>>
@ -29,6 +43,8 @@ export const StrategyVariants: FC<{
const { trackEvent } = usePlausibleTracker();
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
const theme = useTheme();
const newStrategyConfiguration = useUiFlag('newStrategyConfiguration');
const stickiness =
strategy?.parameters && 'stickiness' in strategy?.parameters
? String(strategy.parameters.stickiness)
@ -91,6 +107,86 @@ export const StrategyVariants: FC<{
});
};
if (newStrategyConfiguration) {
return (
<>
<StyledVariantsHeader>
Variants enhance a feature flag by providing a version of
the feature to be enabled
</StyledVariantsHeader>
<StyledHelpIconBox>
<Typography>Variants</Typography>
<HelpIcon
htmlTooltip
tooltip={
<Box>
<Typography variant='body2'>
Variants in feature toggling allow you to
serve different versions of a feature to
different users. This can be used for A/B
testing, gradual rollouts, and canary
releases. Variants provide a way to control
the user experience at a granular level,
enabling you to test and optimize different
aspects of your features. Read more about
variants{' '}
<a
href='https://docs.getunleash.io/reference/strategy-variants'
target='_blank'
rel='noopener noreferrer'
>
here
</a>
</Typography>
</Box>
}
/>
</StyledHelpIconBox>
<StyledVariantForms>
<StrategyVariantsUpgradeAlert />
{variantsEdit.map((variant, i) => (
<VariantForm
disableOverrides={true}
key={variant.id}
variant={variant}
variants={variantsEdit}
updateVariant={(updatedVariant) =>
updateVariant(updatedVariant, variant.id)
}
removeVariant={() =>
setVariantsEdit((variantsEdit) =>
updateWeightEdit(
variantsEdit.filter(
(v) => v.id !== variant.id,
),
1000,
),
)
}
decorationColor={
theme.palette.variants[
i % theme.palette.variants.length
]
}
/>
))}
</StyledVariantForms>
<PermissionButton
onClick={addVariant}
variant='outlined'
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
projectId={projectId}
environmentId={environment}
data-testid='ADD_STRATEGY_VARIANT_BUTTON'
startIcon={<Add />}
>
Add variant
</PermissionButton>
<SplitPreviewSlider variants={variantsEdit} />
</>
);
}
return (
<>
<Typography