1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: add a suggestion banner at the bottom of empty feature-environments (#10725)

This commit is contained in:
David Leek 2025-10-06 09:02:15 +02:00 committed by GitHub
parent c65a336783
commit c39b4cd1b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 525 additions and 10 deletions

View File

@ -1,4 +1,4 @@
import type { FC, PropsWithChildren } from 'react'; import { useMemo, type FC, type PropsWithChildren } from 'react';
import { import {
AccordionSummary, AccordionSummary,
type AccordionSummaryProps, type AccordionSummaryProps,
@ -7,12 +7,16 @@ import {
import ExpandMore from '@mui/icons-material/ExpandMore'; import ExpandMore from '@mui/icons-material/ExpandMore';
import { Truncator } from 'component/common/Truncator/Truncator'; import { Truncator } from 'component/common/Truncator/Truncator';
import { useId } from 'hooks/useId'; import { useId } from 'hooks/useId';
import { EnvironmentStrategySuggestion } from './EnvironmentStrategySuggestion/EnvironmentStrategySuggestion.js';
import type { IFeatureStrategy } from 'interfaces/strategy';
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
const StyledAccordionSummary = styled(AccordionSummary, { const StyledAccordionSummary = styled(AccordionSummary, {
shouldForwardProp: (prop) => prop !== 'expandable', shouldForwardProp: (prop) => prop !== 'expandable' && prop !== 'empty',
})<{ })<{
expandable?: boolean; expandable?: boolean;
}>(({ theme, expandable }) => ({ empty?: boolean;
}>(({ theme, expandable, empty }) => ({
boxShadow: 'none', boxShadow: 'none',
padding: theme.spacing(0.5, 3, 0.5, 2), padding: theme.spacing(0.5, 3, 0.5, 2),
display: 'flex', display: 'flex',
@ -27,9 +31,26 @@ const StyledAccordionSummary = styled(AccordionSummary, {
':focus-within': { ':focus-within': {
background: 'none', background: 'none',
}, },
...(empty && {
padding: 0,
alignItems: 'normal',
'.MuiAccordionSummary-content': {
marginBottom: '0px',
paddingBottom: '0px',
flexDirection: 'column',
},
'.MuiAccordionSummary-expandIconWrapper': {
width: '0px',
},
}),
})); }));
const StyledHeader = styled('header')(({ theme }) => ({ const StyledHeader = styled('header', {
shouldForwardProp: (prop) => prop !== 'empty',
})<{
empty?: boolean;
}>(({ theme, empty }) => ({
display: 'flex', display: 'flex',
columnGap: theme.spacing(1), columnGap: theme.spacing(1),
paddingRight: theme.spacing(1), paddingRight: theme.spacing(1),
@ -37,6 +58,9 @@ const StyledHeader = styled('header')(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
alignItems: 'center', alignItems: 'center',
minHeight: theme.spacing(8), minHeight: theme.spacing(8),
...(empty && {
padding: theme.spacing(0, 8, 0, 2),
}),
})); }));
const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({ const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({
@ -79,9 +103,12 @@ type EnvironmentMetadata = {
}; };
type EnvironmentHeaderProps = { type EnvironmentHeaderProps = {
projectId: string;
featureId: string;
environmentId: string; environmentId: string;
expandable?: boolean; expandable?: boolean;
environmentMetadata?: EnvironmentMetadata; environmentMetadata?: EnvironmentMetadata;
hasActivations?: boolean;
} & AccordionSummaryProps; } & AccordionSummaryProps;
const MetadataChip = ({ const MetadataChip = ({
@ -110,19 +137,53 @@ const MetadataChip = ({
return <StyledStrategyCount>{text}</StyledStrategyCount>; return <StyledStrategyCount>{text}</StyledStrategyCount>;
}; };
const DEFAULT_STRATEGY: Omit<IFeatureStrategy, 'id'> = {
name: 'flexibleRollout',
disabled: false,
constraints: [],
title: '',
parameters: {
rollout: '100',
stickiness: 'default',
groupId: '',
},
};
export const environmentAccordionSummaryClassName = export const environmentAccordionSummaryClassName =
'environment-accordion-summary'; 'environment-accordion-summary';
export const EnvironmentHeader: FC< export const EnvironmentHeader: FC<
PropsWithChildren<EnvironmentHeaderProps> PropsWithChildren<EnvironmentHeaderProps>
> = ({ > = ({
projectId,
featureId,
environmentId, environmentId,
children, children,
expandable = true, expandable = true,
environmentMetadata, environmentMetadata,
hasActivations = false,
...props ...props
}) => { }) => {
const id = useId(); const id = useId();
const { environments } = useProjectEnvironments(projectId);
const defaultStrategy = environments.find(
(env) => env.name === environmentId,
)?.defaultStrategy;
const strategy: Omit<IFeatureStrategy, 'id'> = useMemo(() => {
const baseDefaultStrategy = {
...DEFAULT_STRATEGY,
...defaultStrategy,
};
return {
...baseDefaultStrategy,
disabled: false,
constraints: baseDefaultStrategy.constraints ?? [],
title: baseDefaultStrategy.title ?? '',
parameters: baseDefaultStrategy.parameters ?? {},
};
}, [JSON.stringify(defaultStrategy)]);
return ( return (
<StyledAccordionSummary <StyledAccordionSummary
{...props} {...props}
@ -136,8 +197,9 @@ export const EnvironmentHeader: FC<
expandable={expandable} expandable={expandable}
tabIndex={expandable ? 0 : -1} tabIndex={expandable ? 0 : -1}
className={environmentAccordionSummaryClassName} className={environmentAccordionSummaryClassName}
empty={!hasActivations}
> >
<StyledHeader data-loading> <StyledHeader empty={!hasActivations} data-loading>
<StyledHeaderTitle> <StyledHeaderTitle>
<StyledHeaderTitleLabel>Environment</StyledHeaderTitleLabel> <StyledHeaderTitleLabel>Environment</StyledHeaderTitleLabel>
<StyledTruncator component='h2'> <StyledTruncator component='h2'>
@ -149,6 +211,14 @@ export const EnvironmentHeader: FC<
</StyledHeaderTitle> </StyledHeaderTitle>
{children} {children}
</StyledHeader> </StyledHeader>
{!hasActivations && (
<EnvironmentStrategySuggestion
projectId={projectId}
featureId={featureId}
environmentId={environmentId}
strategy={strategy}
/>
)}
</StyledAccordionSummary> </StyledAccordionSummary>
); );
}; };

View File

@ -0,0 +1,115 @@
import { Box, styled } from '@mui/material';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Link, useNavigate } from 'react-router-dom';
import { StrategyExecution } from '../../EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.js';
import PermissionButton from 'component/common/PermissionButton/PermissionButton.js';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker.js';
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate.js';
import { UPDATE_FEATURE } from '@server/types/permissions.js';
import type { IFeatureStrategy } from 'interfaces/strategy.js';
const StyledSuggestion = styled('div')(({ theme }) => ({
width: '100%',
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0.5, 3),
background: theme.palette.secondary.light,
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge,
color: theme.palette.primary.main,
fontSize: theme.fontSizes.smallerBody,
}));
const StyledBold = styled('b')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}));
const StyledSpan = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
textDecoration: 'underline',
}));
const TooltipHeader = styled('div')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}));
const TooltipDescription = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
paddingBottom: theme.spacing(1.5),
}));
const StyledBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1.5),
}));
type DefaultStrategySuggestionProps = {
projectId: string;
featureId: string;
environmentId: string;
strategy: Omit<IFeatureStrategy, 'id'>;
};
export const EnvironmentStrategySuggestion = ({
projectId,
featureId,
environmentId,
strategy,
}: DefaultStrategySuggestionProps) => {
const { trackEvent } = usePlausibleTracker();
const navigate = useNavigate();
const editDefaultStrategyPath = `/projects/${projectId}/settings/default-strategy`;
const createStrategyPath = formatCreateStrategyPath(
projectId,
featureId,
environmentId,
'flexibleRollout',
true,
);
const openStrategyCreationModal = () => {
trackEvent('suggestion-strategy-add', {
props: {
buttonTitle: 'flexibleRollout',
},
});
navigate(createStrategyPath);
};
return (
<StyledSuggestion>
<StyledBold>Suggestion:</StyledBold>
&nbsp;Add the&nbsp;
<HtmlTooltip
title={
<StyledBox>
<TooltipHeader>Default strategy</TooltipHeader>
<TooltipDescription>
Defined per project, per environment&nbsp;
<Link
to={editDefaultStrategyPath}
title='Project default strategies'
>
here
</Link>
</TooltipDescription>
<StrategyExecution strategy={strategy} />
</StyledBox>
}
maxWidth='200'
arrow
>
<StyledSpan>default strategy</StyledSpan>
</HtmlTooltip>
&nbsp;for this project&nbsp;
<PermissionButton
size='small'
permission={UPDATE_FEATURE}
projectId={projectId}
variant='text'
onClick={() => openStrategyCreationModal()}
>
Apply
</PermissionButton>
</StyledSuggestion>
);
};

