1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +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,6 +162,25 @@ export const FeatureStrategyMenu = ({
return ( return (
<StyledStrategyMenu onClick={(event) => event.stopPropagation()}> <StyledStrategyMenu onClick={(event) => event.stopPropagation()}>
{addConfigurationEnabled ? (
<PermissionButton
data-testid='ADD_STRATEGY_BUTTON'
permission={CREATE_FEATURE_STRATEGY}
projectId={projectId}
environmentId={environmentId}
onClick={openMoreStrategies}
aria-labelledby={dialogId}
variant={variant}
sx={{ minWidth: matchWidth ? '282px' : 'auto' }}
disabled={Boolean(disableReason)}
tooltipProps={{
title: disableReason ? disableReason : undefined,
}}
>
Add configuration
</PermissionButton>
) : (
<>
{displayReleasePlanButton ? ( {displayReleasePlanButton ? (
<PermissionButton <PermissionButton
data-testid='ADD_TEMPLATE_BUTTON' data-testid='ADD_TEMPLATE_BUTTON'
@ -173,7 +193,9 @@ export const FeatureStrategyMenu = ({
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 Use template
@ -206,11 +228,15 @@ export const FeatureStrategyMenu = ({
hideLockIcon hideLockIcon
disabled={Boolean(disableReason)} disabled={Boolean(disableReason)}
tooltipProps={{ tooltipProps={{
title: disableReason ? disableReason : 'More strategies', title: disableReason
? disableReason
: 'More strategies',
}} }}
> >
<MoreVert /> <MoreVert />
</StyledAdditionalMenuButton> </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,8 +150,18 @@ export const FeatureStrategyMenuCards = ({
return null; return null;
} }
if (!templates.length) {
return ( return (
<Box>
<SectionTitle>
<Typography color='inherit' variant='body2'>
Release templates
</Typography>
<HelpIcon
tooltip='Use a predefined template to roll out features to users'
size='16px'
/>
</SectionTitle>
{!templates.length ? (
<EmptyStateContainer> <EmptyStateContainer>
<EmptyStateTitle> <EmptyStateTitle>
<StyledIcon> <StyledIcon>
@ -182,22 +180,7 @@ export const FeatureStrategyMenuCards = ({
</ClickableBoldText> </ClickableBoldText>
</EmptyStateDescription> </EmptyStateDescription>
</EmptyStateContainer> </EmptyStateContainer>
); ) : (
}
return (
<Box>
<SectionTitle>
<Typography color='inherit' variant='body2'>
Apply a release template
</Typography>
<Tooltip
title='Use a predefined template to roll out features to users'
arrow
>
<StyledInfoIcon />
</Tooltip>
</SectionTitle>
<GridSection> <GridSection>
{templates.map((template) => ( {templates.map((template) => (
<CardWrapper key={template.id}> <CardWrapper key={template.id}>
@ -211,6 +194,7 @@ export const FeatureStrategyMenuCards = ({
</CardWrapper> </CardWrapper>
))} ))}
</GridSection> </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,7 +15,15 @@ const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({
marginRight: 'auto', marginRight: 'auto',
})); }));
export const ReleaseTemplatesFeedback: FC = () => ( export const ReleaseTemplatesFeedback: FC = () => {
const { isEnterprise } = useUiConfig();
const releaseTemplatesEnabled = useUiFlag('releasePlans');
if (!isEnterprise() || !releaseTemplatesEnabled) {
return null;
}
return (
<StyledLink <StyledLink
component='a' component='a'
href={RELEASE_TEMPLATE_FEEDBACK} href={RELEASE_TEMPLATE_FEEDBACK}
@ -23,4 +33,5 @@ export const ReleaseTemplatesFeedback: FC = () => (
> >
Give feedback to release templates Give feedback to release templates
</StyledLink> </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: {