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,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 }} | ||||||
|         /> |         /> | ||||||
|  | |||||||
| @ -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