View File

@ -0,0 +1,154 @@
import type { FC, PropsWithChildren } from 'react';
import {
AccordionSummary,
type AccordionSummaryProps,
styled,
} from '@mui/material';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { Truncator } from 'component/common/Truncator/Truncator';
import { useId } from 'hooks/useId';
const StyledAccordionSummary = styled(AccordionSummary, {
shouldForwardProp: (prop) => prop !== 'expandable',
})<{
expandable?: boolean;
}>(({ theme, expandable }) => ({
boxShadow: 'none',
padding: theme.spacing(0.5, 3, 0.5, 2),
display: 'flex',
alignItems: 'center',
borderRadius: theme.shape.borderRadiusLarge,
pointerEvents: 'auto',
opacity: 1,
'&&&': {
cursor: expandable ? 'pointer' : 'default',
},
':focus-within': {
background: 'none',
},
}));
const StyledHeader = styled('header')(({ theme }) => ({
display: 'flex',
columnGap: theme.spacing(1),
paddingRight: theme.spacing(1),
width: '100%',
color: theme.palette.text.primary,
alignItems: 'center',
minHeight: theme.spacing(8),
}));
const StyledHeaderTitle = styled('hgroup')(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
flex: 1,
columnGap: theme.spacing(1),
}));
const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({
width: '100%',
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
}));
const StyledTruncator = styled(Truncator)(({ theme }) => ({
fontSize: theme.typography.h2.fontSize,
fontWeight: theme.typography.fontWeightMedium,
}));
const StyledStrategyCount = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.info.contrastText,
backgroundColor: theme.palette.info.light,
whiteSpace: 'nowrap',
width: 'min-content',
borderRadius: theme.shape.borderRadiusExtraLarge,
padding: theme.spacing(0.5, 1),
}));
const NeutralStrategyCount = styled(StyledStrategyCount)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
backgroundColor: theme.palette.neutral.light,
}));
type EnvironmentMetadata = {
strategyCount: number;
releasePlanCount: number;
};
type EnvironmentHeaderProps = {
environmentId: string;
expandable?: boolean;
environmentMetadata?: EnvironmentMetadata;
} & AccordionSummaryProps;
const MetadataChip = ({
strategyCount,
releasePlanCount,
}: EnvironmentMetadata) => {
if (strategyCount === 0 && releasePlanCount === 0) {
return <NeutralStrategyCount>0 strategies added</NeutralStrategyCount>;
}
const releasePlanText = releasePlanCount > 0 ? 'Release plan' : undefined;
const strategyText = () => {
switch (strategyCount) {
case 0:
return undefined;
case 1:
return `1 strategy`;
default:
return `${strategyCount} strategies`;
}
};
const text = `${[releasePlanText, strategyText()].filter(Boolean).join(', ')} added`;
return <StyledStrategyCount>{text}</StyledStrategyCount>;
};
export const environmentAccordionSummaryClassName =
'environment-accordion-summary';
export const LegacyEnvironmentHeader: FC<
PropsWithChildren<EnvironmentHeaderProps>
> = ({
environmentId,
children,
expandable = true,
environmentMetadata,
...props
}) => {
const id = useId();
return (
<StyledAccordionSummary
{...props}
expandIcon={
<ExpandMore
sx={{ visibility: expandable ? 'visible' : 'hidden' }}
/>
}
id={id}
aria-controls={`environment-accordion-${id}-content`}
expandable={expandable}
tabIndex={expandable ? 0 : -1}
className={environmentAccordionSummaryClassName}
>
<StyledHeader data-loading>
<StyledHeaderTitle>
<StyledHeaderTitleLabel>Environment</StyledHeaderTitleLabel>
<StyledTruncator component='h2'>
{environmentId}
</StyledTruncator>
{environmentMetadata ? (
<MetadataChip {...environmentMetadata} />
) : null}
</StyledHeaderTitle>
{children}
</StyledHeader>
</StyledAccordionSummary>
);
};

