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:
parent
848415c5ca
commit
cbd6aa1324
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -180,10 +180,10 @@ export const NewFeatureStrategyCreate = () => {
|
||||
return (
|
||||
<FormTemplate
|
||||
modal
|
||||
title={formatStrategyName(strategyName)}
|
||||
description={featureStrategyHelp}
|
||||
documentationLink={featureStrategyDocsLink}
|
||||
documentationLinkLabel={featureStrategyDocsLinkLabel}
|
||||
disablePadding
|
||||
formatApiCode={() =>
|
||||
formatAddStrategyApiCode(
|
||||
projectId,
|
||||
|
@ -200,7 +200,7 @@ export const NewFeatureStrategyEdit = () => {
|
||||
return (
|
||||
<FormTemplate
|
||||
modal
|
||||
title={formatStrategyName(strategy.name ?? '')}
|
||||
disablePadding
|
||||
description={featureStrategyHelp}
|
||||
documentationLink={featureStrategyDocsLink}
|
||||
documentationLinkLabel={featureStrategyDocsLinkLabel}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user