mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: add push to all button to UI (#2969)
## About the changes This adds the push to all button to the UI Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: #2254 ### UI  Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
		
							parent
							
								
									c71c0bb3ac
								
							
						
					
					
						commit
						3713764a4b
					
				| @ -19,6 +19,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { EnvironmentVariantsCopyFrom } from './EnvironmentVariantsCopyFrom/EnvironmentVariantsCopyFrom'; | ||||
| import { PushVariantsButton } from './PushVariantsButton/PushVariantsButton'; | ||||
| 
 | ||||
| const StyledAlert = styled(Alert)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(4), | ||||
| @ -43,7 +44,8 @@ export const FeatureEnvironmentVariants = () => { | ||||
|         projectId, | ||||
|         featureId | ||||
|     ); | ||||
|     const { patchFeatureEnvironmentVariants } = useFeatureApi(); | ||||
|     const { patchFeatureEnvironmentVariants, overrideVariantsInEnvironments } = | ||||
|         useFeatureApi(); | ||||
| 
 | ||||
|     const [searchValue, setSearchValue] = useState(''); | ||||
|     const [selectedEnvironment, setSelectedEnvironment] = | ||||
| @ -88,6 +90,27 @@ export const FeatureEnvironmentVariants = () => { | ||||
|         refetchFeature(); | ||||
|     }; | ||||
| 
 | ||||
|     const pushToEnvironments = async ( | ||||
|         variants: IFeatureVariant[], | ||||
|         selected: string[] | ||||
|     ) => { | ||||
|         try { | ||||
|             await overrideVariantsInEnvironments( | ||||
|                 projectId, | ||||
|                 featureId, | ||||
|                 variants, | ||||
|                 selected | ||||
|             ); | ||||
|             refetchFeature(); | ||||
|             setToastData({ | ||||
|                 title: `Variants pushed successfully`, | ||||
|                 type: 'success', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const addVariant = (environment: IFeatureEnvironment) => { | ||||
|         setSelectedEnvironment(environment); | ||||
|         setSelectedVariant(undefined); | ||||
| @ -241,6 +264,18 @@ export const FeatureEnvironmentVariants = () => { | ||||
|                         } | ||||
|                     > | ||||
|                         <StyledButtonContainer> | ||||
|                             <PushVariantsButton | ||||
|                                 current={environment.name} | ||||
|                                 environments={feature.environments} | ||||
|                                 permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS} | ||||
|                                 projectId={projectId} | ||||
|                                 onSubmit={selected => | ||||
|                                     pushToEnvironments( | ||||
|                                         environment.variants ?? [], | ||||
|                                         selected | ||||
|                                     ) | ||||
|                                 } | ||||
|                             /> | ||||
|                             <EnvironmentVariantsCopyFrom | ||||
|                                 environment={environment} | ||||
|                                 permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS} | ||||
|  | ||||
| @ -0,0 +1,158 @@ | ||||
| import { | ||||
|     Button, | ||||
|     Checkbox, | ||||
|     Divider, | ||||
|     FormControlLabel, | ||||
|     Menu, | ||||
|     MenuItem, | ||||
|     styled, | ||||
| } from '@mui/material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IFeatureEnvironment } from 'interfaces/featureToggle'; | ||||
| import { useState } from 'react'; | ||||
| import { useCheckProjectAccess } from 'hooks/useHasAccess'; | ||||
| 
 | ||||
| const StyledMenu = styled(Menu)(({ theme }) => ({ | ||||
|     '&>div>ul': { | ||||
|         display: 'flex', | ||||
|         flexDirection: 'column', | ||||
|         justifyContent: 'center', | ||||
|         padding: theme.spacing(2), | ||||
|         '&>li': { | ||||
|             padding: theme.spacing(0), | ||||
|         }, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledButton = styled(Button)(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| interface IPushVariantsButtonProps { | ||||
|     current: string; | ||||
|     environments: IFeatureEnvironment[]; | ||||
|     permission: string; | ||||
|     projectId: string; | ||||
|     onSubmit: (selected: string[]) => void; | ||||
| } | ||||
| 
 | ||||
| export const PushVariantsButton = ({ | ||||
|     current, | ||||
|     environments, | ||||
|     permission, | ||||
|     projectId, | ||||
|     onSubmit, | ||||
| }: IPushVariantsButtonProps) => { | ||||
|     const [pushToAnchorEl, setPushToAnchorEl] = useState<null | HTMLElement>( | ||||
|         null | ||||
|     ); | ||||
|     const pushToOpen = Boolean(pushToAnchorEl); | ||||
| 
 | ||||
|     const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>( | ||||
|         [] | ||||
|     ); | ||||
| 
 | ||||
|     const hasAccess = useCheckProjectAccess(projectId); | ||||
|     const hasAccessTo = environments.reduce((acc, env) => { | ||||
|         acc[env.name] = hasAccess(permission, env.name); | ||||
|         return acc; | ||||
|     }, {} as Record<string, boolean>); | ||||
| 
 | ||||
|     const addSelectedEnvironment = (name: string) => { | ||||
|         setSelectedEnvironments(prevSelectedEnvironments => [ | ||||
|             ...prevSelectedEnvironments, | ||||
|             name, | ||||
|         ]); | ||||
|     }; | ||||
| 
 | ||||
|     const removeSelectedEnvironment = (name: string) => { | ||||
|         setSelectedEnvironments(prevSelectedEnvironments => | ||||
|             prevSelectedEnvironments.filter(env => env !== name) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     const cleanupState = () => { | ||||
|         setSelectedEnvironments([]); | ||||
|         setPushToAnchorEl(null); | ||||
|     }; | ||||
| 
 | ||||
|     const variants = | ||||
|         environments.find(environment => environment.name === current) | ||||
|             ?.variants ?? []; | ||||
| 
 | ||||
|     return ( | ||||
|         <ConditionallyRender | ||||
|             condition={variants.length > 0 && environments.length > 1} | ||||
|             show={ | ||||
|                 <> | ||||
|                     <Button | ||||
|                         onClick={e => { | ||||
|                             setPushToAnchorEl(e.currentTarget); | ||||
|                         }} | ||||
|                         id={`push-to-menu-${current}`} | ||||
|                         aria-controls={pushToOpen ? 'basic-menu' : undefined} | ||||
|                         aria-haspopup="true" | ||||
|                         aria-expanded={pushToOpen ? 'true' : undefined} | ||||
|                         variant="outlined" | ||||
|                     > | ||||
|                         Push to environment | ||||
|                     </Button> | ||||
| 
 | ||||
|                     <StyledMenu | ||||
|                         anchorEl={pushToAnchorEl} | ||||
|                         open={pushToOpen} | ||||
|                         onClose={() => setPushToAnchorEl(null)} | ||||
|                         MenuListProps={{ | ||||
|                             'aria-labelledby': `push-to-menu-${current}`, | ||||
|                         }} | ||||
|                     > | ||||
|                         {environments | ||||
|                             .filter(environment => environment.name !== current) | ||||
|                             .map(otherEnvironment => ( | ||||
|                                 <MenuItem key={otherEnvironment.name}> | ||||
|                                     <FormControlLabel | ||||
|                                         disabled={ | ||||
|                                             !hasAccessTo[ | ||||
|                                                 otherEnvironment.name | ||||
|                                             ] ?? false | ||||
|                                         } | ||||
|                                         control={ | ||||
|                                             <Checkbox | ||||
|                                                 onChange={event => { | ||||
|                                                     if (event.target.checked) { | ||||
|                                                         addSelectedEnvironment( | ||||
|                                                             otherEnvironment.name | ||||
|                                                         ); | ||||
|                                                     } else { | ||||
|                                                         removeSelectedEnvironment( | ||||
|                                                             otherEnvironment.name | ||||
|                                                         ); | ||||
|                                                     } | ||||
|                                                 }} | ||||
|                                                 checked={selectedEnvironments.includes( | ||||
|                                                     otherEnvironment.name | ||||
|                                                 )} | ||||
|                                                 value={otherEnvironment.name} | ||||
|                                             /> | ||||
|                                         } | ||||
|                                         label={otherEnvironment.name} | ||||
|                                     /> | ||||
|                                 </MenuItem> | ||||
|                             ))} | ||||
|                         <Divider /> | ||||
|                         <StyledButton | ||||
|                             variant="outlined" | ||||
|                             onClick={() => { | ||||
|                                 onSubmit(selectedEnvironments); | ||||
|                                 cleanupState(); | ||||
|                             }} | ||||
|                             disabled={selectedEnvironments.length === 0} | ||||
|                         > | ||||
|                             Push to selected ({selectedEnvironments.length}) | ||||
|                         </StyledButton> | ||||
|                     </StyledMenu> | ||||
|                 </> | ||||
|             } | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -4,6 +4,7 @@ import { Operation } from 'fast-json-patch'; | ||||
| import { IConstraint } from 'interfaces/strategy'; | ||||
| import { CreateFeatureSchema } from 'openapi'; | ||||
| import useAPI from '../useApi/useApi'; | ||||
| import { IFeatureVariant } from 'interfaces/featureToggle'; | ||||
| 
 | ||||
| const useFeatureApi = () => { | ||||
|     const { makeRequest, createRequest, errors, loading } = useAPI({ | ||||
| @ -223,6 +224,26 @@ const useFeatureApi = () => { | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const overrideVariantsInEnvironments = async ( | ||||
|         projectId: string, | ||||
|         featureId: string, | ||||
|         variants: IFeatureVariant[], | ||||
|         environments: string[] | ||||
|     ) => { | ||||
|         const put = `api/admin/projects/${projectId}/features/${featureId}/variants-batch`; | ||||
|         const req = createRequest(put, { | ||||
|             method: 'PUT', | ||||
|             body: JSON.stringify({ variants, environments }), | ||||
|         }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const cloneFeatureToggle = async ( | ||||
|         projectId: string, | ||||
|         featureId: string, | ||||
| @ -257,6 +278,7 @@ const useFeatureApi = () => { | ||||
|         patchFeatureToggle, | ||||
|         patchFeatureVariants, | ||||
|         patchFeatureEnvironmentVariants, | ||||
|         overrideVariantsInEnvironments, | ||||
|         cloneFeatureToggle, | ||||
|         loading, | ||||
|     }; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user