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:
parent
c65a336783
commit
c39b4cd1b0
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
Add the
|
||||||
|
<HtmlTooltip
|
||||||
|
title={
|
||||||
|
<StyledBox>
|
||||||
|
<TooltipHeader>Default strategy</TooltipHeader>
|
||||||
|
<TooltipDescription>
|
||||||
|
Defined per project, per environment
|
||||||
|
<Link
|
||||||
|
to={editDefaultStrategyPath}
|
||||||
|
title='Project default strategies'
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</TooltipDescription>
|
||||||
|
<StrategyExecution strategy={strategy} />
|
||||||
|
</StyledBox>
|
||||||
|
}
|
||||||
|
maxWidth='200'
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledSpan>default strategy</StyledSpan>
|
||||||
|
</HtmlTooltip>
|
||||||
|
for this project
|
||||||
|
<PermissionButton
|
||||||
|
size='small'
|
||||||
|
permission={UPDATE_FEATURE}
|
||||||
|
projectId={projectId}
|
||||||
|
variant='text'
|
||||||
|
onClick={() => openStrategyCreationModal()}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</PermissionButton>
|
||||||
|
</StyledSuggestion>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,13 +23,24 @@ const FeatureOverviewWithReleasePlans: FC<
|
|||||||
featureId,
|
featureId,
|
||||||
environment?.name,
|
environment?.name,
|
||||||
);
|
);
|
||||||
|
const envAddStrategySuggestionEnabled = useUiFlag(
|
||||||
|
'envAddStrategySuggestion',
|
||||||
|
);
|
||||||
|
if (envAddStrategySuggestionEnabled) {
|
||||||
return (
|
return (
|
||||||
<FeatureOverviewEnvironment
|
<FeatureOverviewEnvironment
|
||||||
{...props}
|
{...props}
|
||||||
environment={{ ...environment, releasePlans }}
|
environment={{ ...environment, releasePlans }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LegacyFeatureOverviewEnvironment
|
||||||
|
{...props}
|
||||||
|
environment={{ ...environment, releasePlans }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeatureOverviewEnvironments: FC<
|
export const FeatureOverviewEnvironments: FC<
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -59,6 +59,7 @@ process.nextTick(async () => {
|
|||||||
flagsUiFilterRefactor: true,
|
flagsUiFilterRefactor: true,
|
||||||
trafficBillingDisplay: true,
|
trafficBillingDisplay: true,
|
||||||
milestoneProgression: true,
|
milestoneProgression: true,
|
||||||
|
envAddStrategySuggestion: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user