1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-13 11:17:26 +02:00

chore: add feature configuration based on addConfiguration flag (#10420)

https://linear.app/unleash/issue/2-3729/single-add-configuration-button-based-on-flag

Shows a single "Add configuration" button based on whether the new
`addConfiguration` flag is enabled.

This button then shows our "Add configuration" modal which allows you to
choose how to proceed in terms of your feature flag configuration. Also
updates this modal to better match the latest sketches.

Includes scouting.

### Single "Add configuration" button
<img width="738" height="121" alt="image"
src="https://github.com/user-attachments/assets/9cce7fba-5e0c-42e0-a3d1-8ccc34f730bb"
/>

### Modal
<img width="983" height="663" alt="image"
src="https://github.com/user-attachments/assets/b59abad2-f1cd-4b62-bf2e-9c3b24cbb60e"
/>
This commit is contained in:
Nuno Góis 2025-07-29 08:27:13 +01:00 committed by GitHub
parent 0a9d6437c5
commit 15449e83d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 122 deletions

View File

@ -27,7 +27,10 @@ const CardContent = styled('div')(({ theme }) => ({
const HoverButtonsContainer = styled('div')(({ theme }) => ({ const HoverButtonsContainer = styled('div')(({ theme }) => ({
position: 'absolute', position: 'absolute',
right: theme.spacing(2), background: theme.palette.background.paper,
padding: theme.spacing(1),
top: theme.spacing(1),
right: theme.spacing(1),
display: 'flex', display: 'flex',
gap: theme.spacing(1), gap: theme.spacing(1),
opacity: 0, opacity: 0,

View File

@ -75,9 +75,10 @@ export const FeatureStrategyMenu = ({
usePendingChangeRequests(projectId); usePendingChangeRequests(projectId);
const { refetch } = useReleasePlans(projectId, featureId, environmentId); const { refetch } = useReleasePlans(projectId, featureId, environmentId);
const { addReleasePlanToFeature } = useReleasePlansApi(); const { addReleasePlanToFeature } = useReleasePlansApi();
const { isOss } = useUiConfig(); const { isEnterprise } = useUiConfig();
const addConfigurationEnabled = useUiFlag('addConfiguration');
const releasePlansEnabled = useUiFlag('releasePlans'); const releasePlansEnabled = useUiFlag('releasePlans');
const displayReleasePlanButton = !isOss() && releasePlansEnabled; const displayReleasePlanButton = isEnterprise() && releasePlansEnabled;
const crProtected = const crProtected =
releasePlansEnabled && isChangeRequestConfigured(environmentId); releasePlansEnabled && isChangeRequestConfigured(environmentId);
@ -161,56 +162,81 @@ export const FeatureStrategyMenu = ({
return ( return (
<StyledStrategyMenu onClick={(event) => event.stopPropagation()}> <StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
{displayReleasePlanButton ? ( {addConfigurationEnabled ? (
<PermissionButton <PermissionButton
data-testid='ADD_TEMPLATE_BUTTON' data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
onClick={openReleasePlans} onClick={openMoreStrategies}
aria-labelledby={dialogId} aria-labelledby={dialogId}
variant='outlined' variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }} sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)} disabled={Boolean(disableReason)}
tooltipProps={{ tooltipProps={{
title: disableReason ? disableReason : undefined, title: disableReason ? disableReason : undefined,
}} }}
> >
Use template Add configuration
</PermissionButton> </PermissionButton>
) : null} ) : (
<>
{displayReleasePlanButton ? (
<PermissionButton
data-testid='ADD_TEMPLATE_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openReleasePlans}
aria-labelledby={dialogId}
variant='outlined'
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason
? disableReason
: undefined,
}}
>
Use template
</PermissionButton>
) : null}
<PermissionButton <PermissionButton
data-testid='ADD_STRATEGY_BUTTON' data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
onClick={openDefaultStrategyCreationModal} onClick={openDefaultStrategyCreationModal}
aria-labelledby={dialogId} aria-labelledby={dialogId}
variant={variant} variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }} sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)} disabled={Boolean(disableReason)}
tooltipProps={{ tooltipProps={{
title: disableReason ? disableReason : undefined, title: disableReason ? disableReason : undefined,
}} }}
> >
{label} {label}
</PermissionButton> </PermissionButton>
<StyledAdditionalMenuButton <StyledAdditionalMenuButton
permission={CREATE_FEATURE_STRATEGY} permission={CREATE_FEATURE_STRATEGY}
projectId={projectId} projectId={projectId}
environmentId={environmentId} environmentId={environmentId}
onClick={openMoreStrategies} onClick={openMoreStrategies}
variant='outlined' variant='outlined'
hideLockIcon hideLockIcon
disabled={Boolean(disableReason)} disabled={Boolean(disableReason)}
tooltipProps={{ tooltipProps={{
title: disableReason ? disableReason : 'More strategies', title: disableReason
}} ? disableReason
> : 'More strategies',
<MoreVert /> }}
</StyledAdditionalMenuButton> >
<MoreVert />
</StyledAdditionalMenuButton>
</>
)}
<Dialog <Dialog
open={isStrategyMenuDialogOpen} open={isStrategyMenuDialogOpen}
onClose={onClose} onClose={onClose}

View File

@ -1,11 +1,4 @@
import { import { Link, styled, Typography, Box, IconButton } from '@mui/material';
Link,
styled,
Typography,
Box,
IconButton,
Tooltip,
} from '@mui/material';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx'; import { FeatureStrategyMenuCard } from '../FeatureStrategyMenuCard/FeatureStrategyMenuCard.tsx';
import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates'; import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates';
@ -13,10 +6,10 @@ import { FeatureReleasePlanCard } from '../FeatureReleasePlanCard/FeatureRelease
import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; import type { IReleasePlanTemplate } from 'interfaces/releasePlans';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig.ts';
import { useUiFlag } from 'hooks/useUiFlag.ts'; import { useUiFlag } from 'hooks/useUiFlag.ts';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon.tsx';
interface IFeatureStrategyMenuCardsProps { interface IFeatureStrategyMenuCardsProps {
projectId: string; projectId: string;
@ -78,11 +71,6 @@ const SectionTitle = styled(Box)(({ theme }) => ({
width: '100%', width: '100%',
})); }));
const StyledInfoIcon = styled(InfoOutlinedIcon)(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
color: theme.palette.text.secondary,
}));
const StyledIcon = styled('span')(({ theme }) => ({ const StyledIcon = styled('span')(({ theme }) => ({
width: theme.spacing(3), width: theme.spacing(3),
'& > svg': { '& > svg': {
@ -162,55 +150,51 @@ export const FeatureStrategyMenuCards = ({
return null; return null;
} }
if (!templates.length) {
return (
<EmptyStateContainer>
<EmptyStateTitle>
<StyledIcon>
<FactCheckOutlinedIcon />
</StyledIcon>
Create your own release templates
</EmptyStateTitle>
<EmptyStateDescription>
Standardize your rollouts and save time by reusing
predefined strategies. Find release templates in the
side menu under{' '}
<ClickableBoldText
onClick={() => navigate('/release-templates')}
>
Configure &gt; Release templates
</ClickableBoldText>
</EmptyStateDescription>
</EmptyStateContainer>
);
}
return ( return (
<Box> <Box>
<SectionTitle> <SectionTitle>
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Apply a release template Release templates
</Typography> </Typography>
<Tooltip <HelpIcon
title='Use a predefined template to roll out features to users' tooltip='Use a predefined template to roll out features to users'
arrow size='16px'
> />
<StyledInfoIcon />
</Tooltip>
</SectionTitle> </SectionTitle>
<GridSection> {!templates.length ? (
{templates.map((template) => ( <EmptyStateContainer>
<CardWrapper key={template.id}> <EmptyStateTitle>
<FeatureReleasePlanCard <StyledIcon>
template={template} <FactCheckOutlinedIcon />
onClick={() => onAddReleasePlan(template)} </StyledIcon>
onPreviewClick={() => Create your own release templates
onReviewReleasePlan(template) </EmptyStateTitle>
} <EmptyStateDescription>
/> Standardize your rollouts and save time by reusing
</CardWrapper> predefined strategies. Find release templates in the
))} side menu under{' '}
</GridSection> <ClickableBoldText
onClick={() => navigate('/release-templates')}
>
Configure &gt; Release templates
</ClickableBoldText>
</EmptyStateDescription>
</EmptyStateContainer>
) : (
<GridSection>
{templates.map((template) => (
<CardWrapper key={template.id}>
<FeatureReleasePlanCard
template={template}
onClick={() => onAddReleasePlan(template)}
onPreviewClick={() =>
onReviewReleasePlan(template)
}
/>
</CardWrapper>
))}
</GridSection>
)}
</Box> </Box>
); );
}; };
@ -219,7 +203,7 @@ export const FeatureStrategyMenuCards = ({
<GridContainer> <GridContainer>
<TitleRow> <TitleRow>
<TitleText variant='h2'> <TitleText variant='h2'>
{onlyReleasePlans ? 'Select template' : 'Select strategy'} {onlyReleasePlans ? 'Select template' : 'Add configuration'}
</TitleText> </TitleText>
<IconButton <IconButton
size='small' size='small'
@ -238,14 +222,12 @@ export const FeatureStrategyMenuCards = ({
<Box> <Box>
<SectionTitle> <SectionTitle>
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Pre-defined strategy types Standard strategies
</Typography> </Typography>
<Tooltip <HelpIcon
title='Select a starting setup, and customize the strategy to your need with targeting and variants' tooltip='Select a starting setup, then customize your strategy with targeting and variants'
arrow size='16px'
> />
<StyledInfoIcon />
</Tooltip>
</SectionTitle> </SectionTitle>
<GridSection> <GridSection>
<CardWrapper key={defaultStrategy.name}> <CardWrapper key={defaultStrategy.name}>
@ -278,12 +260,10 @@ export const FeatureStrategyMenuCards = ({
<Typography color='inherit' variant='body2'> <Typography color='inherit' variant='body2'>
Custom strategies Custom strategies
</Typography> </Typography>
<Tooltip <HelpIcon
title='Custom strategies you have defined in Unleash' tooltip='Custom strategies you have defined in Unleash'
arrow size='16px'
> />
<StyledInfoIcon />
</Tooltip>
</SectionTitle> </SectionTitle>
<GridSection> <GridSection>
{customStrategies.map((strategy) => ( {customStrategies.map((strategy) => (

View File

@ -2,6 +2,8 @@ import type { FC } from 'react';
import { styled, Link } from '@mui/material'; import { styled, Link } from '@mui/material';
import type { Link as RouterLink } from 'react-router-dom'; import type { Link as RouterLink } from 'react-router-dom';
import { RELEASE_TEMPLATE_FEEDBACK } from 'constants/links'; import { RELEASE_TEMPLATE_FEEDBACK } from 'constants/links';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({ const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -13,14 +15,23 @@ const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({
marginRight: 'auto', marginRight: 'auto',
})); }));
export const ReleaseTemplatesFeedback: FC = () => ( export const ReleaseTemplatesFeedback: FC = () => {
<StyledLink const { isEnterprise } = useUiConfig();
component='a' const releaseTemplatesEnabled = useUiFlag('releasePlans');
href={RELEASE_TEMPLATE_FEEDBACK}
underline='hover' if (!isEnterprise() || !releaseTemplatesEnabled) {
rel='noopener noreferrer' return null;
target='_blank' }
>
Give feedback to release templates return (
</StyledLink> <StyledLink
); component='a'
href={RELEASE_TEMPLATE_FEEDBACK}
underline='hover'
rel='noopener noreferrer'
target='_blank'
>
Give feedback to release templates
</StyledLink>
);
};

View File

@ -95,6 +95,7 @@ export type UiFlags = {
timestampsInChangeRequestTimeline?: boolean; timestampsInChangeRequestTimeline?: boolean;
reportUnknownFlags?: boolean; reportUnknownFlags?: boolean;
lifecycleGraphs?: boolean; lifecycleGraphs?: boolean;
addConfiguration?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -65,7 +65,8 @@ export type IFlagKey =
| 'paygInstanceStatsEvents' | 'paygInstanceStatsEvents'
| 'timestampsInChangeRequestTimeline' | 'timestampsInChangeRequestTimeline'
| 'lifecycleGraphs' | 'lifecycleGraphs'
| 'githubAuth'; | 'githubAuth'
| 'addConfiguration';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -305,6 +306,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_GITHUB_AUTH, process.env.UNLEASH_EXPERIMENTAL_GITHUB_AUTH,
false, false,
), ),
addConfiguration: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ADD_CONFIGURATION,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -61,6 +61,7 @@ process.nextTick(async () => {
paygTrialEvents: true, paygTrialEvents: true,
timestampsInChangeRequestTimeline: true, timestampsInChangeRequestTimeline: true,
lifecycleGraphs: true, lifecycleGraphs: true,
addConfiguration: true,
}, },
}, },
authentication: { authentication: {