View File

@ -29,6 +29,8 @@ const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
const StyledAccordion = styled(Accordion)(({ theme }) => ({ const StyledAccordion = styled(Accordion)(({ theme }) => ({
boxShadow: 'none', boxShadow: 'none',
background: 'none', background: 'none',
borderRadius: theme.shape.borderRadiusLarge,
[`&:has(.${environmentAccordionSummaryClassName}:focus-visible)`]: { [`&:has(.${environmentAccordionSummaryClassName}:focus-visible)`]: {
background: theme.palette.table.headerHover, background: theme.palette.table.headerHover,
}, },
@ -97,7 +99,10 @@ export const FeatureOverviewEnvironment = ({
releasePlanCount: environment.releasePlans?.length ?? 0, releasePlanCount: environment.releasePlans?.length ?? 0,
}} }}
environmentId={environment.name} environmentId={environment.name}
projectId={projectId}
featureId={featureId}
expandable={hasActivations} expandable={hasActivations}
hasActivations={hasActivations}
> >
<FeatureOverviewEnvironmentToggle <FeatureOverviewEnvironmentToggle
environment={environment} environment={environment}

View File

@ -0,0 +1,147 @@
import { Accordion, AccordionDetails, styled } from '@mui/material';
import type {
IFeatureEnvironment,
IFeatureEnvironmentMetrics,
} from 'interfaces/featureToggle';
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { UpgradeChangeRequests } from '../UpgradeChangeRequests/UpgradeChangeRequests.tsx';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import {
environmentAccordionSummaryClassName,
LegacyEnvironmentHeader,
} from '../EnvironmentHeader/LegacyEnvironmentHeader/LegacyEnvironmentHeader.tsx';
import FeatureOverviewEnvironmentMetrics from '../EnvironmentHeader/FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics.tsx';
import { FeatureOverviewEnvironmentToggle } from '../EnvironmentHeader/FeatureOverviewEnvironmentToggle/FeatureOverviewEnvironmentToggle.tsx';
import { useState } from 'react';
import type { IReleasePlan } from 'interfaces/releasePlans';
import { EnvironmentAccordionBody } from '../EnvironmentAccordionBody/EnvironmentAccordionBody.tsx';
import { Box } from '@mui/material';
import { ReleaseTemplatesFeedback } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/ReleaseTemplatesFeedback/ReleaseTemplatesFeedback';
const StyledFeatureOverviewEnvironment = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
}));
const StyledAccordion = styled(Accordion)(({ theme }) => ({
boxShadow: 'none',
background: 'none',
[`&:has(.${environmentAccordionSummaryClassName}:focus-visible)`]: {
background: theme.palette.table.headerHover,
},
}));
const NewStyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
padding: 0,
background: theme.palette.background.elevation1,
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
borderBottomRightRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.accordionFooter,
}));
const StyledAccordionFooter = styled('footer')(({ theme }) => ({
padding: theme.spacing(2, 3, 3),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const StyledEnvironmentAccordionContainer = styled('div')(({ theme }) => ({
width: '100%',
position: 'relative',
}));
type FeatureOverviewEnvironmentProps = {
environment: IFeatureEnvironment & {
releasePlans?: IReleasePlan[];
};
metrics?: Pick<IFeatureEnvironmentMetrics, 'yes' | 'no'>;
otherEnvironments?: string[];
onToggleEnvOpen?: (isOpen: boolean) => void;
};
export const LegacyFeatureOverviewEnvironment = ({
environment,
metrics = { yes: 0, no: 0 },
otherEnvironments = [],
onToggleEnvOpen = () => {},
}: FeatureOverviewEnvironmentProps) => {
const [isOpen, setIsOpen] = useState(false);
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { isOss } = useUiConfig();
const hasActivations = Boolean(
environment?.enabled ||
(environment?.strategies && environment?.strategies.length > 0) ||
(environment?.releasePlans && environment?.releasePlans.length > 0),
);
return (
<StyledFeatureOverviewEnvironment>
<StyledAccordion
TransitionProps={{ mountOnEnter: true, unmountOnExit: true }}
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${environment.name}`}
expanded={isOpen && hasActivations}
onChange={() => {
const state = isOpen ? !isOpen : hasActivations;
onToggleEnvOpen(state);
setIsOpen(state);
}}
>
<LegacyEnvironmentHeader
environmentMetadata={{
strategyCount: environment.strategies?.length ?? 0,
releasePlanCount: environment.releasePlans?.length ?? 0,
}}
environmentId={environment.name}
expandable={hasActivations}
>
<FeatureOverviewEnvironmentToggle
environment={environment}
/>
{!hasActivations ? (
<FeatureStrategyMenu
label='Add strategy'
projectId={projectId}
featureId={featureId}
environmentId={environment.name}
variant='outlined'
/>
) : (
<FeatureOverviewEnvironmentMetrics
environmentMetric={metrics}
/>
)}
</LegacyEnvironmentHeader>
<NewStyledAccordionDetails>
<StyledEnvironmentAccordionContainer>
<EnvironmentAccordionBody
featureEnvironment={environment}
isDisabled={!environment.enabled}
otherEnvironments={otherEnvironments}
/>
</StyledEnvironmentAccordionContainer>
<StyledAccordionFooter>
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
<ReleaseTemplatesFeedback />
<Box ml='auto'>
<FeatureStrategyMenu
label='Add strategy'
projectId={projectId}
featureId={featureId}
environmentId={environment.name}
/>
</Box>
</Box>
{isOss() && environment?.type === 'production' ? (
<UpgradeChangeRequests />
) : null}
</StyledAccordionFooter>
</NewStyledAccordionDetails>
</StyledAccordion>
</StyledFeatureOverviewEnvironment>
);
};

View File

@ -1,10 +1,12 @@
import type { ComponentProps, FC } from 'react'; import type { ComponentProps, FC } from 'react';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { LegacyFeatureOverviewEnvironment } from './FeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment/LegacyFeatureOverviewEnvironment.tsx';
import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx'; import { FeatureOverviewEnvironment } from './FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics'; import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
import { getFeatureMetrics } from 'utils/getFeatureMetrics'; import { getFeatureMetrics } from 'utils/getFeatureMetrics';
import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans';
import { useUiFlag } from 'hooks/useUiFlag';
type FeatureOverviewEnvironmentsProps = { type FeatureOverviewEnvironmentsProps = {
hiddenEnvironments?: string[]; hiddenEnvironments?: string[];
@ -12,7 +14,7 @@ type FeatureOverviewEnvironmentsProps = {
}; };
const FeatureOverviewWithReleasePlans: FC< const FeatureOverviewWithReleasePlans: FC<
ComponentProps<typeof FeatureOverviewEnvironment> ComponentProps<typeof LegacyFeatureOverviewEnvironment>
> = ({ environment, ...props }) => { > = ({ environment, ...props }) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -21,9 +23,20 @@ const FeatureOverviewWithReleasePlans: FC<
featureId, featureId,
environment?.name, environment?.name,
); );
const envAddStrategySuggestionEnabled = useUiFlag(
'envAddStrategySuggestion',
);
if (envAddStrategySuggestionEnabled) {
return (
<FeatureOverviewEnvironment
{...props}
environment={{ ...environment, releasePlans }}
/>
);
}
return ( return (
<FeatureOverviewEnvironment <LegacyFeatureOverviewEnvironment
{...props} {...props}
environment={{ ...environment, releasePlans }} environment={{ ...environment, releasePlans }}
/> />

View File

@ -2,7 +2,7 @@ import { Accordion, AccordionDetails, styled } from '@mui/material';
import { PROJECT_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { PROJECT_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import type { ProjectEnvironmentType } from '../../../../../../interfaces/environments.ts'; import type { ProjectEnvironmentType } from '../../../../../../interfaces/environments.ts';
import { ProjectEnvironmentDefaultStrategy } from './ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx'; import { ProjectEnvironmentDefaultStrategy } from './ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx';
import { EnvironmentHeader } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/EnvironmentHeader'; import { LegacyEnvironmentHeader } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentHeader/LegacyEnvironmentHeader/LegacyEnvironmentHeader';
interface IProjectEnvironmentProps { interface IProjectEnvironmentProps {
environment: ProjectEnvironmentType; environment: ProjectEnvironmentType;
@ -35,7 +35,10 @@ export const ProjectEnvironment = ({
onChange={(e) => e.stopPropagation()} onChange={(e) => e.stopPropagation()}
data-testid={`${PROJECT_ENVIRONMENT_ACCORDION}_${name}`} data-testid={`${PROJECT_ENVIRONMENT_ACCORDION}_${name}`}
> >
<EnvironmentHeader environmentId={name} expandable={false} /> <LegacyEnvironmentHeader
environmentId={name}
expandable={false}
/>
<StyledAccordionDetails> <StyledAccordionDetails>
<ProjectEnvironmentDefaultStrategy <ProjectEnvironmentDefaultStrategy
environment={environment} environment={environment}

View File

@ -41,6 +41,7 @@ export type CustomEvents =
| 'context-usage' | 'context-usage'
| 'segment-usage' | 'segment-usage'
| 'strategy-add' | 'strategy-add'
| 'suggestion-strategy-add'
| 'playground' | 'playground'
| 'feature-type-edit' | 'feature-type-edit'
| 'strategy-variants' | 'strategy-variants'

View File

@ -91,6 +91,7 @@ export type UiFlags = {
flagsUiFilterRefactor?: boolean; flagsUiFilterRefactor?: boolean;
trafficBillingDisplay?: boolean; trafficBillingDisplay?: boolean;
milestoneProgression?: boolean; milestoneProgression?: boolean;
envAddStrategySuggestion?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -62,7 +62,8 @@ export type IFlagKey =
| 'newUiConfigService' | 'newUiConfigService'
| 'flagsUiFilterRefactor' | 'flagsUiFilterRefactor'
| 'trafficBillingDisplay' | 'trafficBillingDisplay'
| 'milestoneProgression'; | 'milestoneProgression'
| 'envAddStrategySuggestion';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -287,6 +288,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_MILESTONE_PROGRESSION, process.env.UNLEASH_EXPERIMENTAL_MILESTONE_PROGRESSION,
false, false,
), ),
envAddStrategySuggestion: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ENV_ADD_STRATEGY_SUGGESTION,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -59,6 +59,7 @@ process.nextTick(async () => {
flagsUiFilterRefactor: true, flagsUiFilterRefactor: true,
trafficBillingDisplay: true, trafficBillingDisplay: true,
milestoneProgression: true, milestoneProgression: true,
envAddStrategySuggestion: true,
}, },
}, },
authentication: { authentication: {