mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Disable and enable strategies - frontend (#3582)
Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									1e3f652311
								
							
						
					
					
						commit
						3bb09c5ce4
					
				| @ -1,6 +1,5 @@ | ||||
| import { FC, ReactNode } from 'react'; | ||||
| import { | ||||
|     hasNameField, | ||||
|     IChange, | ||||
|     IChangeRequest, | ||||
|     IChangeRequestFeature, | ||||
| @ -8,18 +7,8 @@ import { | ||||
| import { objectId } from 'utils/objectId'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { Alert, Box, styled } from '@mui/material'; | ||||
| 
 | ||||
| import { | ||||
|     StrategyTooltipLink, | ||||
|     StrategyDiff, | ||||
| } from 'component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink'; | ||||
| import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; | ||||
| import { ToggleStatusChange } from './ToggleStatusChange'; | ||||
| import { | ||||
|     StrategyAddedChange, | ||||
|     StrategyDeletedChange, | ||||
|     StrategyEditedChange, | ||||
| } from './StrategyChange'; | ||||
| import { StrategyChange } from './StrategyChange'; | ||||
| import { VariantPatch } from './VariantPatch/VariantPatch'; | ||||
| 
 | ||||
| const StyledSingleChangeBox = styled(Box, { | ||||
| @ -74,6 +63,7 @@ export const Change: FC<{ | ||||
|     const lastIndex = feature.defaultChange | ||||
|         ? feature.changes.length + 1 | ||||
|         : feature.changes.length; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledSingleChangeBox | ||||
|             key={objectId(change)} | ||||
| @ -98,50 +88,17 @@ export const Change: FC<{ | ||||
|                         discard={discard} | ||||
|                     /> | ||||
|                 )} | ||||
|                 {change.action === 'addStrategy' && ( | ||||
|                     <> | ||||
|                         <StrategyAddedChange discard={discard}> | ||||
|                             <StrategyTooltipLink change={change}> | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     feature={feature.name} | ||||
|                                     environmentName={changeRequest.environment} | ||||
|                                     project={changeRequest.project} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                         </StrategyAddedChange> | ||||
|                         <StrategyExecution strategy={change.payload} /> | ||||
|                     </> | ||||
|                 )} | ||||
|                 {change.action === 'deleteStrategy' && ( | ||||
|                     <StrategyDeletedChange discard={discard}> | ||||
|                         {hasNameField(change.payload) && ( | ||||
|                             <StrategyTooltipLink change={change}> | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     feature={feature.name} | ||||
|                                     environmentName={changeRequest.environment} | ||||
|                                     project={changeRequest.project} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                         )} | ||||
|                     </StrategyDeletedChange> | ||||
|                 )} | ||||
|                 {change.action === 'updateStrategy' && ( | ||||
|                     <> | ||||
|                         <StrategyEditedChange discard={discard}> | ||||
|                             <StrategyTooltipLink change={change}> | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     feature={feature.name} | ||||
|                                     environmentName={changeRequest.environment} | ||||
|                                     project={changeRequest.project} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                         </StrategyEditedChange> | ||||
|                         <StrategyExecution strategy={change.payload} /> | ||||
|                     </> | ||||
|                 )} | ||||
|                 {change.action === 'addStrategy' || | ||||
|                 change.action === 'deleteStrategy' || | ||||
|                 change.action === 'updateStrategy' ? ( | ||||
|                     <StrategyChange | ||||
|                         discard={discard} | ||||
|                         change={change} | ||||
|                         featureName={feature.name} | ||||
|                         environmentName={changeRequest.environment} | ||||
|                         projectId={changeRequest.project} | ||||
|                     /> | ||||
|                 ) : null} | ||||
|                 {change.action === 'patchVariant' && ( | ||||
|                     <VariantPatch | ||||
|                         feature={feature.name} | ||||
|  | ||||
| @ -1,5 +1,21 @@ | ||||
| import { Box, styled, Typography } from '@mui/material'; | ||||
| import { FC, ReactNode } from 'react'; | ||||
| import { VFC, FC, ReactNode } from 'react'; | ||||
| import { Box, styled, Tooltip, Typography } from '@mui/material'; | ||||
| import BlockIcon from '@mui/icons-material/Block'; | ||||
| import TrackChangesIcon from '@mui/icons-material/TrackChanges'; | ||||
| import { | ||||
|     StrategyDiff, | ||||
|     StrategyTooltipLink, | ||||
| } from '../../StrategyTooltipLink/StrategyTooltipLink'; | ||||
| import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; | ||||
| import { | ||||
|     IChangeRequestAddStrategy, | ||||
|     IChangeRequestDeleteStrategy, | ||||
|     IChangeRequestUpdateStrategy, | ||||
| } from 'component/changeRequest/changeRequest.types'; | ||||
| import { useCurrentStrategy } from './hooks/useCurrentStrategy'; | ||||
| import { Badge } from 'component/common/Badge/Badge'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { flexRow } from 'themes/themeStyles'; | ||||
| 
 | ||||
| export const ChangeItemWrapper = styled(Box)({ | ||||
|     display: 'flex', | ||||
| @ -7,7 +23,7 @@ export const ChangeItemWrapper = styled(Box)({ | ||||
|     alignItems: 'center', | ||||
| }); | ||||
| 
 | ||||
| export const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({ | ||||
| const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
| @ -20,55 +36,175 @@ const ChangeItemInfo: FC = styled(Box)(({ theme }) => ({ | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const StrategyAddedChange: FC<{ discard?: ReactNode }> = ({ | ||||
|     children, | ||||
|     discard, | ||||
| }) => { | ||||
| const hasNameField = (payload: unknown): payload is { name: string } => | ||||
|     typeof payload === 'object' && payload !== null && 'name' in payload; | ||||
| 
 | ||||
| const DisabledEnabledState: VFC<{ disabled: boolean }> = ({ disabled }) => { | ||||
|     if (disabled) { | ||||
|         return ( | ||||
|             <Tooltip | ||||
|                 title="This strategy will not be taken into account when evaluating feature toggle." | ||||
|                 arrow | ||||
|                 sx={{ cursor: 'pointer' }} | ||||
|             > | ||||
|                 <Badge color="disabled" icon={<BlockIcon />}> | ||||
|                     Disabled | ||||
|                 </Badge> | ||||
|             </Tooltip> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <ChangeItemCreateEditWrapper> | ||||
|             <ChangeItemInfo> | ||||
|                 <Typography | ||||
|                     sx={theme => ({ | ||||
|                         color: theme.palette.success.dark, | ||||
|                     })} | ||||
|                 > | ||||
|                     + Adding strategy: | ||||
|                 </Typography> | ||||
|                 {children} | ||||
|             </ChangeItemInfo> | ||||
|             {discard} | ||||
|         </ChangeItemCreateEditWrapper> | ||||
|         <Tooltip | ||||
|             title="This was disabled before and with this change it will be taken into account when evaluating feature toggle." | ||||
|             arrow | ||||
|             sx={{ cursor: 'pointer' }} | ||||
|         > | ||||
|             <Badge color="success" icon={<TrackChangesIcon />}> | ||||
|                 Enabled | ||||
|             </Badge> | ||||
|         </Tooltip> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const StrategyEditedChange: FC<{ discard?: ReactNode }> = ({ | ||||
|     children, | ||||
|     discard, | ||||
| }) => { | ||||
|     return ( | ||||
|         <ChangeItemCreateEditWrapper> | ||||
|             <ChangeItemInfo> | ||||
|                 <Typography>Editing strategy:</Typography> | ||||
|                 {children} | ||||
|             </ChangeItemInfo> | ||||
|             {discard} | ||||
|         </ChangeItemCreateEditWrapper> | ||||
|     ); | ||||
| const EditHeader: VFC<{ | ||||
|     wasDisabled?: boolean; | ||||
|     willBeDisabled?: boolean; | ||||
| }> = ({ wasDisabled = false, willBeDisabled = false }) => { | ||||
|     if (wasDisabled && willBeDisabled) { | ||||
|         return ( | ||||
|             <Typography color="action.disabled"> | ||||
|                 Editing disabled strategy | ||||
|             </Typography> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if (!wasDisabled && willBeDisabled) { | ||||
|         return <Typography color="error.dark">Editing strategy</Typography>; | ||||
|     } | ||||
| 
 | ||||
|     if (wasDisabled && !willBeDisabled) { | ||||
|         return <Typography color="success.dark">Editing strategy</Typography>; | ||||
|     } | ||||
| 
 | ||||
|     return <Typography>Editing strategy:</Typography>; | ||||
| }; | ||||
| 
 | ||||
| export const StrategyDeletedChange: FC<{ discard?: ReactNode }> = ({ | ||||
|     discard, | ||||
|     children, | ||||
| }) => { | ||||
| export const StrategyChange: VFC<{ | ||||
|     discard?: ReactNode; | ||||
|     change: | ||||
|         | IChangeRequestAddStrategy | ||||
|         | IChangeRequestDeleteStrategy | ||||
|         | IChangeRequestUpdateStrategy; | ||||
|     environmentName: string; | ||||
|     featureName: string; | ||||
|     projectId: string; | ||||
| }> = ({ discard, change, featureName, environmentName, projectId }) => { | ||||
|     const currentStrategy = useCurrentStrategy( | ||||
|         change, | ||||
|         projectId, | ||||
|         featureName, | ||||
|         environmentName | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <ChangeItemWrapper> | ||||
|             <ChangeItemInfo> | ||||
|                 <Typography sx={theme => ({ color: theme.palette.error.main })}> | ||||
|                     - Deleting strategy | ||||
|                 </Typography> | ||||
|                 {children} | ||||
|             </ChangeItemInfo> | ||||
|             {discard} | ||||
|         </ChangeItemWrapper> | ||||
|         <> | ||||
|             {change.action === 'addStrategy' && ( | ||||
|                 <> | ||||
|                     <ChangeItemCreateEditWrapper> | ||||
|                         <ChangeItemInfo> | ||||
|                             <Typography | ||||
|                                 color={ | ||||
|                                     change.payload?.disabled | ||||
|                                         ? 'action.disabled' | ||||
|                                         : 'success.dark' | ||||
|                                 } | ||||
|                             > | ||||
|                                 + Adding strategy: | ||||
|                             </Typography> | ||||
|                             <StrategyTooltipLink change={change}> | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     currentStrategy={currentStrategy} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={Boolean( | ||||
|                                     change.payload?.disabled === true | ||||
|                                 )} | ||||
|                                 show={<DisabledEnabledState disabled={true} />} | ||||
|                             /> | ||||
|                         </ChangeItemInfo> | ||||
|                         {discard} | ||||
|                     </ChangeItemCreateEditWrapper> | ||||
|                     <StrategyExecution strategy={change.payload} /> | ||||
|                 </> | ||||
|             )} | ||||
|             {change.action === 'deleteStrategy' && ( | ||||
|                 <ChangeItemWrapper> | ||||
|                     <ChangeItemInfo> | ||||
|                         <Typography | ||||
|                             sx={theme => ({ color: theme.palette.error.main })} | ||||
|                         > | ||||
|                             - Deleting strategy | ||||
|                         </Typography> | ||||
|                         {hasNameField(change.payload) && ( | ||||
|                             <StrategyTooltipLink change={change}> | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     currentStrategy={currentStrategy} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                         )} | ||||
|                     </ChangeItemInfo> | ||||
|                     {discard} | ||||
|                 </ChangeItemWrapper> | ||||
|             )} | ||||
|             {change.action === 'updateStrategy' && ( | ||||
|                 <> | ||||
|                     <ChangeItemCreateEditWrapper> | ||||
|                         <ChangeItemInfo> | ||||
|                             <EditHeader | ||||
|                                 wasDisabled={currentStrategy?.disabled} | ||||
|                                 willBeDisabled={change.payload?.disabled} | ||||
|                             /> | ||||
|                             <StrategyTooltipLink | ||||
|                                 change={change} | ||||
|                                 previousTitle={currentStrategy?.title} | ||||
|                             > | ||||
|                                 <StrategyDiff | ||||
|                                     change={change} | ||||
|                                     currentStrategy={currentStrategy} | ||||
|                                 /> | ||||
|                             </StrategyTooltipLink> | ||||
|                         </ChangeItemInfo> | ||||
|                         {discard} | ||||
|                     </ChangeItemCreateEditWrapper> | ||||
|                     <StrategyExecution strategy={change.payload} /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={ | ||||
|                             change.payload?.disabled !== | ||||
|                             currentStrategy?.disabled | ||||
|                         } | ||||
|                         show={ | ||||
|                             <Typography | ||||
|                                 sx={{ | ||||
|                                     marginTop: theme => theme.spacing(2), | ||||
|                                     paddingLeft: theme => theme.spacing(3), | ||||
|                                     paddingRight: theme => theme.spacing(3), | ||||
|                                     ...flexRow, | ||||
|                                     gap: theme => theme.spacing(1), | ||||
|                                 }} | ||||
|                             > | ||||
|                                 This strategy will be{' '} | ||||
|                                 <DisabledEnabledState | ||||
|                                     disabled={change.payload?.disabled || false} | ||||
|                                 /> | ||||
|                             </Typography> | ||||
|                         } | ||||
|                     /> | ||||
|                 </> | ||||
|             )} | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,25 @@ | ||||
| import { | ||||
|     IChangeRequestAddStrategy, | ||||
|     IChangeRequestDeleteStrategy, | ||||
|     IChangeRequestUpdateStrategy, | ||||
| } from 'component/changeRequest/changeRequest.types'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| 
 | ||||
| export const useCurrentStrategy = ( | ||||
|     change: | ||||
|         | IChangeRequestAddStrategy | ||||
|         | IChangeRequestUpdateStrategy | ||||
|         | IChangeRequestDeleteStrategy, | ||||
|     project: string, | ||||
|     feature: string, | ||||
|     environmentName: string | ||||
| ) => { | ||||
|     const currentFeature = useFeature(project, feature); | ||||
|     const currentStrategy = currentFeature.feature?.environments | ||||
|         .find(environment => environment.name === environmentName) | ||||
|         ?.strategies.find( | ||||
|             strategy => | ||||
|                 'id' in change.payload && strategy.id === change.payload.id | ||||
|         ); | ||||
|     return currentStrategy; | ||||
| }; | ||||
| @ -8,11 +8,13 @@ import { | ||||
|     formatStrategyName, | ||||
|     GetFeatureStrategyIcon, | ||||
| } from 'utils/strategyNames'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import EventDiff from 'component/events/EventDiff/EventDiff'; | ||||
| import omit from 'lodash.omit'; | ||||
| import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { Typography, styled } from '@mui/material'; | ||||
| import { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { textTruncated } from 'themes/themeStyles'; | ||||
| 
 | ||||
| const StyledCodeSection = styled('div')(({ theme }) => ({ | ||||
|     overflowX: 'auto', | ||||
| @ -25,41 +27,13 @@ const StyledCodeSection = styled('div')(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const useCurrentStrategy = ( | ||||
|     change: | ||||
|         | IChangeRequestAddStrategy | ||||
|         | IChangeRequestUpdateStrategy | ||||
|         | IChangeRequestDeleteStrategy, | ||||
|     project: string, | ||||
|     feature: string, | ||||
|     environmentName: string | ||||
| ) => { | ||||
|     const currentFeature = useFeature(project, feature); | ||||
|     const currentStrategy = currentFeature.feature?.environments | ||||
|         .find(environment => environment.name === environmentName) | ||||
|         ?.strategies.find( | ||||
|             strategy => | ||||
|                 'id' in change.payload && strategy.id === change.payload.id | ||||
|         ); | ||||
|     return currentStrategy; | ||||
| }; | ||||
| 
 | ||||
| export const StrategyDiff: FC<{ | ||||
|     change: | ||||
|         | IChangeRequestAddStrategy | ||||
|         | IChangeRequestUpdateStrategy | ||||
|         | IChangeRequestDeleteStrategy; | ||||
|     project: string; | ||||
|     feature: string; | ||||
|     environmentName: string; | ||||
| }> = ({ change, project, feature, environmentName }) => { | ||||
|     const currentStrategy = useCurrentStrategy( | ||||
|         change, | ||||
|         project, | ||||
|         feature, | ||||
|         environmentName | ||||
|     ); | ||||
| 
 | ||||
|     currentStrategy?: IFeatureStrategy; | ||||
| }> = ({ change, currentStrategy }) => { | ||||
|     const changeRequestStrategy = | ||||
|         change.action === 'deleteStrategy' ? undefined : change.payload; | ||||
| 
 | ||||
| @ -79,14 +53,35 @@ interface IStrategyTooltipLinkProps { | ||||
|         | IChangeRequestAddStrategy | ||||
|         | IChangeRequestUpdateStrategy | ||||
|         | IChangeRequestDeleteStrategy; | ||||
|     previousTitle?: string; | ||||
| } | ||||
| 
 | ||||
| export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({ | ||||
|     change, | ||||
|     previousTitle, | ||||
|     children, | ||||
| }) => ( | ||||
|     <> | ||||
|         <GetFeatureStrategyIcon strategyName={change.payload.name} /> | ||||
|         <ConditionallyRender | ||||
|             condition={Boolean( | ||||
|                 previousTitle && previousTitle !== change.payload.title | ||||
|             )} | ||||
|             show={ | ||||
|                 <> | ||||
|                     <Typography | ||||
|                         component="s" | ||||
|                         color="action.disabled" | ||||
|                         sx={{ | ||||
|                             ...textTruncated, | ||||
|                             maxWidth: '100px', | ||||
|                         }} | ||||
|                     > | ||||
|                         {previousTitle} | ||||
|                     </Typography>{' '} | ||||
|                 </> | ||||
|             } | ||||
|         /> | ||||
|         <TooltipLink | ||||
|             tooltip={children} | ||||
|             tooltipProps={{ | ||||
| @ -94,7 +89,20 @@ export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({ | ||||
|                 maxHeight: 600, | ||||
|             }} | ||||
|         > | ||||
|             {formatStrategyName(change.payload.name)} | ||||
|             <Typography | ||||
|                 component="span" | ||||
|                 sx={{ | ||||
|                     ...textTruncated, | ||||
|                     maxWidth: | ||||
|                         previousTitle === change.payload.title | ||||
|                             ? '300px' | ||||
|                             : '200px', | ||||
|                     display: 'block', | ||||
|                 }} | ||||
|             > | ||||
|                 {change.payload.title || | ||||
|                     formatStrategyName(change.payload.name)} | ||||
|             </Typography> | ||||
|         </TooltipLink> | ||||
|     </> | ||||
| ); | ||||
|  | ||||
| @ -106,7 +106,7 @@ type ChangeRequestEnabled = { enabled: boolean }; | ||||
| 
 | ||||
| type ChangeRequestAddStrategy = Pick< | ||||
|     IFeatureStrategy, | ||||
|     'parameters' | 'constraints' | 'segments' | ||||
|     'parameters' | 'constraints' | 'segments' | 'title' | 'disabled' | ||||
| > & { name: string }; | ||||
| 
 | ||||
| type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string }; | ||||
| @ -114,6 +114,8 @@ type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string }; | ||||
| type ChangeRequestDeleteStrategy = { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     title?: string; | ||||
|     disabled?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export type ChangeRequestAction = | ||||
| @ -122,6 +124,3 @@ export type ChangeRequestAction = | ||||
|     | 'updateStrategy' | ||||
|     | 'deleteStrategy' | ||||
|     | 'patchVariant'; | ||||
| 
 | ||||
| export const hasNameField = (payload: unknown): payload is { name: string } => | ||||
|     typeof payload === 'object' && payload !== null && 'name' in payload; | ||||
|  | ||||
| @ -9,7 +9,14 @@ import React, { | ||||
| } from 'react'; | ||||
| import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| type Color = 'info' | 'success' | 'warning' | 'error' | 'secondary' | 'neutral'; | ||||
| type Color = | ||||
|     | 'info' | ||||
|     | 'success' | ||||
|     | 'warning' | ||||
|     | 'error' | ||||
|     | 'secondary' | ||||
|     | 'neutral' | ||||
|     | 'disabled'; // TODO: refactor theme
 | ||||
| 
 | ||||
| interface IBadgeProps { | ||||
|     as?: React.ElementType; | ||||
| @ -37,16 +44,27 @@ const StyledBadge = styled('div')<IBadgeProps>( | ||||
|         fontSize: theme.fontSizes.smallerBody, | ||||
|         fontWeight: theme.fontWeight.bold, | ||||
|         lineHeight: 1, | ||||
|         backgroundColor: theme.palette[color].light, | ||||
|         color: theme.palette[color].contrastText, | ||||
|         border: `1px solid ${theme.palette[color].border}`, | ||||
|         ...(color === 'disabled' | ||||
|             ? { | ||||
|                   color: theme.palette.text.secondary, | ||||
|                   background: theme.palette.background.paper, | ||||
|                   border: `1px solid ${theme.palette.divider}`, | ||||
|               } | ||||
|             : { | ||||
|                   backgroundColor: theme.palette[color].light, | ||||
|                   color: theme.palette[color].contrastText, | ||||
|                   border: `1px solid ${theme.palette[color].border}`, | ||||
|               }), | ||||
|     }) | ||||
| ); | ||||
| 
 | ||||
| const StyledBadgeIcon = styled('div')<IBadgeIconProps>( | ||||
|     ({ theme, color = 'neutral', iconRight = false }) => ({ | ||||
|         display: 'flex', | ||||
|         color: theme.palette[color].main, | ||||
|         color: | ||||
|             color === 'disabled' | ||||
|                 ? theme.palette.action.disabled | ||||
|                 : theme.palette[color].main, | ||||
|         margin: iconRight | ||||
|             ? theme.spacing(0, 0, 0, 0.5) | ||||
|             : theme.spacing(0, 0.5, 0, 0), | ||||
|  | ||||
| @ -29,7 +29,7 @@ interface IConstraintAccordionViewProps { | ||||
| const StyledAccordion = styled(Accordion)(({ theme }) => ({ | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     backgroundColor: 'transparent', | ||||
|     boxShadow: 'none', | ||||
|     margin: 0, | ||||
|     '&:before': { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { DragEventHandler, FC, ReactNode } from 'react'; | ||||
| import { DragIndicator } from '@mui/icons-material'; | ||||
| import { styled, IconButton, Box } from '@mui/material'; | ||||
| import { styled, IconButton, Box, Chip } from '@mui/material'; | ||||
| import { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| import { | ||||
|     getFeatureStrategyIcon, | ||||
| @ -10,6 +10,7 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { PlaygroundStrategySchema } from 'openapi'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { Badge } from '../Badge/Badge'; | ||||
| 
 | ||||
| interface IStrategyItemContainerProps { | ||||
|     strategy: IFeatureStrategy | PlaygroundStrategySchema; | ||||
| @ -39,26 +40,35 @@ const StyledIndexLabel = styled('div')(({ theme }) => ({ | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledContainer = styled(Box)(({ theme }) => ({ | ||||
| const StyledContainer = styled(Box, { | ||||
|     shouldForwardProp: prop => prop !== 'disabled', | ||||
| })<{ disabled?: boolean }>(({ theme, disabled }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     border: `1px solid ${theme.palette.divider}`, | ||||
|     '& + &': { | ||||
|         marginTop: theme.spacing(2), | ||||
|     }, | ||||
|     background: theme.palette.background.paper, | ||||
|     background: disabled | ||||
|         ? theme.palette.envAccordion.disabled | ||||
|         : theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('div', { | ||||
|     shouldForwardProp: prop => prop !== 'draggable', | ||||
| })(({ theme, draggable }) => ({ | ||||
|     padding: theme.spacing(0.5, 2), | ||||
|     display: 'flex', | ||||
|     gap: theme.spacing(1), | ||||
|     alignItems: 'center', | ||||
|     borderBottom: `1px solid ${theme.palette.divider}`, | ||||
|     fontWeight: theme.typography.fontWeightMedium, | ||||
|     paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), | ||||
| })); | ||||
|     shouldForwardProp: prop => prop !== 'draggable' && prop !== 'disabled', | ||||
| })<{ draggable: boolean; disabled: boolean }>( | ||||
|     ({ theme, draggable, disabled }) => ({ | ||||
|         padding: theme.spacing(0.5, 2), | ||||
|         display: 'flex', | ||||
|         gap: theme.spacing(1), | ||||
|         alignItems: 'center', | ||||
|         borderBottom: `1px solid ${theme.palette.divider}`, | ||||
|         fontWeight: theme.typography.fontWeightMedium, | ||||
|         paddingLeft: draggable ? theme.spacing(1) : theme.spacing(2), | ||||
|         color: disabled | ||||
|             ? theme.palette.action.disabled | ||||
|             : theme.palette.text.primary, | ||||
|     }) | ||||
| ); | ||||
| 
 | ||||
| export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|     strategy, | ||||
| @ -78,8 +88,14 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|                 condition={orderNumber !== undefined} | ||||
|                 show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>} | ||||
|             /> | ||||
|             <StyledContainer style={style}> | ||||
|                 <StyledHeader draggable={Boolean(onDragStart)}> | ||||
|             <StyledContainer | ||||
|                 disabled={strategy?.disabled || false} | ||||
|                 style={style} | ||||
|             > | ||||
|                 <StyledHeader | ||||
|                     draggable={Boolean(onDragStart)} | ||||
|                     disabled={Boolean(strategy?.disabled)} | ||||
|                 > | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(onDragStart)} | ||||
|                         show={() => ( | ||||
| @ -113,6 +129,14 @@ export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({ | ||||
|                                 : strategy.name | ||||
|                         )} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(strategy?.disabled)} | ||||
|                         show={() => ( | ||||
|                             <> | ||||
|                                 <Badge color="disabled">Disabled</Badge> | ||||
|                             </> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <Box | ||||
|                         sx={{ | ||||
|                             marginLeft: 'auto', | ||||
|  | ||||
| @ -29,6 +29,52 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh | ||||
| import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| 
 | ||||
| const useTitleTracking = () => { | ||||
|     const [previousTitle, setPreviousTitle] = useState<string>(''); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
| 
 | ||||
|     const trackTitle = (title: string = '') => { | ||||
|         // don't expose the title, just if it was added, removed, or edited
 | ||||
|         if (title === previousTitle) { | ||||
|             trackEvent('strategyTitle', { | ||||
|                 props: { | ||||
|                     action: 'none', | ||||
|                     on: 'edit', | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|         if (previousTitle === '' && title !== '') { | ||||
|             trackEvent('strategyTitle', { | ||||
|                 props: { | ||||
|                     action: 'added', | ||||
|                     on: 'edit', | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|         if (previousTitle !== '' && title === '') { | ||||
|             trackEvent('strategyTitle', { | ||||
|                 props: { | ||||
|                     action: 'removed', | ||||
|                     on: 'edit', | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|         if (previousTitle !== '' && title !== '' && title !== previousTitle) { | ||||
|             trackEvent('strategyTitle', { | ||||
|                 props: { | ||||
|                     action: 'edited', | ||||
|                     on: 'edit', | ||||
|                 }, | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         setPreviousTitle, | ||||
|         trackTitle, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export const FeatureStrategyEdit = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
| @ -48,7 +94,7 @@ export const FeatureStrategyEdit = () => { | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const { refetch: refetchChangeRequests } = | ||||
|         usePendingChangeRequests(projectId); | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
|     const { setPreviousTitle, trackTitle } = useTitleTracking(); | ||||
| 
 | ||||
|     const { feature, refetchFeature } = useFeature(projectId, featureId); | ||||
| 
 | ||||
| @ -87,6 +133,7 @@ export const FeatureStrategyEdit = () => { | ||||
|             .flatMap(environment => environment.strategies) | ||||
|             .find(strategy => strategy.id === strategyId); | ||||
|         setStrategy(prev => ({ ...prev, ...savedStrategy })); | ||||
|         setPreviousTitle(savedStrategy?.title || ''); | ||||
|     }, [strategyId, data]); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
| @ -106,12 +153,10 @@ export const FeatureStrategyEdit = () => { | ||||
|             payload | ||||
|         ); | ||||
| 
 | ||||
|         trackEvent('strategyTitle', { | ||||
|             props: { | ||||
|                 hasTitle: Boolean(strategy.title), | ||||
|                 on: 'edit', | ||||
|             }, | ||||
|         }); | ||||
|         if (uiConfig?.flags?.strategyTitle) { | ||||
|             // NOTE: remove tracking when feature flag is removed
 | ||||
|             trackTitle(strategy.title); | ||||
|         } | ||||
| 
 | ||||
|         await refetchSavedStrategySegments(); | ||||
|         setToastData({ | ||||
| @ -202,6 +247,7 @@ export const createStrategyPayload = ( | ||||
|     constraints: strategy.constraints ?? [], | ||||
|     parameters: strategy.parameters ?? {}, | ||||
|     segments: segments.map(segment => segment.id), | ||||
|     disabled: strategy.disabled ?? false, | ||||
| }); | ||||
| 
 | ||||
| export const formatFeaturePath = ( | ||||
|  | ||||
| @ -0,0 +1,24 @@ | ||||
| import { FormControlLabel, Switch } from '@mui/material'; | ||||
| import { VFC } from 'react'; | ||||
| 
 | ||||
| interface IFeatureStrategyEnabledDisabledProps { | ||||
|     enabled: boolean; | ||||
|     onToggleEnabled: () => void; | ||||
| } | ||||
| 
 | ||||
| export const FeatureStrategyEnabledDisabled: VFC< | ||||
|     IFeatureStrategyEnabledDisabledProps | ||||
| > = ({ enabled, onToggleEnabled }) => { | ||||
|     return ( | ||||
|         <FormControlLabel | ||||
|             control={ | ||||
|                 <Switch | ||||
|                     name="enabled" | ||||
|                     onChange={onToggleEnabled} | ||||
|                     checked={enabled} | ||||
|                 /> | ||||
|             } | ||||
|             label="Enabled – This strategy will be used when evaluating feature toggles." | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -30,6 +30,7 @@ import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewW | ||||
| import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; | ||||
| import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess'; | ||||
| import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle'; | ||||
| import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled'; | ||||
| 
 | ||||
| interface IFeatureStrategyFormProps { | ||||
|     feature: IFeatureToggle; | ||||
| @ -250,6 +251,16 @@ export const FeatureStrategyForm = ({ | ||||
|                 hasAccess={access} | ||||
|             /> | ||||
|             <StyledHr /> | ||||
|             <FeatureStrategyEnabledDisabled | ||||
|                 enabled={!strategy?.disabled} | ||||
|                 onToggleEnabled={() => | ||||
|                     setStrategy(strategyState => ({ | ||||
|                         ...strategyState, | ||||
|                         disabled: !strategyState.disabled, | ||||
|                     })) | ||||
|                 } | ||||
|             /> | ||||
|             <StyledHr /> | ||||
|             <StyledButtons> | ||||
|                 <PermissionButton | ||||
|                     permission={permission} | ||||
|  | ||||
| @ -0,0 +1,152 @@ | ||||
| import { VFC, useState } from 'react'; | ||||
| import { Alert } from '@mui/material'; | ||||
| import BlockIcon from '@mui/icons-material/Block'; | ||||
| import TrackChangesIcon from '@mui/icons-material/TrackChanges'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { UPDATE_FEATURE_STRATEGY } from '@server/types/permissions'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import { useEnableDisable } from './hooks/useEnableDisable'; | ||||
| import { useSuggestEnableDisable } from './hooks/useSuggestEnableDisable'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { FeatureStrategyChangeRequestAlert } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert'; | ||||
| import { IDisableEnableStrategyProps } from './IDisableEnableStrategyProps'; | ||||
| 
 | ||||
| const DisableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => { | ||||
|     const { projectId, environmentId } = props; | ||||
|     const [isDialogueOpen, setDialogueOpen] = useState(false); | ||||
|     const { onDisable } = useEnableDisable({ ...props }); | ||||
|     const { onSuggestDisable } = useSuggestEnableDisable({ ...props }); | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const isChangeRequest = isChangeRequestConfigured(environmentId); | ||||
| 
 | ||||
|     const onClick = (event: React.FormEvent) => { | ||||
|         event.preventDefault(); | ||||
|         if (isChangeRequest) { | ||||
|             onSuggestDisable(); | ||||
|         } else { | ||||
|             onDisable(); | ||||
|         } | ||||
|         setDialogueOpen(false); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <PermissionIconButton | ||||
|                 onClick={() => setDialogueOpen(true)} | ||||
|                 projectId={projectId} | ||||
|                 environmentId={environmentId} | ||||
|                 permission={UPDATE_FEATURE_STRATEGY} | ||||
|                 tooltipProps={{ | ||||
|                     title: 'Disable strategy', | ||||
|                 }} | ||||
|                 type="button" | ||||
|             > | ||||
|                 <BlockIcon /> | ||||
|             </PermissionIconButton> | ||||
|             <Dialogue | ||||
|                 title={ | ||||
|                     isChangeRequest | ||||
|                         ? 'Add disabling strategy to change request?' | ||||
|                         : 'Are you sure you want to disable this strategy?' | ||||
|                 } | ||||
|                 open={isDialogueOpen} | ||||
|                 primaryButtonText={ | ||||
|                     isChangeRequest ? 'Add to draft' : 'Disable strategy' | ||||
|                 } | ||||
|                 secondaryButtonText="Cancel" | ||||
|                 onClick={onClick} | ||||
|                 onClose={() => setDialogueOpen(false)} | ||||
|             > | ||||
|                 <ConditionallyRender | ||||
|                     condition={isChangeRequest} | ||||
|                     show={ | ||||
|                         <FeatureStrategyChangeRequestAlert | ||||
|                             environment={environmentId} | ||||
|                         /> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <Alert severity="error"> | ||||
|                             Disabling the strategy will change which users | ||||
|                             receive access to the feature. | ||||
|                         </Alert> | ||||
|                     } | ||||
|                 /> | ||||
|             </Dialogue> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const EnableStrategy: VFC<IDisableEnableStrategyProps> = ({ ...props }) => { | ||||
|     const { projectId, environmentId } = props; | ||||
|     const [isDialogueOpen, setDialogueOpen] = useState(false); | ||||
|     const { onEnable } = useEnableDisable({ ...props }); | ||||
|     const { onSuggestEnable } = useSuggestEnableDisable({ ...props }); | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const isChangeRequest = isChangeRequestConfigured(environmentId); | ||||
| 
 | ||||
|     const onClick = (event: React.FormEvent) => { | ||||
|         event.preventDefault(); | ||||
|         if (isChangeRequest) { | ||||
|             onSuggestEnable(); | ||||
|         } else { | ||||
|             onEnable(); | ||||
|         } | ||||
|         setDialogueOpen(false); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <PermissionIconButton | ||||
|                 onClick={() => setDialogueOpen(true)} | ||||
|                 projectId={projectId} | ||||
|                 environmentId={environmentId} | ||||
|                 permission={UPDATE_FEATURE_STRATEGY} | ||||
|                 tooltipProps={{ | ||||
|                     title: 'Enable strategy', | ||||
|                 }} | ||||
|                 type="button" | ||||
|             > | ||||
|                 <TrackChangesIcon /> | ||||
|             </PermissionIconButton> | ||||
|             <Dialogue | ||||
|                 title={ | ||||
|                     isChangeRequest | ||||
|                         ? 'Add enabling strategy to change request?' | ||||
|                         : 'Are you sure you want to enable this strategy?' | ||||
|                 } | ||||
|                 open={isDialogueOpen} | ||||
|                 primaryButtonText={ | ||||
|                     isChangeRequest ? 'Add to draft' : 'Enable strategy' | ||||
|                 } | ||||
|                 secondaryButtonText="Cancel" | ||||
|                 onClick={onClick} | ||||
|                 onClose={() => setDialogueOpen(false)} | ||||
|             > | ||||
|                 <ConditionallyRender | ||||
|                     condition={isChangeRequest} | ||||
|                     show={ | ||||
|                         <FeatureStrategyChangeRequestAlert | ||||
|                             environment={environmentId} | ||||
|                         /> | ||||
|                     } | ||||
|                     elseShow={ | ||||
|                         <Alert severity="error"> | ||||
|                             Enabling the strategy will change which users | ||||
|                             receive access to the feature. | ||||
|                         </Alert> | ||||
|                     } | ||||
|                 /> | ||||
|             </Dialogue> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const DisableEnableStrategy: VFC<IDisableEnableStrategyProps> = ({ | ||||
|     ...props | ||||
| }) => | ||||
|     props.strategy.disabled ? ( | ||||
|         <EnableStrategy {...props} /> | ||||
|     ) : ( | ||||
|         <DisableStrategy {...props} /> | ||||
|     ); | ||||
| @ -0,0 +1,8 @@ | ||||
| import { IFeatureStrategy } from 'interfaces/strategy'; | ||||
| 
 | ||||
| export interface IDisableEnableStrategyProps { | ||||
|     projectId: string; | ||||
|     featureId: string; | ||||
|     environmentId: string; | ||||
|     strategy: IFeatureStrategy; | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { IDisableEnableStrategyProps } from '../IDisableEnableStrategyProps'; | ||||
| 
 | ||||
| export const useEnableDisable = ({ | ||||
|     projectId, | ||||
|     environmentId, | ||||
|     featureId, | ||||
|     strategy, | ||||
| }: IDisableEnableStrategyProps) => { | ||||
|     const { refetchFeature } = useFeature(projectId, featureId); | ||||
|     const { setStrategyDisabledState } = useFeatureStrategyApi(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| 
 | ||||
|     const onEnableDisable = (enabled: boolean) => async () => { | ||||
|         try { | ||||
|             await setStrategyDisabledState( | ||||
|                 projectId, | ||||
|                 featureId, | ||||
|                 environmentId, | ||||
|                 strategy.id, | ||||
|                 !enabled | ||||
|             ); | ||||
|             setToastData({ | ||||
|                 title: `Strategy ${enabled ? 'enabled' : 'disabled'}`, | ||||
|                 type: 'success', | ||||
|             }); | ||||
| 
 | ||||
|             refetchFeature(); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         onDisable: onEnableDisable(false), | ||||
|         onEnable: onEnableDisable(true), | ||||
|     }; | ||||
| }; | ||||
| @ -0,0 +1,40 @@ | ||||
| import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; | ||||
| import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { IDisableEnableStrategyProps } from '../IDisableEnableStrategyProps'; | ||||
| 
 | ||||
| export const useSuggestEnableDisable = ({ | ||||
|     projectId, | ||||
|     environmentId, | ||||
|     featureId, | ||||
|     strategy, | ||||
| }: IDisableEnableStrategyProps) => { | ||||
|     const { addChange } = useChangeRequestApi(); | ||||
|     const { refetch: refetchChangeRequests } = | ||||
|         usePendingChangeRequests(projectId); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const onSuggestEnableDisable = (enabled: boolean) => async () => { | ||||
|         try { | ||||
|             await addChange(projectId, environmentId, { | ||||
|                 action: 'updateStrategy', | ||||
|                 feature: featureId, | ||||
|                 payload: { | ||||
|                     ...strategy, | ||||
|                     disabled: !enabled, | ||||
|                 }, | ||||
|             }); | ||||
|             setToastData({ | ||||
|                 title: 'Changes added to the draft!', | ||||
|                 type: 'success', | ||||
|             }); | ||||
|             await refetchChangeRequests(); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
|     return { | ||||
|         onSuggestDisable: onSuggestEnableDisable(false), | ||||
|         onSuggestEnable: onSuggestEnableDisable(true), | ||||
|     }; | ||||
| }; | ||||
| @ -12,6 +12,8 @@ import { StrategyExecution } from './StrategyExecution/StrategyExecution'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; | ||||
| import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer'; | ||||
| import { DisableEnableStrategy } from './DisableEnableStrategy/DisableEnableStrategy'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| 
 | ||||
| interface IStrategyItemProps { | ||||
|     environmentId: string; | ||||
| @ -32,6 +34,7 @@ export const StrategyItem: FC<IStrategyItemProps> = ({ | ||||
|     orderNumber, | ||||
|     headerChildren, | ||||
| }) => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
| 
 | ||||
| @ -76,6 +79,17 @@ export const StrategyItem: FC<IStrategyItemProps> = ({ | ||||
|                     > | ||||
|                         <Edit /> | ||||
|                     </PermissionIconButton> | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(uiConfig?.flags?.strategyDisable)} | ||||
|                         show={() => ( | ||||
|                             <DisableEnableStrategy | ||||
|                                 projectId={projectId} | ||||
|                                 featureId={featureId} | ||||
|                                 environmentId={environmentId} | ||||
|                                 strategy={strategy} | ||||
|                             /> | ||||
|                         )} | ||||
|                     /> | ||||
|                     <FeatureStrategyRemove | ||||
|                         projectId={projectId} | ||||
|                         featureId={featureId} | ||||
|  | ||||
| @ -111,7 +111,7 @@ export const ChangeRequestTable: VFC = () => { | ||||
|             return { | ||||
|                 key, | ||||
|                 label: `${key} ${labelText}`, | ||||
|                 sx: { 'font-size': theme.fontSizes.smallBody }, | ||||
|                 sx: { fontSize: theme.fontSizes.smallBody }, | ||||
|             }; | ||||
|         }); | ||||
| 
 | ||||
|  | ||||
| @ -71,11 +71,37 @@ const useFeatureStrategyApi = () => { | ||||
|         await makeRequest(req.caller, req.id); | ||||
|     }; | ||||
| 
 | ||||
|     const setStrategyDisabledState = async ( | ||||
|         projectId: string, | ||||
|         featureId: string, | ||||
|         environmentId: string, | ||||
|         strategyId: string, | ||||
|         disabled: boolean | ||||
|     ): Promise<void> => { | ||||
|         const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`; | ||||
|         const req = createRequest( | ||||
|             path, | ||||
|             { | ||||
|                 method: 'PATCH', | ||||
|                 body: JSON.stringify([ | ||||
|                     { | ||||
|                         path: '/disabled', | ||||
|                         value: disabled, | ||||
|                         op: 'replace', | ||||
|                     }, | ||||
|                 ]), | ||||
|             }, | ||||
|             'setStrategyDisabledState' | ||||
|         ); | ||||
|         await makeRequest(req.caller, req.id); | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         addStrategyToFeature, | ||||
|         updateStrategyOnFeature, | ||||
|         deleteStrategyFromFeature, | ||||
|         setStrategiesSortOrder, | ||||
|         setStrategyDisabledState, | ||||
|         loading, | ||||
|         errors, | ||||
|     }; | ||||
|  | ||||
| @ -11,6 +11,7 @@ export interface IFeatureStrategy { | ||||
|     projectId?: string; | ||||
|     environment?: string; | ||||
|     segments?: number[]; | ||||
|     disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IFeatureStrategyParameters { | ||||
| @ -24,6 +25,7 @@ export interface IFeatureStrategyPayload { | ||||
|     constraints: IConstraint[]; | ||||
|     parameters: IFeatureStrategyParameters; | ||||
|     segments?: number[]; | ||||
|     disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IStrategy { | ||||
|  | ||||
| @ -51,6 +51,7 @@ export interface IFlags { | ||||
|     demo?: boolean; | ||||
|     strategyTitle?: boolean; | ||||
|     groupRootRoles?: boolean; | ||||
|     strategyDisable?: boolean; | ||||
|     googleAuthEnabled?: boolean; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -11,6 +11,8 @@ export interface CreateFeatureStrategySchema { | ||||
|     name: string; | ||||
|     /** A descriptive title for the strategy */ | ||||
|     title?: string | null; | ||||
|     /** A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs */ | ||||
|     disabled?: boolean | null; | ||||
|     /** The order of the strategy in the list */ | ||||
|     sortOrder?: number; | ||||
|     /** A list of the constraints attached to the strategy */ | ||||
|  | ||||
| @ -16,6 +16,8 @@ export interface FeatureStrategySchema { | ||||
|     name: string; | ||||
|     /** A descriptive title for the strategy */ | ||||
|     title?: string | null; | ||||
|     /** A toggle to disable the strategy. defaults to false. Disabled strategies are not evaluated or returned to the SDKs */ | ||||
|     disabled?: boolean | null; | ||||
|     /** The name or feature the strategy is attached to */ | ||||
|     featureName?: string; | ||||
|     /** The order of the strategy in the list */ | ||||
|  | ||||
| @ -10,6 +10,8 @@ export interface GroupSchema { | ||||
|     name: string; | ||||
|     description?: string | null; | ||||
|     mappingsSSO?: string[]; | ||||
|     /** A role id that is used as the root role for all users in this group. This can be either the id of the Editor or Admin role. */ | ||||
|     rootRole?: number | null; | ||||
|     createdBy?: string | null; | ||||
|     createdAt?: string | null; | ||||
|     users?: GroupUserModelSchema[]; | ||||
|  | ||||
| @ -17,6 +17,8 @@ export interface PlaygroundStrategySchema { | ||||
|     id: string; | ||||
|     /** The strategy's evaluation result. If the strategy is a custom strategy that Unleash can't evaluate, `evaluationStatus` will be `unknown`. Otherwise, it will be `true` or `false` */ | ||||
|     result: PlaygroundStrategySchemaResult; | ||||
|     /** The strategy's status. Disabled strategies are not evaluated */ | ||||
|     disabled: boolean | null; | ||||
|     /** The strategy's segments and their evaluation results. */ | ||||
|     segments: PlaygroundSegmentSchema[]; | ||||
|     /** The strategy's constraints and their evaluation results. */ | ||||
|  | ||||
| @ -15,6 +15,10 @@ import type { EnvironmentSchema } from './environmentSchema'; | ||||
| import type { SegmentSchema } from './segmentSchema'; | ||||
| import type { FeatureStrategySegmentSchema } from './featureStrategySegmentSchema'; | ||||
| 
 | ||||
| /** | ||||
|  * The state of the application used by export/import APIs which are deprecated in favor of the more fine grained /api/admin/export and /api/admin/import APIs | ||||
|  * @deprecated | ||||
|  */ | ||||
| export interface StateSchema { | ||||
|     version: number; | ||||
|     features?: FeatureSchema[]; | ||||
|  | ||||
| @ -10,5 +10,9 @@ export interface UpdateFeatureStrategySchema { | ||||
|     name?: string; | ||||
|     sortOrder?: number; | ||||
|     constraints?: ConstraintSchema[]; | ||||
|     /** A descriptive title for the strategy */ | ||||
|     title?: string | null; | ||||
|     /** A toggle to disable the strategy. defaults to true. Disabled strategies are not evaluated or returned to the SDKs */ | ||||
|     disabled?: boolean | null; | ||||
|     parameters?: ParametersSchema; | ||||
| } | ||||
|  | ||||
| @ -87,6 +87,7 @@ exports[`should create default config 1`] = ` | ||||
|       "proPlanAutoCharge": false, | ||||
|       "projectScopedStickiness": false, | ||||
|       "responseTimeWithAppNameKillSwitch": false, | ||||
|       "strategyDisable": false, | ||||
|       "strategyTitle": false, | ||||
|       "strictSchemaValidation": false, | ||||
|     }, | ||||
| @ -114,6 +115,7 @@ exports[`should create default config 1`] = ` | ||||
|       "proPlanAutoCharge": false, | ||||
|       "projectScopedStickiness": false, | ||||
|       "responseTimeWithAppNameKillSwitch": false, | ||||
|       "strategyDisable": false, | ||||
|       "strategyTitle": false, | ||||
|       "strictSchemaValidation": false, | ||||
|     }, | ||||
|  | ||||
| @ -80,6 +80,10 @@ const flags = { | ||||
|         process.env.UNLEASH_STRATEGY_TITLE, | ||||
|         false, | ||||
|     ), | ||||
|     strategyDisable: parseEnvVarBoolean( | ||||
|         process.env.UNLEASH_STRATEGY_DISABLE, | ||||
|         false, | ||||
|     ), | ||||
|     googleAuthEnabled: parseEnvVarBoolean( | ||||
|         process.env.GOOGLE_AUTH_ENABLED, | ||||
|         false, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user