mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: release plans flow in flag environments (#8843)
https://linear.app/unleash/issue/2-2816/add-release-plan-to-feature-flag-from-release-template https://linear.app/unleash/issue/2-2818/list-release-plan-with-milestones-in-feature-flag-environment-section https://linear.app/unleash/issue/2-2819/removing-release-plan-from-feature Implements the release plan flow in the feature flag environment. You can now manage release plans in a feature flag environment by adding or removing them, as well as start milestones. https://github.com/user-attachments/assets/24db9db4-7c3a-463e-b48a-611358f2b212
This commit is contained in:
		
							parent
							
								
									26d96a0002
								
							
						
					
					
						commit
						14403d7836
					
				| @ -6,6 +6,7 @@ import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; | ||||
| import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| 
 | ||||
| const StyledIcon = styled('div')(({ theme }) => ({ | ||||
|     width: theme.spacing(4), | ||||
| @ -60,6 +61,7 @@ export const FeatureReleasePlanCard = ({ | ||||
| }: IFeatureReleasePlanCardProps) => { | ||||
|     const Icon = getFeatureStrategyIcon('releasePlanTemplate'); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
|     const { refetch } = useReleasePlans(projectId, featureId, environmentId); | ||||
|     const { addReleasePlanToFeature } = useReleasePlansApi(); | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
| 
 | ||||
| @ -75,6 +77,7 @@ export const FeatureReleasePlanCard = ({ | ||||
|                 type: 'success', | ||||
|                 title: 'Release plan added', | ||||
|             }); | ||||
|             refetch(); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|  | ||||
| @ -21,6 +21,10 @@ import usePagination from 'hooks/usePagination'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { ReleasePlan } from '../../../ReleasePlan/ReleasePlan'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; | ||||
| 
 | ||||
| interface IEnvironmentAccordionBodyProps { | ||||
|     isDisabled: boolean; | ||||
| @ -40,6 +44,14 @@ const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledBadge = styled(Badge)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.primary.light, | ||||
|     border: 'none', | ||||
|     padding: theme.spacing(0.75, 1.5), | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     color: theme.palette.common.white, | ||||
| })); | ||||
| 
 | ||||
| const EnvironmentAccordionBody = ({ | ||||
|     featureEnvironment, | ||||
|     isDisabled, | ||||
| @ -58,6 +70,11 @@ const EnvironmentAccordionBody = ({ | ||||
|     const [strategies, setStrategies] = useState( | ||||
|         featureEnvironment?.strategies || [], | ||||
|     ); | ||||
|     const { releasePlans } = useReleasePlans( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         featureEnvironment?.name, | ||||
|     ); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const [dragItem, setDragItem] = useState<{ | ||||
| @ -201,7 +218,10 @@ const EnvironmentAccordionBody = ({ | ||||
|         <StyledAccordionBody> | ||||
|             <StyledAccordionBodyInnerContainer> | ||||
|                 <ConditionallyRender | ||||
|                     condition={strategies.length > 0 && isDisabled} | ||||
|                     condition={ | ||||
|                         (releasePlans.length > 0 || strategies.length > 0) && | ||||
|                         isDisabled | ||||
|                     } | ||||
|                     show={() => ( | ||||
|                         <Alert severity='warning' sx={{ mb: 2 }}> | ||||
|                             This environment is disabled, which means that none | ||||
| @ -210,74 +230,97 @@ const EnvironmentAccordionBody = ({ | ||||
|                     )} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={strategies.length > 0} | ||||
|                     condition={releasePlans.length > 0 || strategies.length > 0} | ||||
|                     show={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={ | ||||
|                                 strategies.length < 50 || | ||||
|                                 !manyStrategiesPagination | ||||
|                             } | ||||
|                             show={ | ||||
|                                 <> | ||||
|                                     {strategies.map((strategy, index) => ( | ||||
|                                         <StrategyDraggableItem | ||||
|                                             key={strategy.id} | ||||
|                                             strategy={strategy} | ||||
|                                             index={index} | ||||
|                                             environmentName={ | ||||
|                                                 featureEnvironment.name | ||||
|                         <> | ||||
|                             {releasePlans.map((plan) => ( | ||||
|                                 <ReleasePlan key={plan.id} plan={plan} /> | ||||
|                             ))} | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     releasePlans.length > 0 && | ||||
|                                     strategies.length > 0 | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <SectionSeparator> | ||||
|                                         <StyledBadge>OR</StyledBadge> | ||||
|                                     </SectionSeparator> | ||||
|                                 } | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     strategies.length < 50 || | ||||
|                                     !manyStrategiesPagination | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         {strategies.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={index} | ||||
|                                                 environmentName={ | ||||
|                                                     featureEnvironment.name | ||||
|                                                 } | ||||
|                                                 otherEnvironments={ | ||||
|                                                     otherEnvironments | ||||
|                                                 } | ||||
|                                                 isDragging={ | ||||
|                                                     dragItem?.id === strategy.id | ||||
|                                                 } | ||||
|                                                 onDragStartRef={onDragStartRef} | ||||
|                                                 onDragOver={onDragOver( | ||||
|                                                     strategy.id, | ||||
|                                                 )} | ||||
|                                                 onDragEnd={onDragEnd} | ||||
|                                             /> | ||||
|                                         ))} | ||||
|                                     </> | ||||
|                                 } | ||||
|                                 elseShow={ | ||||
|                                     <> | ||||
|                                         <Alert severity='error'> | ||||
|                                             We noticed you're using a high | ||||
|                                             number of activation strategies. To | ||||
|                                             ensure a more targeted approach, | ||||
|                                             consider leveraging constraints or | ||||
|                                             segments. | ||||
|                                         </Alert> | ||||
|                                         <br /> | ||||
|                                         {page.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={ | ||||
|                                                     index + pageIndex * pageSize | ||||
|                                                 } | ||||
|                                                 environmentName={ | ||||
|                                                     featureEnvironment.name | ||||
|                                                 } | ||||
|                                                 otherEnvironments={ | ||||
|                                                     otherEnvironments | ||||
|                                                 } | ||||
|                                                 isDragging={false} | ||||
|                                                 onDragStartRef={ | ||||
|                                                     (() => {}) as any | ||||
|                                                 } | ||||
|                                                 onDragOver={(() => {}) as any} | ||||
|                                                 onDragEnd={(() => {}) as any} | ||||
|                                             /> | ||||
|                                         ))} | ||||
|                                         <br /> | ||||
|                                         <Pagination | ||||
|                                             count={pages.length} | ||||
|                                             shape='rounded' | ||||
|                                             page={pageIndex + 1} | ||||
|                                             onChange={(_, page) => | ||||
|                                                 setPageIndex(page - 1) | ||||
|                                             } | ||||
|                                             otherEnvironments={ | ||||
|                                                 otherEnvironments | ||||
|                                             } | ||||
|                                             isDragging={ | ||||
|                                                 dragItem?.id === strategy.id | ||||
|                                             } | ||||
|                                             onDragStartRef={onDragStartRef} | ||||
|                                             onDragOver={onDragOver(strategy.id)} | ||||
|                                             onDragEnd={onDragEnd} | ||||
|                                         /> | ||||
|                                     ))} | ||||
|                                 </> | ||||
|                             } | ||||
|                             elseShow={ | ||||
|                                 <> | ||||
|                                     <Alert severity='error'> | ||||
|                                         We noticed you're using a high number of | ||||
|                                         activation strategies. To ensure a more | ||||
|                                         targeted approach, consider leveraging | ||||
|                                         constraints or segments. | ||||
|                                     </Alert> | ||||
|                                     <br /> | ||||
|                                     {page.map((strategy, index) => ( | ||||
|                                         <StrategyDraggableItem | ||||
|                                             key={strategy.id} | ||||
|                                             strategy={strategy} | ||||
|                                             index={index + pageIndex * pageSize} | ||||
|                                             environmentName={ | ||||
|                                                 featureEnvironment.name | ||||
|                                             } | ||||
|                                             otherEnvironments={ | ||||
|                                                 otherEnvironments | ||||
|                                             } | ||||
|                                             isDragging={false} | ||||
|                                             onDragStartRef={(() => {}) as any} | ||||
|                                             onDragOver={(() => {}) as any} | ||||
|                                             onDragEnd={(() => {}) as any} | ||||
|                                         /> | ||||
|                                     ))} | ||||
|                                     <br /> | ||||
|                                     <Pagination | ||||
|                                         count={pages.length} | ||||
|                                         shape='rounded' | ||||
|                                         page={pageIndex + 1} | ||||
|                                         onChange={(_, page) => | ||||
|                                             setPageIndex(page - 1) | ||||
|                                         } | ||||
|                                     /> | ||||
|                                 </> | ||||
|                             } | ||||
|                         /> | ||||
|                                     </> | ||||
|                                 } | ||||
|                             /> | ||||
|                         </> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <FeatureStrategyEmpty | ||||
|  | ||||
| @ -1,6 +1,12 @@ | ||||
| import type { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle'; | ||||
| import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats'; | ||||
| import { SectionSeparator } from '../SectionSeparator/SectionSeparator'; | ||||
| import { styled } from '@mui/material'; | ||||
| 
 | ||||
| const StyledLabel = styled('span')(({ theme }) => ({ | ||||
|     background: theme.palette.envAccordion.expanded, | ||||
|     padding: theme.spacing(0, 2), | ||||
| })); | ||||
| 
 | ||||
| interface IEnvironmentFooterProps { | ||||
|     environmentMetric?: IFeatureEnvironmentMetrics; | ||||
| @ -15,7 +21,9 @@ export const EnvironmentFooter = ({ | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <SectionSeparator>Feature flag exposure</SectionSeparator> | ||||
|             <SectionSeparator> | ||||
|                 <StyledLabel>Feature flag exposure</StyledLabel> | ||||
|             </SectionSeparator> | ||||
| 
 | ||||
|             <div> | ||||
|                 <FeatureMetricsStats | ||||
|  | ||||
| @ -23,7 +23,6 @@ const SeparatorContent = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
|     textAlign: 'center', | ||||
|     padding: '0 1rem', | ||||
|     background: theme.palette.envAccordion.expanded, | ||||
|     position: 'relative', | ||||
|     maxWidth: '80%', | ||||
|     color: theme.palette.text.primary, | ||||
|  | ||||
| @ -22,6 +22,10 @@ import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import isEqual from 'lodash/isEqual'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { ReleasePlan } from '../ReleasePlan/ReleasePlan'; | ||||
| import { SectionSeparator } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| 
 | ||||
| interface IEnvironmentAccordionBodyProps { | ||||
|     isDisabled: boolean; | ||||
| @ -41,6 +45,14 @@ const StyledAccordionBodyInnerContainer = styled('div')(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledBadge = styled(Badge)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.primary.light, | ||||
|     border: 'none', | ||||
|     padding: theme.spacing(0.75, 1.5), | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     color: theme.palette.common.white, | ||||
| })); | ||||
| 
 | ||||
| export const FeatureOverviewEnvironmentBody = ({ | ||||
|     featureEnvironment, | ||||
|     isDisabled, | ||||
| @ -59,6 +71,11 @@ export const FeatureOverviewEnvironmentBody = ({ | ||||
|     const [strategies, setStrategies] = useState( | ||||
|         featureEnvironment?.strategies || [], | ||||
|     ); | ||||
|     const { releasePlans } = useReleasePlans( | ||||
|         projectId, | ||||
|         featureId, | ||||
|         featureEnvironment?.name, | ||||
|     ); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const [dragItem, setDragItem] = useState<{ | ||||
| @ -215,7 +232,11 @@ export const FeatureOverviewEnvironmentBody = ({ | ||||
|         <StyledAccordionBody> | ||||
|             <StyledAccordionBodyInnerContainer> | ||||
|                 <ConditionallyRender | ||||
|                     condition={strategiesToDisplay.length > 0 && isDisabled} | ||||
|                     condition={ | ||||
|                         (releasePlans.length > 0 || | ||||
|                             strategiesToDisplay.length > 0) && | ||||
|                         isDisabled | ||||
|                     } | ||||
|                     show={() => ( | ||||
|                         <Alert severity='warning' sx={{ mb: 2 }}> | ||||
|                             This environment is disabled, which means that none | ||||
| @ -224,78 +245,105 @@ export const FeatureOverviewEnvironmentBody = ({ | ||||
|                     )} | ||||
|                 /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={strategiesToDisplay.length > 0} | ||||
|                     condition={ | ||||
|                         releasePlans.length > 0 || | ||||
|                         strategiesToDisplay.length > 0 | ||||
|                     } | ||||
|                     show={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={ | ||||
|                                 strategiesToDisplay.length < 50 || | ||||
|                                 !manyStrategiesPagination | ||||
|                             } | ||||
|                             show={ | ||||
|                                 <> | ||||
|                                     {strategiesToDisplay.map( | ||||
|                                         (strategy, index) => ( | ||||
|                         <> | ||||
|                             {releasePlans.map((plan) => ( | ||||
|                                 <ReleasePlan key={plan.id} plan={plan} /> | ||||
|                             ))} | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     releasePlans.length > 0 && | ||||
|                                     strategies.length > 0 | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <SectionSeparator> | ||||
|                                         <StyledBadge>OR</StyledBadge> | ||||
|                                     </SectionSeparator> | ||||
|                                 } | ||||
|                             /> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={ | ||||
|                                     strategiesToDisplay.length < 50 || | ||||
|                                     !manyStrategiesPagination | ||||
|                                 } | ||||
|                                 show={ | ||||
|                                     <> | ||||
|                                         {strategiesToDisplay.map( | ||||
|                                             (strategy, index) => ( | ||||
|                                                 <StrategyDraggableItem | ||||
|                                                     key={strategy.id} | ||||
|                                                     strategy={strategy} | ||||
|                                                     index={index} | ||||
|                                                     environmentName={ | ||||
|                                                         featureEnvironment.name | ||||
|                                                     } | ||||
|                                                     otherEnvironments={ | ||||
|                                                         otherEnvironments | ||||
|                                                     } | ||||
|                                                     isDragging={ | ||||
|                                                         dragItem?.id === | ||||
|                                                         strategy.id | ||||
|                                                     } | ||||
|                                                     onDragStartRef={ | ||||
|                                                         onDragStartRef | ||||
|                                                     } | ||||
|                                                     onDragOver={onDragOver( | ||||
|                                                         strategy.id, | ||||
|                                                     )} | ||||
|                                                     onDragEnd={onDragEnd} | ||||
|                                                 /> | ||||
|                                             ), | ||||
|                                         )} | ||||
|                                     </> | ||||
|                                 } | ||||
|                                 elseShow={ | ||||
|                                     <> | ||||
|                                         <Alert severity='error'> | ||||
|                                             We noticed you're using a high | ||||
|                                             number of activation strategies. To | ||||
|                                             ensure a more targeted approach, | ||||
|                                             consider leveraging constraints or | ||||
|                                             segments. | ||||
|                                         </Alert> | ||||
|                                         <br /> | ||||
|                                         {page.map((strategy, index) => ( | ||||
|                                             <StrategyDraggableItem | ||||
|                                                 key={strategy.id} | ||||
|                                                 strategy={strategy} | ||||
|                                                 index={index} | ||||
|                                                 index={ | ||||
|                                                     index + pageIndex * pageSize | ||||
|                                                 } | ||||
|                                                 environmentName={ | ||||
|                                                     featureEnvironment.name | ||||
|                                                 } | ||||
|                                                 otherEnvironments={ | ||||
|                                                     otherEnvironments | ||||
|                                                 } | ||||
|                                                 isDragging={ | ||||
|                                                     dragItem?.id === strategy.id | ||||
|                                                 isDragging={false} | ||||
|                                                 onDragStartRef={ | ||||
|                                                     (() => {}) as any | ||||
|                                                 } | ||||
|                                                 onDragStartRef={onDragStartRef} | ||||
|                                                 onDragOver={onDragOver( | ||||
|                                                     strategy.id, | ||||
|                                                 )} | ||||
|                                                 onDragEnd={onDragEnd} | ||||
|                                                 onDragOver={(() => {}) as any} | ||||
|                                                 onDragEnd={(() => {}) as any} | ||||
|                                             /> | ||||
|                                         ), | ||||
|                                     )} | ||||
|                                 </> | ||||
|                             } | ||||
|                             elseShow={ | ||||
|                                 <> | ||||
|                                     <Alert severity='error'> | ||||
|                                         We noticed you're using a high number of | ||||
|                                         activation strategies. To ensure a more | ||||
|                                         targeted approach, consider leveraging | ||||
|                                         constraints or segments. | ||||
|                                     </Alert> | ||||
|                                     <br /> | ||||
|                                     {page.map((strategy, index) => ( | ||||
|                                         <StrategyDraggableItem | ||||
|                                             key={strategy.id} | ||||
|                                             strategy={strategy} | ||||
|                                             index={index + pageIndex * pageSize} | ||||
|                                             environmentName={ | ||||
|                                                 featureEnvironment.name | ||||
|                                         ))} | ||||
|                                         <br /> | ||||
|                                         <Pagination | ||||
|                                             count={pages.length} | ||||
|                                             shape='rounded' | ||||
|                                             page={pageIndex + 1} | ||||
|                                             onChange={(_, page) => | ||||
|                                                 setPageIndex(page - 1) | ||||
|                                             } | ||||
|                                             otherEnvironments={ | ||||
|                                                 otherEnvironments | ||||
|                                             } | ||||
|                                             isDragging={false} | ||||
|                                             onDragStartRef={(() => {}) as any} | ||||
|                                             onDragOver={(() => {}) as any} | ||||
|                                             onDragEnd={(() => {}) as any} | ||||
|                                         /> | ||||
|                                     ))} | ||||
|                                     <br /> | ||||
|                                     <Pagination | ||||
|                                         count={pages.length} | ||||
|                                         shape='rounded' | ||||
|                                         page={pageIndex + 1} | ||||
|                                         onChange={(_, page) => | ||||
|                                             setPageIndex(page - 1) | ||||
|                                         } | ||||
|                                     /> | ||||
|                                 </> | ||||
|                             } | ||||
|                         /> | ||||
|                                     </> | ||||
|                                 } | ||||
|                             /> | ||||
|                         </> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <FeatureStrategyEmpty | ||||
|  | ||||
| @ -0,0 +1,193 @@ | ||||
| import Delete from '@mui/icons-material/Delete'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi'; | ||||
| import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import type { | ||||
|     IReleasePlan, | ||||
|     IReleasePlanMilestone, | ||||
| } from 'interfaces/releasePlans'; | ||||
| import { useState } from 'react'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog'; | ||||
| import { ReleasePlanMilestone } from './ReleasePlanMilestone'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| const StyledContainer = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'disabled', | ||||
| })<{ disabled?: boolean }>(({ theme, disabled }) => ({ | ||||
|     padding: theme.spacing(2), | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: disabled | ||||
|         ? theme.palette.envAccordion.disabled | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'disabled', | ||||
| })<{ disabled?: boolean }>(({ theme, disabled }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     color: disabled ? theme.palette.text.secondary : theme.palette.text.primary, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     justifyContent: 'center', | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderTitleLabel = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     lineHeight: 0.5, | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeaderDescription = styled('span')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
|     lineHeight: 0.5, | ||||
|     color: theme.palette.text.secondary, | ||||
| })); | ||||
| 
 | ||||
| const StyledBody = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     marginTop: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledConnection = styled('div')(({ theme }) => ({ | ||||
|     width: 4, | ||||
|     height: theme.spacing(2), | ||||
|     backgroundColor: theme.palette.divider, | ||||
|     marginLeft: theme.spacing(3.25), | ||||
| })); | ||||
| 
 | ||||
| interface IReleasePlanProps { | ||||
|     plan: IReleasePlan; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlan = ({ plan }: IReleasePlanProps) => { | ||||
|     const { | ||||
|         id, | ||||
|         name, | ||||
|         description, | ||||
|         activeMilestoneId, | ||||
|         featureName, | ||||
|         environment, | ||||
|         milestones, | ||||
|     } = plan; | ||||
| 
 | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { refetch } = useReleasePlans(projectId, featureName, environment); | ||||
|     const { removeReleasePlanFromFeature, startReleasePlanMilestone } = | ||||
|         useReleasePlansApi(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| 
 | ||||
|     const [removeOpen, setRemoveOpen] = useState(false); | ||||
| 
 | ||||
|     const onRemoveConfirm = async () => { | ||||
|         try { | ||||
|             await removeReleasePlanFromFeature( | ||||
|                 projectId, | ||||
|                 featureName, | ||||
|                 environment, | ||||
|                 id, | ||||
|             ); | ||||
|             setToastData({ | ||||
|                 title: `Release plan "${name}" has been removed from ${featureName} in ${environment}`, | ||||
|                 type: 'success', | ||||
|             }); | ||||
|             refetch(); | ||||
|             setRemoveOpen(false); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onStartMilestone = async (milestone: IReleasePlanMilestone) => { | ||||
|         try { | ||||
|             await startReleasePlanMilestone( | ||||
|                 projectId, | ||||
|                 featureName, | ||||
|                 environment, | ||||
|                 id, | ||||
|                 milestone.id, | ||||
|             ); | ||||
|             setToastData({ | ||||
|                 title: `Milestone "${milestone.name}" has started`, | ||||
|                 type: 'success', | ||||
|             }); | ||||
|             refetch(); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const disabled = !activeMilestoneId; | ||||
|     const activeIndex = milestones.findIndex( | ||||
|         (milestone) => milestone.id === activeMilestoneId, | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer disabled={disabled}> | ||||
|             <StyledHeader disabled={disabled}> | ||||
|                 <StyledHeaderTitleContainer> | ||||
|                     <StyledHeaderTitleLabel> | ||||
|                         Release plan | ||||
|                     </StyledHeaderTitleLabel> | ||||
|                     <span>{name}</span> | ||||
|                     <StyledHeaderDescription> | ||||
|                         {description} | ||||
|                     </StyledHeaderDescription> | ||||
|                 </StyledHeaderTitleContainer> | ||||
|                 <PermissionIconButton | ||||
|                     onClick={() => setRemoveOpen(true)} | ||||
|                     permission={DELETE_FEATURE_STRATEGY} | ||||
|                     environmentId={environment} | ||||
|                     projectId={projectId} | ||||
|                     tooltipProps={{ | ||||
|                         title: 'Remove release plan', | ||||
|                     }} | ||||
|                 > | ||||
|                     <Delete /> | ||||
|                 </PermissionIconButton> | ||||
|             </StyledHeader> | ||||
|             <StyledBody> | ||||
|                 {milestones.map((milestone, index) => ( | ||||
|                     <div key={milestone.id}> | ||||
|                         <ReleasePlanMilestone | ||||
|                             milestone={milestone} | ||||
|                             status={ | ||||
|                                 milestone.id === activeMilestoneId | ||||
|                                     ? 'active' | ||||
|                                     : index < activeIndex | ||||
|                                       ? 'completed' | ||||
|                                       : 'not-started' | ||||
|                             } | ||||
|                             onStartMilestone={onStartMilestone} | ||||
|                         /> | ||||
|                         <ConditionallyRender | ||||
|                             condition={index < milestones.length - 1} | ||||
|                             show={<StyledConnection />} | ||||
|                         /> | ||||
|                     </div> | ||||
|                 ))} | ||||
|             </StyledBody> | ||||
|             <ReleasePlanRemoveDialog | ||||
|                 plan={plan} | ||||
|                 open={removeOpen} | ||||
|                 setOpen={setRemoveOpen} | ||||
|                 onConfirm={onRemoveConfirm} | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,146 @@ | ||||
| import ExpandMore from '@mui/icons-material/ExpandMore'; | ||||
| import { | ||||
|     Accordion, | ||||
|     AccordionDetails, | ||||
|     AccordionSummary, | ||||
|     Link, | ||||
|     styled, | ||||
| } from '@mui/material'; | ||||
| import PlayCircleIcon from '@mui/icons-material/PlayCircle'; | ||||
| import TripOriginIcon from '@mui/icons-material/TripOrigin'; | ||||
| import type { IReleasePlanMilestone } from 'interfaces/releasePlans'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ReleasePlanMilestoneStrategy } from './ReleasePlanMilestoneStrategy'; | ||||
| import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; | ||||
| 
 | ||||
| type MilestoneStatus = 'not-started' | 'active' | 'completed'; | ||||
| 
 | ||||
| const StyledAccordion = styled(Accordion, { | ||||
|     shouldForwardProp: (prop) => prop !== 'status', | ||||
| })<{ status: MilestoneStatus }>(({ theme, status }) => ({ | ||||
|     border: `1px solid ${status === 'active' ? theme.palette.success.border : theme.palette.divider}`, | ||||
|     boxShadow: 'none', | ||||
|     margin: 0, | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     '&:before': { | ||||
|         display: 'none', | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledAccordionSummary = styled(AccordionSummary)({ | ||||
|     '& .MuiAccordionSummary-content': { | ||||
|         justifyContent: 'space-between', | ||||
|         alignItems: 'center', | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| const StyledTitleContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'start', | ||||
|     flexDirection: 'column', | ||||
|     gap: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledTitle = styled('span')(({ theme }) => ({ | ||||
|     fontWeight: theme.fontWeight.bold, | ||||
| })); | ||||
| 
 | ||||
| const StyledStatus = styled('div', { | ||||
|     shouldForwardProp: (prop) => prop !== 'status', | ||||
| })<{ status: MilestoneStatus }>(({ theme, status }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     gap: theme.spacing(1), | ||||
|     paddingRight: theme.spacing(1), | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     backgroundColor: | ||||
|         status === 'active' ? theme.palette.success.light : 'transparent', | ||||
|     color: | ||||
|         status === 'active' | ||||
|             ? theme.palette.success.contrastText | ||||
|             : status === 'completed' | ||||
|               ? theme.palette.text.secondary | ||||
|               : theme.palette.text.primary, | ||||
|     '& svg': { | ||||
|         color: | ||||
|             status === 'active' | ||||
|                 ? theme.palette.success.main | ||||
|                 : status === 'completed' | ||||
|                   ? theme.palette.neutral.border | ||||
|                   : theme.palette.primary.main, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledSecondaryLabel = styled('span')(({ theme }) => ({ | ||||
|     color: theme.palette.text.secondary, | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.envAccordion.expanded, | ||||
|     borderBottomLeftRadius: theme.shape.borderRadiusLarge, | ||||
|     borderBottomRightRadius: theme.shape.borderRadiusLarge, | ||||
| })); | ||||
| 
 | ||||
| interface IReleasePlanMilestoneProps { | ||||
|     milestone: IReleasePlanMilestone; | ||||
|     status: MilestoneStatus; | ||||
|     onStartMilestone: (milestone: IReleasePlanMilestone) => void; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlanMilestone = ({ | ||||
|     milestone, | ||||
|     status, | ||||
|     onStartMilestone, | ||||
| }: IReleasePlanMilestoneProps) => { | ||||
|     const statusText = | ||||
|         status === 'active' | ||||
|             ? 'Running' | ||||
|             : status === 'completed' | ||||
|               ? 'Restart' | ||||
|               : 'Start'; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledAccordion status={status}> | ||||
|             <StyledAccordionSummary expandIcon={<ExpandMore />}> | ||||
|                 <StyledTitleContainer> | ||||
|                     <StyledTitle>{milestone.name}</StyledTitle> | ||||
|                     <StyledStatus status={status}> | ||||
|                         <ConditionallyRender | ||||
|                             condition={status === 'active'} | ||||
|                             show={<TripOriginIcon />} | ||||
|                             elseShow={<PlayCircleIcon />} | ||||
|                         /> | ||||
|                         <ConditionallyRender | ||||
|                             condition={status === 'active'} | ||||
|                             show={<span>{statusText}</span>} | ||||
|                             elseShow={ | ||||
|                                 <Link | ||||
|                                     onClick={(e) => { | ||||
|                                         e.stopPropagation(); | ||||
|                                         onStartMilestone(milestone); | ||||
|                                     }} | ||||
|                                 > | ||||
|                                     {statusText} | ||||
|                                 </Link> | ||||
|                             } | ||||
|                         /> | ||||
|                     </StyledStatus> | ||||
|                 </StyledTitleContainer> | ||||
|                 <StyledSecondaryLabel>View strategies</StyledSecondaryLabel> | ||||
|             </StyledAccordionSummary> | ||||
|             <StyledAccordionDetails> | ||||
|                 {milestone.strategies.map((strategy, index) => ( | ||||
|                     <div key={strategy.id}> | ||||
|                         <ConditionallyRender | ||||
|                             condition={index > 0} | ||||
|                             show={<StrategySeparator text='OR' />} | ||||
|                         /> | ||||
|                         <ReleasePlanMilestoneStrategy strategy={strategy} /> | ||||
|                     </div> | ||||
|                 ))} | ||||
|             </StyledAccordionDetails> | ||||
|         </StyledAccordion> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,55 @@ | ||||
| import { Box, styled } from '@mui/material'; | ||||
| import { StrategyExecution } from '../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; | ||||
| import SplitPreviewSlider from 'component/feature/StrategyTypes/SplitPreviewSlider/SplitPreviewSlider'; | ||||
| import { | ||||
|     formatStrategyName, | ||||
|     getFeatureStrategyIcon, | ||||
| } from 'utils/strategyNames'; | ||||
| import type { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| 
 | ||||
| const StyledStrategy = styled('div')(({ theme }) => ({ | ||||
|     background: theme.palette.background.paper, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     padding: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     gap: theme.spacing(1), | ||||
|     alignItems: 'center', | ||||
|     color: theme.palette.text.primary, | ||||
|     '& > svg': { | ||||
|         fill: theme.palette.action.disabled, | ||||
|     }, | ||||
|     marginBottom: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| interface IReleasePlanMilestoneStrategyProps { | ||||
|     strategy: IFeatureStrategy; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlanMilestoneStrategy = ({ | ||||
|     strategy, | ||||
| }: IReleasePlanMilestoneStrategyProps) => { | ||||
|     const Icon = getFeatureStrategyIcon(strategy.strategyName); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledStrategy> | ||||
|             <StyledHeader> | ||||
|                 <Icon /> | ||||
|                 {`${formatStrategyName(String(strategy.strategyName))}${strategy.title ? `: ${strategy.title}` : ''}`} | ||||
|             </StyledHeader> | ||||
|             <StrategyExecution strategy={strategy} /> | ||||
|             {strategy.variants && | ||||
|                 strategy.variants.length > 0 && | ||||
|                 (strategy.disabled ? ( | ||||
|                     <Box sx={{ opacity: '0.5' }}> | ||||
|                         <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                     </Box> | ||||
|                 ) : ( | ||||
|                     <SplitPreviewSlider variants={strategy.variants} /> | ||||
|                 ))} | ||||
|         </StyledStrategy> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,43 @@ | ||||
| import { Alert } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import type { IReleasePlan } from 'interfaces/releasePlans'; | ||||
| 
 | ||||
| interface IReleasePlanRemoveDialogProps { | ||||
|     plan: IReleasePlan; | ||||
|     open: boolean; | ||||
|     setOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     onConfirm: () => void; | ||||
| } | ||||
| 
 | ||||
| export const ReleasePlanRemoveDialog = ({ | ||||
|     plan, | ||||
|     open, | ||||
|     setOpen, | ||||
|     onConfirm, | ||||
| }: IReleasePlanRemoveDialogProps) => ( | ||||
|     <Dialogue | ||||
|         title='Remove release plan?' | ||||
|         open={open} | ||||
|         primaryButtonText='Remove release plan' | ||||
|         secondaryButtonText='Cancel' | ||||
|         onClick={onConfirm} | ||||
|         onClose={() => setOpen(false)} | ||||
|     > | ||||
|         <ConditionallyRender | ||||
|             condition={Boolean(plan.activeMilestoneId)} | ||||
|             show={ | ||||
|                 <Alert severity='error' sx={{ mb: 2 }}> | ||||
|                     This release plan currently has one active milestone. | ||||
|                     Removing the release plan will change which users receive | ||||
|                     access to the feature. | ||||
|                 </Alert> | ||||
|             } | ||||
|         /> | ||||
|         <p> | ||||
|             You are about to remove release plan <strong>{plan.name}</strong>{' '} | ||||
|             from <strong>{plan.featureName}</strong> in{' '} | ||||
|             <strong>{plan.environment}</strong> | ||||
|         </p> | ||||
|     </Dialogue> | ||||
| ); | ||||
| @ -58,7 +58,7 @@ interface IMilestoneCardProps { | ||||
|     milestoneNameChanged: (milestoneId: string, name: string) => void; | ||||
|     showAddStrategyDialog: ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     clearErrors: () => void; | ||||
| @ -84,7 +84,7 @@ export const MilestoneCard = ({ | ||||
| 
 | ||||
|     const onSelectStrategy = ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => { | ||||
|         showAddStrategyDialog(milestone.id, strategy); | ||||
|     }; | ||||
|  | ||||
| @ -15,7 +15,7 @@ interface IMilestoneListProps { | ||||
|     >; | ||||
|     openAddStrategyForm: ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => void; | ||||
|     errors: { [key: string]: string }; | ||||
|     clearErrors: () => void; | ||||
|  | ||||
| @ -51,7 +51,9 @@ const StyledCard = styled('div')(({ theme }) => ({ | ||||
| 
 | ||||
| interface IMilestoneStrategyMenuCardProps { | ||||
|     strategy: IStrategy; | ||||
|     onClick: (strategy: IReleasePlanMilestoneStrategy) => void; | ||||
|     onClick: ( | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => void; | ||||
| } | ||||
| 
 | ||||
| export const MilestoneStrategyMenuCard = ({ | ||||
|  | ||||
| @ -12,7 +12,7 @@ interface IMilestoneStrategyMenuCardsProps { | ||||
|     milestoneId: string; | ||||
|     openAddStrategy: ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => void; | ||||
| } | ||||
| 
 | ||||
| @ -26,7 +26,9 @@ export const MilestoneStrategyMenuCards = ({ | ||||
|         (strategy) => !strategy.deprecated && !strategy.editable, | ||||
|     ); | ||||
| 
 | ||||
|     const onClick = (strategy: IReleasePlanMilestoneStrategy) => { | ||||
|     const onClick = ( | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => { | ||||
|         openAddStrategy(milestoneId, strategy); | ||||
|     }; | ||||
| 
 | ||||
|  | ||||
| @ -66,10 +66,10 @@ const StyledTargetingHeader = styled('div')(({ theme }) => ({ | ||||
| interface IReleasePlanTemplateAddStrategyFormProps { | ||||
|     milestoneId: string | undefined; | ||||
|     onCancel: () => void; | ||||
|     strategy: IReleasePlanMilestoneStrategy; | ||||
|     strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>; | ||||
|     onAddStrategy: ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => void; | ||||
| } | ||||
| 
 | ||||
| @ -135,7 +135,7 @@ export const ReleasePlanTemplateAddStrategyForm = ({ | ||||
|                 {activeTab === 0 && ( | ||||
|                     <> | ||||
|                         <MilestoneStrategyTitle | ||||
|                             title={addStrategy.title} | ||||
|                             title={addStrategy.title || ''} | ||||
|                             setTitle={(title) => | ||||
|                                 updateParameter('title', title) | ||||
|                             } | ||||
|  | ||||
| @ -62,7 +62,9 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({ | ||||
|     const [activeMilestoneId, setActiveMilestoneId] = useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
|     const [strategy, setStrategy] = useState<IReleasePlanMilestoneStrategy>({ | ||||
|     const [strategy, setStrategy] = useState< | ||||
|         Omit<IReleasePlanMilestoneStrategy, 'milestoneId'> | ||||
|     >({ | ||||
|         name: 'flexibleRollout', | ||||
|         parameters: { rollout: '50' }, | ||||
|         constraints: [], | ||||
| @ -71,7 +73,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({ | ||||
|     }); | ||||
|     const openAddStrategyForm = ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => { | ||||
|         setActiveMilestoneId(milestoneId); | ||||
|         setStrategy(strategy); | ||||
| @ -80,7 +82,7 @@ export const TemplateForm: React.FC<ITemplateFormProps> = ({ | ||||
| 
 | ||||
|     const addStrategy = ( | ||||
|         milestoneId: string, | ||||
|         strategy: IReleasePlanMilestoneStrategy, | ||||
|         strategy: Omit<IReleasePlanMilestoneStrategy, 'milestoneId'>, | ||||
|     ) => { | ||||
|         setMilestones((prev) => | ||||
|             prev.map((milestone, i) => | ||||
|  | ||||
| @ -25,7 +25,36 @@ export const useReleasePlansApi = () => { | ||||
|         await makeRequest(req.caller, req.id); | ||||
|     }; | ||||
| 
 | ||||
|     const removeReleasePlanFromFeature = async ( | ||||
|         projectId: string, | ||||
|         featureName: string, | ||||
|         environment: string, | ||||
|         releasePlanId: string, | ||||
|     ): Promise<void> => { | ||||
|         const requestId = 'removeReleasePlanFromFeature'; | ||||
|         const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${releasePlanId}`; | ||||
|         const req = createRequest(path, { method: 'DELETE' }, requestId); | ||||
| 
 | ||||
|         await makeRequest(req.caller, req.id); | ||||
|     }; | ||||
| 
 | ||||
|     const startReleasePlanMilestone = async ( | ||||
|         projectId: string, | ||||
|         featureName: string, | ||||
|         environment: string, | ||||
|         releasePlanId: string, | ||||
|         milestoneId: string, | ||||
|     ): Promise<void> => { | ||||
|         const requestId = 'startReleasePlanMilestone'; | ||||
|         const path = `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans/${releasePlanId}/milestones/${milestoneId}/start`; | ||||
|         const req = createRequest(path, { method: 'POST' }, requestId); | ||||
| 
 | ||||
|         await makeRequest(req.caller, req.id); | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         addReleasePlanToFeature, | ||||
|         removeReleasePlanFromFeature, | ||||
|         startReleasePlanMilestone, | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,43 @@ | ||||
| import { useMemo } from 'react'; | ||||
| import useUiConfig from '../useUiConfig/useUiConfig'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | ||||
| import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import type { IReleasePlan } from 'interfaces/releasePlans'; | ||||
| 
 | ||||
| const DEFAULT_DATA: IReleasePlan[] = []; | ||||
| 
 | ||||
| export const useReleasePlans = ( | ||||
|     projectId: string, | ||||
|     featureName: string, | ||||
|     environment?: string, | ||||
| ) => { | ||||
|     const { isEnterprise } = useUiConfig(); | ||||
|     const releasePlansEnabled = useUiFlag('releasePlans'); | ||||
| 
 | ||||
|     const { data, error, mutate } = useConditionalSWR<IReleasePlan[]>( | ||||
|         isEnterprise() && releasePlansEnabled && Boolean(environment), | ||||
|         DEFAULT_DATA, | ||||
|         formatApiPath( | ||||
|             `api/admin/projects/${projectId}/features/${featureName}/environments/${environment}/release_plans`, | ||||
|         ), | ||||
|         fetcher, | ||||
|     ); | ||||
| 
 | ||||
|     return useMemo( | ||||
|         () => ({ | ||||
|             releasePlans: data ?? [], | ||||
|             loading: !error && !data, | ||||
|             refetch: () => mutate(), | ||||
|             error, | ||||
|         }), | ||||
|         [data, error, mutate], | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const fetcher = (path: string) => { | ||||
|     return fetch(path) | ||||
|         .then(handleErrorResponses('Release plans')) | ||||
|         .then((res) => res.json()); | ||||
| }; | ||||
| @ -1,5 +1,4 @@ | ||||
| import type { IFeatureVariant } from './featureToggle'; | ||||
| import type { IConstraint, IFeatureStrategyParameters } from './strategy'; | ||||
| import type { IFeatureStrategy } from './strategy'; | ||||
| 
 | ||||
| export interface IReleasePlanTemplate { | ||||
|     id: string; | ||||
| @ -18,19 +17,27 @@ export interface IReleasePlanTemplate { | ||||
|     milestones: IReleasePlanMilestonePayload[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanMilestoneStrategy { | ||||
| export interface IReleasePlan { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     title: string; | ||||
|     disabled?: boolean; | ||||
|     constraints: IConstraint[]; | ||||
|     parameters: IFeatureStrategyParameters; | ||||
|     variants?: IFeatureVariant[]; | ||||
|     description: string; | ||||
|     createdAt: string; | ||||
|     createdByUserId: number; | ||||
|     activeMilestoneId?: string; | ||||
|     featureName: string; | ||||
|     environment: string; | ||||
|     milestones: IReleasePlanMilestone[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanMilestone { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     releasePlanDefinitionId: string; | ||||
|     strategies: IReleasePlanMilestoneStrategy[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanMilestoneStrategy extends IFeatureStrategy { | ||||
|     milestoneId: string; | ||||
| } | ||||
| 
 | ||||
| export interface IReleasePlanTemplatePayload { | ||||
|  | ||||
| @ -20,7 +20,7 @@ const RolloutSvgIcon: FC = (props) => ( | ||||
|     /> | ||||
| ); | ||||
| 
 | ||||
| export const getFeatureStrategyIcon = (strategyName: string) => { | ||||
| export const getFeatureStrategyIcon = (strategyName?: string) => { | ||||
|     switch (strategyName) { | ||||
|         case 'default': | ||||
|             return PowerSettingsNewIcon; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user