mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: improved feature overview sidepanel env toggles (#2487)
https://linear.app/unleash/issue/2-423/update-feature-toggle-overview-sidepanel
This commit is contained in:
		
							parent
							
								
									97372cf48c
								
							
						
					
					
						commit
						3dca3d53f9
					
				| @ -11,8 +11,12 @@ import { | ||||
| } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; | ||||
| 
 | ||||
| const FeatureOverview = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const navigate = useNavigate(); | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
| @ -25,7 +29,11 @@ const FeatureOverview = () => { | ||||
|         <div className={styles.container}> | ||||
|             <div> | ||||
|                 <FeatureOverviewMetaData /> | ||||
|                 <FeatureOverviewEnvSwitches /> | ||||
|                 <ConditionallyRender | ||||
|                     condition={Boolean(uiConfig.flags.variantsPerEnvironment)} | ||||
|                     show={<FeatureOverviewSidePanel />} | ||||
|                     elseShow={<FeatureOverviewEnvSwitches />} | ||||
|                 /> | ||||
|             </div> | ||||
|             <div className={styles.mainContent}> | ||||
|                 <FeatureOverviewEnvironments /> | ||||
|  | ||||
| @ -0,0 +1,61 @@ | ||||
| import { styled } from '@mui/material'; | ||||
| import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     padding: '1.5rem', | ||||
|     maxWidth: '350px', | ||||
|     minWidth: '350px', | ||||
|     marginRight: '1rem', | ||||
|     marginTop: '1rem', | ||||
|     [theme.breakpoints.down(1000)]: { | ||||
|         marginBottom: '1rem', | ||||
|         width: '100%', | ||||
|         maxWidth: 'none', | ||||
|         minWidth: 'auto', | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledHeader = styled('h3')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     gap: theme.spacing(1), | ||||
|     alignItems: 'center', | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
|     margin: 0, | ||||
|     marginBottom: theme.spacing(3), | ||||
| 
 | ||||
|     // Make the help icon align with the text.
 | ||||
|     '& > :last-child': { | ||||
|         position: 'relative', | ||||
|         top: 1, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| export const FeatureOverviewSidePanel = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
|     const { feature } = useFeature(projectId, featureId); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer> | ||||
|             <FeatureOverviewSidePanelEnvironmentSwitches | ||||
|                 header={ | ||||
|                     <StyledHeader data-loading> | ||||
|                         Enabled in environments ({feature.environments.length}) | ||||
|                         <HelpIcon | ||||
|                             tooltip="When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies." | ||||
|                             placement="top" | ||||
|                         /> | ||||
|                     </StyledHeader> | ||||
|                 } | ||||
|                 feature={feature} | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,146 @@ | ||||
| import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors'; | ||||
| import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; | ||||
| import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle'; | ||||
| import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog'; | ||||
| import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { styled } from '@mui/material'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
|     '&:not(:last-of-type)': { | ||||
|         marginBottom: theme.spacing(2), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledLabel = styled('label')(({ theme }) => ({ | ||||
|     display: 'inline-flex', | ||||
|     alignItems: 'center', | ||||
|     cursor: 'pointer', | ||||
| })); | ||||
| 
 | ||||
| interface IFeatureOverviewSidePanelEnvironmentSwitchProps { | ||||
|     env: IFeatureEnvironment; | ||||
|     callback?: () => void; | ||||
|     showInfoBox: () => void; | ||||
|     children?: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const FeatureOverviewSidePanelEnvironmentSwitch = ({ | ||||
|     env, | ||||
|     callback, | ||||
|     showInfoBox, | ||||
|     children, | ||||
| }: IFeatureOverviewSidePanelEnvironmentSwitchProps) => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const featureId = useRequiredPathParam('featureId'); | ||||
|     const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } = | ||||
|         useFeatureApi(); | ||||
|     const { refetchFeature } = useFeature(projectId, featureId); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); | ||||
|     const { | ||||
|         onChangeRequestToggle, | ||||
|         onChangeRequestToggleClose, | ||||
|         onChangeRequestToggleConfirm, | ||||
|         changeRequestDialogDetails, | ||||
|     } = useChangeRequestToggle(projectId); | ||||
| 
 | ||||
|     const handleToggleEnvironmentOn = async () => { | ||||
|         try { | ||||
|             await toggleFeatureEnvironmentOn(projectId, featureId, env.name); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: `Available in ${env.name}`, | ||||
|                 text: `${featureId} is now available in ${env.name} based on its defined strategies.`, | ||||
|             }); | ||||
|             refetchFeature(); | ||||
|             if (callback) { | ||||
|                 callback(); | ||||
|             } | ||||
|         } catch (error: unknown) { | ||||
|             if ( | ||||
|                 error instanceof Error && | ||||
|                 error.message === ENVIRONMENT_STRATEGY_ERROR | ||||
|             ) { | ||||
|                 showInfoBox(); | ||||
|             } else { | ||||
|                 setToastApiError(formatUnknownError(error)); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleToggleEnvironmentOff = async () => { | ||||
|         try { | ||||
|             await toggleFeatureEnvironmentOff(projectId, featureId, env.name); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: `Unavailable in ${env.name}`, | ||||
|                 text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`, | ||||
|             }); | ||||
|             refetchFeature(); | ||||
|             if (callback) { | ||||
|                 callback(); | ||||
|             } | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const toggleEnvironment = async (e: React.ChangeEvent) => { | ||||
|         if (isChangeRequestConfigured(env.name)) { | ||||
|             e.preventDefault(); | ||||
|             onChangeRequestToggle(featureId, env.name, !env.enabled); | ||||
|             return; | ||||
|         } | ||||
|         if (env.enabled) { | ||||
|             await handleToggleEnvironmentOff(); | ||||
|             return; | ||||
|         } | ||||
|         await handleToggleEnvironmentOn(); | ||||
|     }; | ||||
| 
 | ||||
|     const defaultContent = ( | ||||
|         <> | ||||
|             {' '} | ||||
|             <span data-loading>{env.enabled ? 'enabled' : 'disabled'} in</span> | ||||
|               | ||||
|             <StringTruncator text={env.name} maxWidth="120" maxLength={15} /> | ||||
|         </> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledContainer> | ||||
|             <StyledLabel> | ||||
|                 <PermissionSwitch | ||||
|                     permission={UPDATE_FEATURE_ENVIRONMENT} | ||||
|                     projectId={projectId} | ||||
|                     checked={env.enabled} | ||||
|                     onChange={toggleEnvironment} | ||||
|                     environmentId={env.name} | ||||
|                 /> | ||||
|                 {children ?? defaultContent} | ||||
|             </StyledLabel> | ||||
|             <ChangeRequestDialogue | ||||
|                 isOpen={changeRequestDialogDetails.isOpen} | ||||
|                 onClose={onChangeRequestToggleClose} | ||||
|                 environment={changeRequestDialogDetails?.environment} | ||||
|                 onConfirm={onChangeRequestToggleConfirm} | ||||
|                 messageComponent={ | ||||
|                     <UpdateEnabledMessage | ||||
|                         enabled={changeRequestDialogDetails?.enabled!} | ||||
|                         featureName={changeRequestDialogDetails?.featureName!} | ||||
|                         environment={changeRequestDialogDetails.environment!} | ||||
|                     /> | ||||
|                 } | ||||
|             /> | ||||
|         </StyledContainer> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,94 @@ | ||||
| import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog'; | ||||
| import { IFeatureToggle } from 'interfaces/featureToggle'; | ||||
| import { useState } from 'react'; | ||||
| import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch'; | ||||
| import { Link, styled } from '@mui/material'; | ||||
| import { Link as RouterLink } from 'react-router-dom'; | ||||
| 
 | ||||
| const StyledContainer = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
| })); | ||||
| 
 | ||||
| const StyledLabel = styled('p')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledSubLabel = styled('p')(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallerBody, | ||||
|     color: theme.palette.text.secondary, | ||||
| })); | ||||
| 
 | ||||
| const StyledLink = styled(Link<typeof RouterLink | 'a'>)(() => ({ | ||||
|     '&:hover, &:focus': { | ||||
|         textDecoration: 'underline', | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| interface IFeatureOverviewSidePanelEnvironmentSwitchesProps { | ||||
|     feature: IFeatureToggle; | ||||
|     header: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const FeatureOverviewSidePanelEnvironmentSwitches = ({ | ||||
|     feature, | ||||
|     header, | ||||
| }: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => { | ||||
|     const [showInfoBox, setShowInfoBox] = useState(false); | ||||
|     const [environmentName, setEnvironmentName] = useState(''); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             {header} | ||||
|             {feature.environments.map(environment => { | ||||
|                 const strategiesLabel = | ||||
|                     environment.strategies.length === 1 | ||||
|                         ? '1 strategy' | ||||
|                         : `${environment.strategies.length} strategies`; | ||||
| 
 | ||||
|                 const variants = environment.variants ?? []; | ||||
| 
 | ||||
|                 const variantsLink = variants.length > 0 && ( | ||||
|                     <> | ||||
|                         {' - '} | ||||
|                         <StyledLink | ||||
|                             component={RouterLink} | ||||
|                             to={`/projects/${feature.project}/features/${feature.name}/variants`} | ||||
|                             underline="hover" | ||||
|                         > | ||||
|                             {variants.length === 1 | ||||
|                                 ? '1 variant' | ||||
|                                 : `${variants.length} variants`} | ||||
|                         </StyledLink> | ||||
|                     </> | ||||
|                 ); | ||||
| 
 | ||||
|                 return ( | ||||
|                     <FeatureOverviewSidePanelEnvironmentSwitch | ||||
|                         key={environment.name} | ||||
|                         env={environment} | ||||
|                         showInfoBox={() => { | ||||
|                             setEnvironmentName(environment.name); | ||||
|                             setShowInfoBox(true); | ||||
|                         }} | ||||
|                     > | ||||
|                         <StyledContainer> | ||||
|                             <StyledLabel>{environment.name}</StyledLabel> | ||||
|                             <StyledSubLabel> | ||||
|                                 {strategiesLabel} | ||||
|                                 {variantsLink} | ||||
|                             </StyledSubLabel> | ||||
|                         </StyledContainer> | ||||
|                     </FeatureOverviewSidePanelEnvironmentSwitch> | ||||
|                 ); | ||||
|             })} | ||||
|             <EnvironmentStrategyDialog | ||||
|                 open={showInfoBox} | ||||
|                 onClose={() => setShowInfoBox(false)} | ||||
|                 projectId={feature.project} | ||||
|                 featureId={feature.name} | ||||
|                 environmentName={environmentName} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user