mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: replace react-dnd with custom implementation (#988)
* refactor: replace react-dnd with custom implementation * refactor: add TextCell, IconCell, and ActionCell * refactor: port environments list to react-table * refactor: change OfflineBolt to PowerSettingsNew * refactor: simplify environment toast text * refactor: improve IToast type type * refactor: improve useSearchHighlightContext naming * refactor: clarify enableDragAndDrop logic
This commit is contained in:
		
							parent
							
								
									c073908027
								
							
						
					
					
						commit
						34f848ce8a
					
				| @ -83,8 +83,6 @@ | ||||
|     "prop-types": "15.8.1", | ||||
|     "react": "17.0.2", | ||||
|     "react-chartjs-2": "4.1.0", | ||||
|     "react-dnd": "15.1.2", | ||||
|     "react-dnd-html5-backend": "15.1.3", | ||||
|     "react-dom": "17.0.2", | ||||
|     "react-hooks-global-state": "1.0.2", | ||||
|     "react-router-dom": "6.3.0", | ||||
|  | ||||
| @ -4,5 +4,6 @@ const SearchHighlightContext = createContext(''); | ||||
| 
 | ||||
| export const SearchHighlightProvider = SearchHighlightContext.Provider; | ||||
| 
 | ||||
| export const useSearchHighlightContext = () => | ||||
|     useContext(SearchHighlightContext); | ||||
| export const useSearchHighlightContext = (): { searchQuery: string } => { | ||||
|     return { searchQuery: useContext(SearchHighlightContext) }; | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,17 @@ | ||||
| import { Box } from '@mui/material'; | ||||
| import { ReactNode } from 'react'; | ||||
| 
 | ||||
| interface IContextActionsCellProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const ActionCell = ({ children }: IContextActionsCellProps) => { | ||||
|     return ( | ||||
|         <Box | ||||
|             data-loading | ||||
|             sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }} | ||||
|         > | ||||
|             {children} | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,8 +1,8 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||
| import { formatDateYMD, formatDateYMDHMS } from 'utils/formatDate'; | ||||
| import { Box, Tooltip } from '@mui/material'; | ||||
| import { formatDateYMD } from 'utils/formatDate'; | ||||
| import { parseISO } from 'date-fns'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| 
 | ||||
| interface IDateCellProps { | ||||
|     value?: Date | string | null; | ||||
| @ -11,22 +11,11 @@ interface IDateCellProps { | ||||
| export const DateCell: VFC<IDateCellProps> = ({ value }) => { | ||||
|     const { locationSettings } = useLocationSettings(); | ||||
| 
 | ||||
|     if (!value) { | ||||
|         return <Box sx={{ py: 1.5, px: 2 }} />; | ||||
|     } | ||||
|     const date = value | ||||
|         ? value instanceof Date | ||||
|             ? formatDateYMD(value, locationSettings.locale) | ||||
|             : formatDateYMD(parseISO(value), locationSettings.locale) | ||||
|         : undefined; | ||||
| 
 | ||||
|     const date = value instanceof Date ? value : parseISO(value); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ py: 1.5, px: 2 }}> | ||||
|             <Tooltip | ||||
|                 title={formatDateYMDHMS(date, locationSettings.locale)} | ||||
|                 arrow | ||||
|             > | ||||
|                 <span data-loading role="tooltip"> | ||||
|                     {formatDateYMD(date, locationSettings.locale)} | ||||
|                 </span> | ||||
|             </Tooltip> | ||||
|         </Box> | ||||
|     ); | ||||
|     return <TextCell>{date}</TextCell>; | ||||
| }; | ||||
|  | ||||
| @ -0,0 +1,23 @@ | ||||
| import { Box } from '@mui/material'; | ||||
| import { ReactNode } from 'react'; | ||||
| 
 | ||||
| interface IIconCellProps { | ||||
|     icon: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const IconCell = ({ icon }: IIconCellProps) => { | ||||
|     return ( | ||||
|         <Box | ||||
|             data-loading | ||||
|             sx={{ | ||||
|                 pl: 2, | ||||
|                 pr: 1, | ||||
|                 display: 'flex', | ||||
|                 alignItems: 'center', | ||||
|                 minHeight: 60, | ||||
|             }} | ||||
|         > | ||||
|             {icon} | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -20,7 +20,7 @@ export const LinkCell: FC<ILinkCellProps> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const search = useSearchHighlightContext(); | ||||
|     const { searchQuery } = useSearchHighlightContext(); | ||||
| 
 | ||||
|     const content = ( | ||||
|         <div className={styles.container}> | ||||
| @ -32,7 +32,7 @@ export const LinkCell: FC<ILinkCellProps> = ({ | ||||
|                     lineClamp: Boolean(subtitle) ? 1 : 2, | ||||
|                 }} | ||||
|             > | ||||
|                 <Highlighter search={search}>{title}</Highlighter> | ||||
|                 <Highlighter search={searchQuery}>{title}</Highlighter> | ||||
|                 {children} | ||||
|             </span> | ||||
|             <ConditionallyRender | ||||
| @ -44,7 +44,7 @@ export const LinkCell: FC<ILinkCellProps> = ({ | ||||
|                             component="span" | ||||
|                             data-loading | ||||
|                         > | ||||
|                             <Highlighter search={search}> | ||||
|                             <Highlighter search={searchQuery}> | ||||
|                                 {subtitle} | ||||
|                             </Highlighter> | ||||
|                         </Typography> | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| import { VFC, ReactNode } from 'react'; | ||||
| import { Box } from '@mui/material'; | ||||
| 
 | ||||
| interface IDateCellProps { | ||||
|     children?: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const TextCell: VFC<IDateCellProps> = ({ children }) => { | ||||
|     if (!children) { | ||||
|         return <Box sx={{ py: 1.5, px: 2 }} />; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ py: 1.5, px: 2 }}> | ||||
|             <span data-loading role="tooltip"> | ||||
|                 {children} | ||||
|             </span> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -1,12 +1,12 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { Delete, Edit } from '@mui/icons-material'; | ||||
| import { Box } from '@mui/material'; | ||||
| import { | ||||
|     DELETE_CONTEXT_FIELD, | ||||
|     UPDATE_CONTEXT_FIELD, | ||||
| } from 'component/providers/AccessProvider/permissions'; | ||||
| import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; | ||||
| import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; | ||||
| 
 | ||||
| interface IContextActionsCellProps { | ||||
|     name: string; | ||||
| @ -20,14 +20,7 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({ | ||||
|     const navigate = useNavigate(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Box | ||||
|             data-loading | ||||
|             sx={{ | ||||
|                 display: 'flex', | ||||
|                 px: 2, | ||||
|                 justifyContent: 'flex-end', | ||||
|             }} | ||||
|         > | ||||
|         <ActionCell> | ||||
|             <PermissionIconButton | ||||
|                 permission={UPDATE_CONTEXT_FIELD} | ||||
|                 onClick={() => navigate(`/context/edit/${name}`)} | ||||
| @ -50,6 +43,6 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({ | ||||
|             > | ||||
|                 <Delete /> | ||||
|             </PermissionIconButton> | ||||
|         </Box> | ||||
|         </ActionCell> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { sortTypes } from 'utils/sortTypes'; | ||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||
| import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell'; | ||||
| import { Adjust } from '@mui/icons-material'; | ||||
| import { Box } from '@mui/material'; | ||||
| import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | ||||
| 
 | ||||
| const ContextList: VFC = () => { | ||||
|     const [showDelDialogue, setShowDelDialogue] = useState(false); | ||||
| @ -53,19 +53,7 @@ const ContextList: VFC = () => { | ||||
|         () => [ | ||||
|             { | ||||
|                 id: 'Icon', | ||||
|                 Cell: () => ( | ||||
|                     <Box | ||||
|                         data-loading | ||||
|                         sx={{ | ||||
|                             pl: 2, | ||||
|                             pr: 1, | ||||
|                             display: 'flex', | ||||
|                             alignItems: 'center', | ||||
|                         }} | ||||
|                     > | ||||
|                         <Adjust color="disabled" /> | ||||
|                     </Box> | ||||
|                 ), | ||||
|                 Cell: () => <IconCell icon={<Adjust color="disabled" />} />, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Name', | ||||
|  | ||||
| @ -0,0 +1,22 @@ | ||||
| import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; | ||||
| import { Add } from '@mui/icons-material'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| 
 | ||||
| export const CreateEnvironmentButton = () => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const navigate = useNavigate(); | ||||
| 
 | ||||
|     return ( | ||||
|         <ResponsiveButton | ||||
|             onClick={() => navigate('/environments/create')} | ||||
|             maxWidth="700px" | ||||
|             Icon={Add} | ||||
|             permission={ADMIN} | ||||
|             disabled={!Boolean(uiConfig.flags.EEA)} | ||||
|         > | ||||
|             New Environment | ||||
|         </ResponsiveButton> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,193 @@ | ||||
| import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; | ||||
| import { | ||||
|     DELETE_ENVIRONMENT, | ||||
|     UPDATE_ENVIRONMENT, | ||||
| } from 'component/providers/AccessProvider/permissions'; | ||||
| import { | ||||
|     Edit, | ||||
|     Delete, | ||||
|     OfflineBolt, | ||||
|     DragIndicator, | ||||
|     PowerSettingsNew, | ||||
| } from '@mui/icons-material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IconButton, Tooltip } from '@mui/material'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { useContext, useState } from 'react'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import EnvironmentToggleConfirm from '../EnvironmentToggleConfirm/EnvironmentToggleConfirm'; | ||||
| import EnvironmentDeleteConfirm from '../EnvironmentDeleteConfirm/EnvironmentDeleteConfirm'; | ||||
| import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | ||||
| import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; | ||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { useId } from 'hooks/useId'; | ||||
| import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| 
 | ||||
| interface IEnvironmentTableActionsProps { | ||||
|     environment: IEnvironment; | ||||
| } | ||||
| 
 | ||||
| export const EnvironmentActionCell = ({ | ||||
|     environment, | ||||
| }: IEnvironmentTableActionsProps) => { | ||||
|     const navigate = useNavigate(); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const updatePermission = hasAccess(UPDATE_ENVIRONMENT); | ||||
|     const { searchQuery } = useSearchHighlightContext(); | ||||
| 
 | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
|     const { refetchEnvironments } = useEnvironments(); | ||||
|     const { refetch: refetchPermissions } = useProjectRolePermissions(); | ||||
|     const { deleteEnvironment, toggleEnvironmentOn, toggleEnvironmentOff } = | ||||
|         useEnvironmentApi(); | ||||
| 
 | ||||
|     const [deleteModal, setDeleteModal] = useState(false); | ||||
|     const [toggleModal, setToggleModal] = useState(false); | ||||
|     const [confirmName, setConfirmName] = useState(''); | ||||
| 
 | ||||
|     const handleDeleteEnvironment = async () => { | ||||
|         try { | ||||
|             await deleteEnvironment(environment.name); | ||||
|             refetchPermissions(); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment deleted', | ||||
|                 text: 'You have successfully deleted the project environment.', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             setDeleteModal(false); | ||||
|             setConfirmName(''); | ||||
|             await refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleConfirmToggleEnvironment = () => { | ||||
|         return environment.enabled | ||||
|             ? handleToggleEnvironmentOff() | ||||
|             : handleToggleEnvironmentOn(); | ||||
|     }; | ||||
| 
 | ||||
|     const handleToggleEnvironmentOn = async () => { | ||||
|         try { | ||||
|             await toggleEnvironmentOn(environment.name); | ||||
|             setToggleModal(false); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment enabled', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             await refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleToggleEnvironmentOff = async () => { | ||||
|         try { | ||||
|             await toggleEnvironmentOff(environment.name); | ||||
|             setToggleModal(false); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment disabled', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             await refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const toggleIconTooltip = environment.enabled | ||||
|         ? 'Disable environment' | ||||
|         : 'Enable environment'; | ||||
| 
 | ||||
|     const editId = useId(); | ||||
|     const deleteId = useId(); | ||||
| 
 | ||||
|     // Allow drag and drop if the user is permitted to reorder environments.
 | ||||
|     // Disable drag and drop while searching since some rows may be hidden.
 | ||||
|     const enableDragAndDrop = updatePermission && !searchQuery; | ||||
| 
 | ||||
|     return ( | ||||
|         <ActionCell> | ||||
|             <ConditionallyRender | ||||
|                 condition={enableDragAndDrop} | ||||
|                 show={ | ||||
|                     <IconButton size="large"> | ||||
|                         <DragIndicator titleAccess="Drag" cursor="grab" /> | ||||
|                     </IconButton> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={updatePermission} | ||||
|                 show={ | ||||
|                     <Tooltip title={toggleIconTooltip} arrow> | ||||
|                         <IconButton | ||||
|                             onClick={() => setToggleModal(true)} | ||||
|                             size="large" | ||||
|                         > | ||||
|                             <PowerSettingsNew /> | ||||
|                         </IconButton> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={updatePermission} | ||||
|                 show={ | ||||
|                     <Tooltip title="Edit environment" arrow> | ||||
|                         <span id={editId}> | ||||
|                             <IconButton | ||||
|                                 aria-describedby={editId} | ||||
|                                 disabled={environment.protected} | ||||
|                                 onClick={() => { | ||||
|                                     navigate( | ||||
|                                         `/environments/${environment.name}` | ||||
|                                     ); | ||||
|                                 }} | ||||
|                                 size="large" | ||||
|                             > | ||||
|                                 <Edit /> | ||||
|                             </IconButton> | ||||
|                         </span> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={hasAccess(DELETE_ENVIRONMENT)} | ||||
|                 show={ | ||||
|                     <Tooltip title="Delete environment" arrow> | ||||
|                         <span id={deleteId}> | ||||
|                             <IconButton | ||||
|                                 aria-describedby={deleteId} | ||||
|                                 disabled={environment.protected} | ||||
|                                 onClick={() => setDeleteModal(true)} | ||||
|                                 size="large" | ||||
|                             > | ||||
|                                 <Delete /> | ||||
|                             </IconButton> | ||||
|                         </span> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|             <EnvironmentDeleteConfirm | ||||
|                 env={environment} | ||||
|                 setDeldialogue={setDeleteModal} | ||||
|                 open={deleteModal} | ||||
|                 handleDeleteEnvironment={handleDeleteEnvironment} | ||||
|                 confirmName={confirmName} | ||||
|                 setConfirmName={setConfirmName} | ||||
|             /> | ||||
|             <EnvironmentToggleConfirm | ||||
|                 env={environment} | ||||
|                 open={toggleModal} | ||||
|                 setToggleDialog={setToggleModal} | ||||
|                 handleConfirmToggleEnvironment={handleConfirmToggleEnvironment} | ||||
|             /> | ||||
|         </ActionCell> | ||||
|     ); | ||||
| }; | ||||
| @ -1,6 +1,6 @@ | ||||
| import { CloudCircle } from '@mui/icons-material'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import { useStyles } from './EnvironmentCard.styles'; | ||||
| import { useStyles } from 'component/environments/EnvironmentCard/EnvironmentCard.styles'; | ||||
| 
 | ||||
| interface IEnvironmentProps { | ||||
|     name: string; | ||||
| @ -3,13 +3,12 @@ import React from 'react'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import Input from 'component/common/Input/Input'; | ||||
| import EnvironmentCard from '../EnvironmentCard/EnvironmentCard'; | ||||
| import { useStyles } from './EnvironmentDeleteConfirm.styles'; | ||||
| import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard'; | ||||
| import { useStyles } from 'component/environments/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.styles'; | ||||
| 
 | ||||
| interface IEnviromentDeleteConfirmProps { | ||||
|     env: IEnvironment; | ||||
|     open: boolean; | ||||
|     setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>; | ||||
|     setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     handleDeleteEnvironment: () => Promise<void>; | ||||
|     confirmName: string; | ||||
| @ -1,202 +0,0 @@ | ||||
| import { useState } from 'react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { List } from '@mui/material'; | ||||
| import { Add } from '@mui/icons-material'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { IEnvironment, ISortOrderPayload } from 'interfaces/environments'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | ||||
| import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem'; | ||||
| import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm'; | ||||
| import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm'; | ||||
| 
 | ||||
| const EnvironmentList = () => { | ||||
|     const defaultEnv = { | ||||
|         name: '', | ||||
|         type: '', | ||||
|         sortOrder: 0, | ||||
|         createdAt: '', | ||||
|         enabled: true, | ||||
|         protected: false, | ||||
|     }; | ||||
|     const { environments, mutateEnvironments, refetchEnvironments } = | ||||
|         useEnvironments(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { refetch: refetchProjectRolePermissions } = | ||||
|         useProjectRolePermissions(); | ||||
| 
 | ||||
|     const [selectedEnv, setSelectedEnv] = useState(defaultEnv); | ||||
|     const [delDialog, setDeldialogue] = useState(false); | ||||
|     const [toggleDialog, setToggleDialog] = useState(false); | ||||
|     const [confirmName, setConfirmName] = useState(''); | ||||
| 
 | ||||
|     const navigate = useNavigate(); | ||||
|     const { setToastApiError, setToastData } = useToast(); | ||||
|     const { | ||||
|         deleteEnvironment, | ||||
|         changeSortOrder, | ||||
|         toggleEnvironmentOn, | ||||
|         toggleEnvironmentOff, | ||||
|     } = useEnvironmentApi(); | ||||
| 
 | ||||
|     const moveListItem = (dragIndex: number, hoverIndex: number) => { | ||||
|         const newEnvList = [...environments]; | ||||
|         if (newEnvList.length === 0) return newEnvList; | ||||
| 
 | ||||
|         const item = newEnvList.splice(dragIndex, 1)[0]; | ||||
| 
 | ||||
|         newEnvList.splice(hoverIndex, 0, item); | ||||
|         mutateEnvironments(newEnvList); | ||||
|         return newEnvList; | ||||
|     }; | ||||
| 
 | ||||
|     const moveListItemApi = async (dragIndex: number, hoverIndex: number) => { | ||||
|         const newEnvList = moveListItem(dragIndex, hoverIndex); | ||||
|         const sortOrder = newEnvList.reduce( | ||||
|             (acc: ISortOrderPayload, env: IEnvironment, index: number) => { | ||||
|                 acc[env.name] = index + 1; | ||||
|                 return acc; | ||||
|             }, | ||||
|             {} | ||||
|         ); | ||||
| 
 | ||||
|         try { | ||||
|             await sortOrderAPICall(sortOrder); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => { | ||||
|         try { | ||||
|             await changeSortOrder(sortOrder); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleDeleteEnvironment = async () => { | ||||
|         try { | ||||
|             await deleteEnvironment(selectedEnv.name); | ||||
|             refetchProjectRolePermissions(); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment deleted', | ||||
|                 text: 'You have successfully deleted the project environment.', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             setDeldialogue(false); | ||||
|             setSelectedEnv(defaultEnv); | ||||
|             setConfirmName(''); | ||||
|             refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleConfirmToggleEnvironment = () => { | ||||
|         if (selectedEnv.enabled) { | ||||
|             return handleToggleEnvironmentOff(); | ||||
|         } | ||||
|         handleToggleEnvironmentOn(); | ||||
|     }; | ||||
| 
 | ||||
|     const handleToggleEnvironmentOn = async () => { | ||||
|         try { | ||||
|             await toggleEnvironmentOn(selectedEnv.name); | ||||
|             setToggleDialog(false); | ||||
| 
 | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment enabled', | ||||
|                 text: 'Your environment is enabled', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleToggleEnvironmentOff = async () => { | ||||
|         try { | ||||
|             await toggleEnvironmentOff(selectedEnv.name); | ||||
|             setToggleDialog(false); | ||||
|             setToastData({ | ||||
|                 type: 'success', | ||||
|                 title: 'Project environment disabled', | ||||
|                 text: 'Your environment is disabled.', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } finally { | ||||
|             refetchEnvironments(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const environmentList = () => | ||||
|         environments.map((env: IEnvironment, index: number) => ( | ||||
|             <EnvironmentListItem | ||||
|                 key={env.name} | ||||
|                 env={env} | ||||
|                 setDeldialogue={setDeldialogue} | ||||
|                 setSelectedEnv={setSelectedEnv} | ||||
|                 setToggleDialog={setToggleDialog} | ||||
|                 index={index} | ||||
|                 moveListItem={moveListItem} | ||||
|                 moveListItemApi={moveListItemApi} | ||||
|             /> | ||||
|         )); | ||||
| 
 | ||||
|     const navigateToCreateEnvironment = () => { | ||||
|         navigate('/environments/create'); | ||||
|     }; | ||||
|     return ( | ||||
|         <PageContent | ||||
|             header={ | ||||
|                 <PageHeader | ||||
|                     title="Environments" | ||||
|                     actions={ | ||||
|                         <> | ||||
|                             <ResponsiveButton | ||||
|                                 onClick={navigateToCreateEnvironment} | ||||
|                                 maxWidth="700px" | ||||
|                                 Icon={Add} | ||||
|                                 permission={ADMIN} | ||||
|                                 disabled={!Boolean(uiConfig.flags.EEA)} | ||||
|                             > | ||||
|                                 New Environment | ||||
|                             </ResponsiveButton> | ||||
|                         </> | ||||
|                     } | ||||
|                 /> | ||||
|             } | ||||
|         > | ||||
|             <List>{environmentList()}</List> | ||||
|             <EnvironmentDeleteConfirm | ||||
|                 env={selectedEnv} | ||||
|                 setSelectedEnv={setSelectedEnv} | ||||
|                 setDeldialogue={setDeldialogue} | ||||
|                 open={delDialog} | ||||
|                 handleDeleteEnvironment={handleDeleteEnvironment} | ||||
|                 confirmName={confirmName} | ||||
|                 setConfirmName={setConfirmName} | ||||
|             /> | ||||
|             <EnvironmentToggleConfirm | ||||
|                 env={selectedEnv} | ||||
|                 open={toggleDialog} | ||||
|                 setToggleDialog={setToggleDialog} | ||||
|                 handleConfirmToggleEnvironment={handleConfirmToggleEnvironment} | ||||
|             /> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EnvironmentList; | ||||
| @ -1,219 +0,0 @@ | ||||
| import { | ||||
|     ListItem, | ||||
|     ListItemIcon, | ||||
|     ListItemText, | ||||
|     Tooltip, | ||||
|     IconButton, | ||||
| } from '@mui/material'; | ||||
| import { | ||||
|     CloudCircle, | ||||
|     Delete, | ||||
|     DragIndicator, | ||||
|     Edit, | ||||
|     OfflineBolt, | ||||
| } from '@mui/icons-material'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import React, { useContext, useRef } from 'react'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { | ||||
|     DELETE_ENVIRONMENT, | ||||
|     UPDATE_ENVIRONMENT, | ||||
| } from 'component/providers/AccessProvider/permissions'; | ||||
| import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; | ||||
| import { XYCoord, Identifier } from 'dnd-core'; | ||||
| import StringTruncator from 'component/common/StringTruncator/StringTruncator'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; | ||||
| 
 | ||||
| interface IEnvironmentListItemProps { | ||||
|     env: IEnvironment; | ||||
|     setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>; | ||||
|     setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     index: number; | ||||
|     moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[]; | ||||
|     moveListItemApi: (dragIndex: number, hoverIndex: number) => Promise<void>; | ||||
| } | ||||
| 
 | ||||
| interface IDragItem { | ||||
|     index: number; | ||||
|     id: string; | ||||
|     type: string; | ||||
| } | ||||
| 
 | ||||
| interface ICollectedProps { | ||||
|     handlerId: Identifier | null; | ||||
| } | ||||
| 
 | ||||
| const EnvironmentListItem = ({ | ||||
|     env, | ||||
|     setSelectedEnv, | ||||
|     setDeldialogue, | ||||
|     index, | ||||
|     moveListItem, | ||||
|     moveListItemApi, | ||||
|     setToggleDialog, | ||||
| }: IEnvironmentListItemProps) => { | ||||
|     const navigate = useNavigate(); | ||||
|     const ref = useRef<HTMLLIElement>(null); | ||||
|     const ACCEPT_TYPE = 'LIST_ITEM'; | ||||
|     const [{ isDragging }, drag] = useDrag({ | ||||
|         type: ACCEPT_TYPE, | ||||
|         item: () => { | ||||
|             return { env, index }; | ||||
|         }, | ||||
|         collect: (monitor: any) => ({ | ||||
|             isDragging: monitor.isDragging(), | ||||
|         }), | ||||
|     }); | ||||
| 
 | ||||
|     const [{ handlerId }, drop] = useDrop<IDragItem, unknown, ICollectedProps>({ | ||||
|         accept: ACCEPT_TYPE, | ||||
|         collect(monitor) { | ||||
|             return { | ||||
|                 handlerId: monitor.getHandlerId(), | ||||
|             }; | ||||
|         }, | ||||
|         drop(item: IDragItem, monitor: DropTargetMonitor) { | ||||
|             const dragIndex = item.index; | ||||
|             const hoverIndex = index; | ||||
|             moveListItemApi(dragIndex, hoverIndex); | ||||
|         }, | ||||
|         hover(item: IDragItem, monitor: DropTargetMonitor) { | ||||
|             if (!ref.current) { | ||||
|                 return; | ||||
|             } | ||||
|             const dragIndex = item.index; | ||||
|             const hoverIndex = index; | ||||
| 
 | ||||
|             if (dragIndex === hoverIndex) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const hoverBoundingRect = ref.current?.getBoundingClientRect(); | ||||
| 
 | ||||
|             const hoverMiddleY = | ||||
|                 (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; | ||||
| 
 | ||||
|             const clientOffset = monitor.getClientOffset(); | ||||
| 
 | ||||
|             const hoverClientY = | ||||
|                 (clientOffset as XYCoord).y - hoverBoundingRect.top; | ||||
| 
 | ||||
|             if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             moveListItem(dragIndex, hoverIndex); | ||||
|             item.index = hoverIndex; | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     const opacity = isDragging ? 0 : 1; | ||||
| 
 | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const updatePermission = hasAccess(UPDATE_ENVIRONMENT); | ||||
|     const tooltipText = env.enabled ? 'Disable' : 'Enable'; | ||||
| 
 | ||||
|     if (updatePermission) { | ||||
|         drag(drop(ref)); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <ListItem | ||||
|             style={{ position: 'relative', opacity }} | ||||
|             ref={ref} | ||||
|             data-handler-id={handlerId} | ||||
|         > | ||||
|             <ListItemIcon> | ||||
|                 <CloudCircle /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText | ||||
|                 primary={ | ||||
|                     <> | ||||
|                         <strong> | ||||
|                             <StringTruncator | ||||
|                                 text={env.name} | ||||
|                                 maxWidth={'125'} | ||||
|                                 maxLength={25} | ||||
|                             /> | ||||
|                         </strong> | ||||
|                         <ConditionallyRender | ||||
|                             condition={!env.enabled} | ||||
|                             show={ | ||||
|                                 <StatusBadge severity="warning"> | ||||
|                                     Disabled | ||||
|                                 </StatusBadge> | ||||
|                             } | ||||
|                         /> | ||||
|                     </> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={updatePermission} | ||||
|                 show={ | ||||
|                     <IconButton size="large"> | ||||
|                         <DragIndicator titleAccess="Drag" cursor="grab" /> | ||||
|                     </IconButton> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={updatePermission} | ||||
|                 show={ | ||||
|                     <Tooltip title={`${tooltipText} environment`} arrow> | ||||
|                         <IconButton | ||||
|                             onClick={() => { | ||||
|                                 setSelectedEnv(env); | ||||
|                                 setToggleDialog(prev => !prev); | ||||
|                             }} | ||||
|                             size="large" | ||||
|                         > | ||||
|                             <OfflineBolt /> | ||||
|                         </IconButton> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={updatePermission} | ||||
|                 show={ | ||||
|                     <Tooltip title="Edit environment" arrow> | ||||
|                         <IconButton | ||||
|                             disabled={env.protected} | ||||
|                             onClick={() => { | ||||
|                                 navigate(`/environments/${env.name}`); | ||||
|                             }} | ||||
|                             size="large" | ||||
|                         > | ||||
|                             <Edit /> | ||||
|                         </IconButton> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={hasAccess(DELETE_ENVIRONMENT)} | ||||
|                 show={ | ||||
|                     <Tooltip title="Delete environment" arrow> | ||||
|                         <IconButton | ||||
|                             disabled={env.protected} | ||||
|                             onClick={() => { | ||||
|                                 setDeldialogue(true); | ||||
|                                 setSelectedEnv(env); | ||||
|                             }} | ||||
|                             size="large" | ||||
|                         > | ||||
|                             <Delete /> | ||||
|                         </IconButton> | ||||
|                     </Tooltip> | ||||
|                 } | ||||
|             /> | ||||
|         </ListItem> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EnvironmentListItem; | ||||
| @ -0,0 +1,22 @@ | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; | ||||
| 
 | ||||
| interface IEnvironmentNameCellProps { | ||||
|     environment: IEnvironment; | ||||
| } | ||||
| 
 | ||||
| export const EnvironmentNameCell = ({ | ||||
|     environment, | ||||
| }: IEnvironmentNameCellProps) => { | ||||
|     return ( | ||||
|         <TextCell> | ||||
|             {environment.name} | ||||
|             <ConditionallyRender | ||||
|                 condition={!environment.enabled} | ||||
|                 show={<StatusBadge severity="warning">Disabled</StatusBadge>} | ||||
|             /> | ||||
|         </TextCell> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,26 @@ | ||||
| import { useDragItem, MoveListItem } from 'hooks/useDragItem'; | ||||
| import { Row } from 'react-table'; | ||||
| import { TableRow } from '@mui/material'; | ||||
| import { TableCell } from 'component/common/Table'; | ||||
| import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| 
 | ||||
| interface IEnvironmentRowProps { | ||||
|     row: Row; | ||||
|     moveListItem: MoveListItem; | ||||
| } | ||||
| 
 | ||||
| export const EnvironmentRow = ({ row, moveListItem }: IEnvironmentRowProps) => { | ||||
|     const dragItemRef = useDragItem(row.index, moveListItem); | ||||
|     const { searchQuery } = useSearchHighlightContext(); | ||||
|     const draggable = !searchQuery; | ||||
| 
 | ||||
|     return ( | ||||
|         <TableRow hover ref={draggable ? dragItemRef : undefined}> | ||||
|             {row.cells.map((cell: any) => ( | ||||
|                 <TableCell {...cell.getCellProps()}> | ||||
|                     {cell.render('Cell')} | ||||
|                 </TableCell> | ||||
|             ))} | ||||
|         </TableRow> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,128 @@ | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton'; | ||||
| import { useTable, useGlobalFilter } from 'react-table'; | ||||
| import { | ||||
|     TableSearch, | ||||
|     SortableTableHeader, | ||||
|     Table, | ||||
| } from 'component/common/Table'; | ||||
| import { useCallback } from 'react'; | ||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| import { TableBody } from '@mui/material'; | ||||
| import { CloudCircle } from '@mui/icons-material'; | ||||
| import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; | ||||
| import { EnvironmentActionCell } from 'component/environments/EnvironmentActionCell/EnvironmentActionCell'; | ||||
| import { EnvironmentNameCell } from 'component/environments/EnvironmentNameCell/EnvironmentNameCell'; | ||||
| import { EnvironmentRow } from 'component/environments/EnvironmentRow/EnvironmentRow'; | ||||
| import { MoveListItem } from 'hooks/useDragItem'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import useEnvironmentApi, { | ||||
|     createSortOrderPayload, | ||||
| } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| 
 | ||||
| export const EnvironmentTable = () => { | ||||
|     const { changeSortOrder } = useEnvironmentApi(); | ||||
|     const { setToastApiError } = useToast(); | ||||
|     const { environments, mutateEnvironments } = useEnvironments(); | ||||
| 
 | ||||
|     const moveListItem: MoveListItem = useCallback( | ||||
|         async (dragIndex: number, dropIndex: number, save = false) => { | ||||
|             const copy = [...environments]; | ||||
|             const tmp = copy[dragIndex]; | ||||
|             copy.splice(dragIndex, 1); | ||||
|             copy.splice(dropIndex, 0, tmp); | ||||
|             await mutateEnvironments(copy); | ||||
| 
 | ||||
|             if (save) { | ||||
|                 try { | ||||
|                     await changeSortOrder(createSortOrderPayload(copy)); | ||||
|                 } catch (error: unknown) { | ||||
|                     setToastApiError(formatUnknownError(error)); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         [changeSortOrder, environments, mutateEnvironments, setToastApiError] | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
|         getTableProps, | ||||
|         getTableBodyProps, | ||||
|         headerGroups, | ||||
|         rows, | ||||
|         prepareRow, | ||||
|         state: { globalFilter }, | ||||
|         setGlobalFilter, | ||||
|     } = useTable( | ||||
|         { | ||||
|             columns: COLUMNS as any, | ||||
|             data: environments, | ||||
|             autoResetGlobalFilter: false, | ||||
|         }, | ||||
|         useGlobalFilter | ||||
|     ); | ||||
| 
 | ||||
|     const headerSearch = ( | ||||
|         <TableSearch initialValue={globalFilter} onChange={setGlobalFilter} /> | ||||
|     ); | ||||
| 
 | ||||
|     const headerActions = ( | ||||
|         <> | ||||
|             {headerSearch} | ||||
|             <PageHeader.Divider /> | ||||
|             <CreateEnvironmentButton /> | ||||
|         </> | ||||
|     ); | ||||
| 
 | ||||
|     const header = <PageHeader title="Environments" actions={headerActions} />; | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent header={header}> | ||||
|             <SearchHighlightProvider value={globalFilter}> | ||||
|                 <Table {...getTableProps()}> | ||||
|                     <SortableTableHeader headerGroups={headerGroups as any} /> | ||||
|                     <TableBody {...getTableBodyProps()}> | ||||
|                         {rows.map(row => { | ||||
|                             prepareRow(row); | ||||
|                             return ( | ||||
|                                 <EnvironmentRow | ||||
|                                     row={row as any} | ||||
|                                     moveListItem={moveListItem} | ||||
|                                     key={row.original.name} | ||||
|                                 /> | ||||
|                             ); | ||||
|                         })} | ||||
|                     </TableBody> | ||||
|                 </Table> | ||||
|             </SearchHighlightProvider> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const COLUMNS = [ | ||||
|     { | ||||
|         id: 'Icon', | ||||
|         canSort: false, | ||||
|         Cell: () => <IconCell icon={<CloudCircle color="disabled" />} />, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Name', | ||||
|         accessor: 'name', | ||||
|         width: '100%', | ||||
|         canSort: false, | ||||
|         Cell: (props: any) => ( | ||||
|             <EnvironmentNameCell environment={props.row.original} /> | ||||
|         ), | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Actions', | ||||
|         id: 'Actions', | ||||
|         align: 'center', | ||||
|         canSort: false, | ||||
|         Cell: (props: any) => ( | ||||
|             <EnvironmentActionCell environment={props.row.original} /> | ||||
|         ), | ||||
|     }, | ||||
| ]; | ||||
| @ -4,7 +4,7 @@ import React from 'react'; | ||||
| import { IEnvironment } from 'interfaces/environments'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import EnvironmentCard from '../EnvironmentCard/EnvironmentCard'; | ||||
| import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard'; | ||||
| 
 | ||||
| interface IEnvironmentToggleConfirmProps { | ||||
|     env: IEnvironment; | ||||
| @ -18,7 +18,6 @@ import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPasswor | ||||
| import { ProjectListNew } from 'component/project/ProjectList/ProjectList'; | ||||
| import Project from 'component/project/Project/Project'; | ||||
| import RedirectArchive from 'component/archive/RedirectArchive'; | ||||
| import EnvironmentList from 'component/environments/EnvironmentList/EnvironmentList'; | ||||
| import { FeatureView } from 'component/feature/FeatureView/FeatureView'; | ||||
| import ProjectRoles from 'component/admin/projectRoles/ProjectRoles/ProjectRoles'; | ||||
| import CreateProjectRole from 'component/admin/projectRoles/CreateProjectRole/CreateProjectRole'; | ||||
| @ -52,6 +51,7 @@ import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment'; | ||||
| import { EditSegment } from 'component/segments/EditSegment/EditSegment'; | ||||
| import { SegmentsList } from 'component/segments/SegmentList/SegmentList'; | ||||
| import { IRoute } from 'interfaces/route'; | ||||
| import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable'; | ||||
| 
 | ||||
| export const routes: IRoute[] = [ | ||||
|     // Splash
 | ||||
| @ -266,7 +266,7 @@ export const routes: IRoute[] = [ | ||||
|     { | ||||
|         path: '/environments', | ||||
|         title: 'Environments', | ||||
|         component: EnvironmentList, | ||||
|         component: EnvironmentTable, | ||||
|         type: 'protected', | ||||
|         flag: EEA, | ||||
|         menu: { mobile: true, advanced: true }, | ||||
|  | ||||
| @ -10,12 +10,12 @@ interface IUIContext { | ||||
| 
 | ||||
| export const createEmptyToast = (): IToast => { | ||||
|     return { | ||||
|         type: 'success', | ||||
|         title: '', | ||||
|         text: '', | ||||
|         components: [], | ||||
|         show: false, | ||||
|         persist: false, | ||||
|         type: '', | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { | ||||
|     IEnvironmentPayload, | ||||
|     ISortOrderPayload, | ||||
|     IEnvironmentEditPayload, | ||||
|     IEnvironment, | ||||
| } from 'interfaces/environments'; | ||||
| import useAPI from '../useApi/useApi'; | ||||
| 
 | ||||
| @ -145,4 +146,13 @@ const useEnvironmentApi = () => { | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export const createSortOrderPayload = ( | ||||
|     environments: Readonly<IEnvironment[]> | ||||
| ): ISortOrderPayload => { | ||||
|     return environments.reduce((payload, env, index) => { | ||||
|         payload[env.name] = index + 1; | ||||
|         return payload; | ||||
|     }, {} as ISortOrderPayload); | ||||
| }; | ||||
| 
 | ||||
| export default useEnvironmentApi; | ||||
|  | ||||
| @ -13,13 +13,13 @@ interface IUseEnvironmentsOutput { | ||||
| } | ||||
| 
 | ||||
| export const useEnvironments = (): IUseEnvironmentsOutput => { | ||||
|     const { data, error, mutate } = useSWR<IEnvironmentResponse>( | ||||
|     const { data, error, mutate } = useSWR<IEnvironment[]>( | ||||
|         formatApiPath(`api/admin/environments`), | ||||
|         fetcher | ||||
|     ); | ||||
| 
 | ||||
|     const environments = useMemo(() => { | ||||
|         return data?.environments || []; | ||||
|         return data || []; | ||||
|     }, [data]); | ||||
| 
 | ||||
|     const refetchEnvironments = useCallback(async () => { | ||||
| @ -28,7 +28,7 @@ export const useEnvironments = (): IUseEnvironmentsOutput => { | ||||
| 
 | ||||
|     const mutateEnvironments = useCallback( | ||||
|         async (environments: IEnvironment[]) => { | ||||
|             await mutate({ environments }, false); | ||||
|             await mutate(environments, false); | ||||
|         }, | ||||
|         [mutate] | ||||
|     ); | ||||
| @ -42,8 +42,12 @@ export const useEnvironments = (): IUseEnvironmentsOutput => { | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const fetcher = (path: string): Promise<IEnvironmentResponse> => { | ||||
|     return fetch(path) | ||||
| const fetcher = async (path: string): Promise<IEnvironment[]> => { | ||||
|     const res: IEnvironmentResponse = await fetch(path) | ||||
|         .then(handleErrorResponses('Environments')) | ||||
|         .then(res => res.json()); | ||||
| 
 | ||||
|     return res.environments.sort((a, b) => { | ||||
|         return a.sortOrder - b.sortOrder; | ||||
|     }); | ||||
| }; | ||||
|  | ||||
							
								
								
									
										72
									
								
								frontend/src/hooks/useDragItem.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								frontend/src/hooks/useDragItem.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| import { useRef, useEffect, RefObject } from 'react'; | ||||
| 
 | ||||
| export type MoveListItem = ( | ||||
|     dragIndex: number, | ||||
|     dropIndex: number, | ||||
|     save?: boolean | ||||
| ) => void; | ||||
| 
 | ||||
| export const useDragItem = ( | ||||
|     listItemIndex: number, | ||||
|     moveListItem: MoveListItem | ||||
| ): RefObject<HTMLTableRowElement> => { | ||||
|     const ref = useRef<HTMLTableRowElement>(null); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (ref.current) { | ||||
|             ref.current.draggable = true; | ||||
|             ref.current.style.cursor = 'grab'; | ||||
|             ref.current.dataset.index = String(listItemIndex); | ||||
|             return addEventListeners(ref.current, moveListItem); | ||||
|         } | ||||
|     }, [listItemIndex, moveListItem]); | ||||
| 
 | ||||
|     return ref; | ||||
| }; | ||||
| 
 | ||||
| const addEventListeners = ( | ||||
|     el: HTMLTableRowElement, | ||||
|     moveListItem: MoveListItem | ||||
| ): (() => void) => { | ||||
|     const moveDraggedElement = (save: boolean) => { | ||||
|         if (globalDraggedElement) { | ||||
|             moveListItem( | ||||
|                 Number(globalDraggedElement.dataset.index), | ||||
|                 Number(el.dataset.index), | ||||
|                 save | ||||
|             ); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onDragStart = () => { | ||||
|         globalDraggedElement = el; | ||||
|     }; | ||||
| 
 | ||||
|     const onDragEnter = () => { | ||||
|         moveDraggedElement(false); | ||||
|     }; | ||||
| 
 | ||||
|     const onDragOver = (event: DragEvent) => { | ||||
|         event.preventDefault(); | ||||
|     }; | ||||
| 
 | ||||
|     const onDrop = () => { | ||||
|         moveDraggedElement(true); | ||||
|         globalDraggedElement = null; | ||||
|     }; | ||||
| 
 | ||||
|     el.addEventListener('dragstart', onDragStart); | ||||
|     el.addEventListener('dragenter', onDragEnter); | ||||
|     el.addEventListener('dragover', onDragOver); | ||||
|     el.addEventListener('drop', onDrop); | ||||
| 
 | ||||
|     return () => { | ||||
|         el.removeEventListener('dragstart', onDragStart); | ||||
|         el.removeEventListener('dragenter', onDragEnter); | ||||
|         el.removeEventListener('dragover', onDragOver); | ||||
|         el.removeEventListener('drop', onDrop); | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| // The element being dragged in the browser.
 | ||||
| let globalDraggedElement: HTMLTableRowElement | null; | ||||
| @ -4,8 +4,6 @@ import 'regenerator-runtime/runtime'; | ||||
| 
 | ||||
| import ReactDOM from 'react-dom'; | ||||
| import { BrowserRouter } from 'react-router-dom'; | ||||
| import { DndProvider } from 'react-dnd'; | ||||
| import { HTML5Backend } from 'react-dnd-html5-backend'; | ||||
| import { ThemeProvider } from 'themes/ThemeProvider'; | ||||
| import { App } from 'component/App'; | ||||
| import { ScrollTop } from 'component/common/ScrollTop/ScrollTop'; | ||||
| @ -17,23 +15,21 @@ import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/ | ||||
| import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus'; | ||||
| 
 | ||||
| ReactDOM.render( | ||||
|     <DndProvider backend={HTML5Backend}> | ||||
|         <UIProvider> | ||||
|             <AccessProvider> | ||||
|                 <BrowserRouter basename={basePath}> | ||||
|                     <ThemeProvider> | ||||
|                         <AnnouncerProvider> | ||||
|                             <FeedbackCESProvider> | ||||
|                                 <InstanceStatus> | ||||
|                                     <ScrollTop /> | ||||
|                                     <App /> | ||||
|                                 </InstanceStatus> | ||||
|                             </FeedbackCESProvider> | ||||
|                         </AnnouncerProvider> | ||||
|                     </ThemeProvider> | ||||
|                 </BrowserRouter> | ||||
|             </AccessProvider> | ||||
|         </UIProvider> | ||||
|     </DndProvider>, | ||||
|     <UIProvider> | ||||
|         <AccessProvider> | ||||
|             <BrowserRouter basename={basePath}> | ||||
|                 <ThemeProvider> | ||||
|                     <AnnouncerProvider> | ||||
|                         <FeedbackCESProvider> | ||||
|                             <InstanceStatus> | ||||
|                                 <ScrollTop /> | ||||
|                                 <App /> | ||||
|                             </InstanceStatus> | ||||
|                         </FeedbackCESProvider> | ||||
|                     </AnnouncerProvider> | ||||
|                 </ThemeProvider> | ||||
|             </BrowserRouter> | ||||
|         </AccessProvider> | ||||
|     </UIProvider>, | ||||
|     document.getElementById('app') | ||||
| ); | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| export interface IToast { | ||||
|     type: string; | ||||
|     type: 'success' | 'error'; | ||||
|     title: string; | ||||
|     text?: string; | ||||
|     components?: JSX.Element[]; | ||||
|  | ||||
| @ -1,5 +0,0 @@ | ||||
| { | ||||
|   "globalDependencies": { | ||||
|     "react-dnd": "registry:dt/react-dnd#2.0.2+20161111212335", | ||||
|   } | ||||
| } | ||||
| @ -1538,21 +1538,6 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" | ||||
|   integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== | ||||
| 
 | ||||
| "@react-dnd/asap@4.0.1": | ||||
|   version "4.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab" | ||||
|   integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== | ||||
| 
 | ||||
| "@react-dnd/invariant@3.0.1": | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.1.tgz#7e70be19ea21b539e8bf1da28466f4f05df2a4cc" | ||||
|   integrity sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g== | ||||
| 
 | ||||
| "@react-dnd/shallowequal@3.0.1": | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-3.0.1.tgz#8056fe046a8d10a275e321ec0557ae652d7a4d06" | ||||
|   integrity sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA== | ||||
| 
 | ||||
| "@rollup/pluginutils@^4.2.1": | ||||
|   version "4.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" | ||||
| @ -3040,15 +3025,6 @@ dir-glob@^3.0.1: | ||||
|   dependencies: | ||||
|     path-type "^4.0.0" | ||||
| 
 | ||||
| dnd-core@15.1.2: | ||||
|   version "15.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.2.tgz#0983bce555c4985f58b731ffe1faed31e1ea7f6f" | ||||
|   integrity sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A== | ||||
|   dependencies: | ||||
|     "@react-dnd/asap" "4.0.1" | ||||
|     "@react-dnd/invariant" "3.0.1" | ||||
|     redux "^4.1.2" | ||||
| 
 | ||||
| doctrine@^2.1.0: | ||||
|   version "2.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" | ||||
| @ -5149,24 +5125,6 @@ react-chartjs-2@4.1.0: | ||||
|   resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d" | ||||
|   integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g== | ||||
| 
 | ||||
| react-dnd-html5-backend@15.1.3: | ||||
|   version "15.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.3.tgz#57b4f47e0f23923e7c243d2d0eefe490069115a9" | ||||
|   integrity sha512-HH/8nOEmrrcRGHMqJR91FOwhnLlx5SRLXmsQwZT3IPcBjx88WT+0pWC5A4tDOYDdoooh9k+KMPvWfxooR5TcOA== | ||||
|   dependencies: | ||||
|     dnd-core "15.1.2" | ||||
| 
 | ||||
| react-dnd@15.1.2: | ||||
|   version "15.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.2.tgz#211b30fd842326209c63f26f1bdf1bc52eef4f64" | ||||
|   integrity sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA== | ||||
|   dependencies: | ||||
|     "@react-dnd/invariant" "3.0.1" | ||||
|     "@react-dnd/shallowequal" "3.0.1" | ||||
|     dnd-core "15.1.2" | ||||
|     fast-deep-equal "^3.1.3" | ||||
|     hoist-non-react-statics "^3.3.2" | ||||
| 
 | ||||
| react-dom@17.0.2: | ||||
|   version "17.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" | ||||
| @ -5303,13 +5261,6 @@ redent@^3.0.0: | ||||
|     indent-string "^4.0.0" | ||||
|     strip-indent "^3.0.0" | ||||
| 
 | ||||
| redux@^4.1.2: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" | ||||
|   integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== | ||||
|   dependencies: | ||||
|     "@babel/runtime" "^7.9.2" | ||||
| 
 | ||||
| reflect-metadata@0.1.13: | ||||
|   version "0.1.13" | ||||
|   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user