mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: project change request page frontend (#2361)
- Refactor project menu - merge "Access" and "Environments" with newly added "Change request configuration" into "Project settings" tab. - Add Change request config page with table - Add Change request dialog Closes https://linear.app/unleash/issue/1-344/frontend-project-options Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: #2251
This commit is contained in:
		
							parent
							
								
									0649262c70
								
							
						
					
					
						commit
						45ee135037
					
				| @ -29,6 +29,7 @@ import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner'; | ||||
| import { MainLayout } from 'component/layout/MainLayout/MainLayout'; | ||||
| import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests'; | ||||
| import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; | ||||
| import { ProjectSettings } from './ProjectSettings/ProjectSettings'; | ||||
| 
 | ||||
| const StyledDiv = styled('div')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -58,7 +59,7 @@ const Project = () => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const navigate = useNavigate(); | ||||
|     const { pathname } = useLocation(); | ||||
|     const { isOss } = useUiConfig(); | ||||
|     const { isOss, uiConfig } = useUiConfig(); | ||||
|     const basePath = `/projects/${projectId}`; | ||||
|     const projectName = project?.name || projectId; | ||||
| 
 | ||||
| @ -78,21 +79,34 @@ const Project = () => { | ||||
|                 path: `${basePath}/health`, | ||||
|                 name: 'health', | ||||
|             }, | ||||
|             { | ||||
|                 title: 'Access', | ||||
|                 path: `${basePath}/access`, | ||||
|                 name: 'access', | ||||
|             }, | ||||
|             { | ||||
|                 title: 'Environments', | ||||
|                 path: `${basePath}/environments`, | ||||
|                 name: 'environments', | ||||
|             }, | ||||
|             ...(!uiConfig?.flags?.changeRequests | ||||
|                 ? [ | ||||
|                       { | ||||
|                           title: 'Access', | ||||
|                           path: `${basePath}/access`, | ||||
|                           name: 'access', | ||||
|                       }, | ||||
|                       { | ||||
|                           title: 'Environments', | ||||
|                           path: `${basePath}/environments`, | ||||
|                           name: 'environments', | ||||
|                       }, | ||||
|                   ] | ||||
|                 : []), | ||||
|             { | ||||
|                 title: 'Archive', | ||||
|                 path: `${basePath}/archive`, | ||||
|                 name: 'archive', | ||||
|             }, | ||||
|             ...(uiConfig?.flags?.changeRequests | ||||
|                 ? [ | ||||
|                       { | ||||
|                           title: 'Project settings', | ||||
|                           path: `${basePath}/settings`, | ||||
|                           name: 'settings', | ||||
|                       }, | ||||
|                   ] | ||||
|                 : []), | ||||
|             { | ||||
|                 title: 'Event log', | ||||
|                 path: `${basePath}/logs`, | ||||
| @ -263,6 +277,7 @@ const Project = () => { | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|                 <Route path="settings/*" element={<ProjectSettings />} /> | ||||
|                 <Route path="*" element={<ProjectOverview />} /> | ||||
|             </Routes> | ||||
|         </MainLayout> | ||||
|  | ||||
| @ -0,0 +1,166 @@ | ||||
| import { useMemo, useState, VFC } from 'react'; | ||||
| import { HeaderGroup, Row } from 'react-table'; | ||||
| import { Alert, Box, Typography } from '@mui/material'; | ||||
| import { | ||||
|     SortableTableHeader, | ||||
|     Table, | ||||
|     TableCell, | ||||
|     TableBody, | ||||
|     TableRow, | ||||
| } from 'component/common/Table'; | ||||
| import { useGlobalFilter, useTable } from 'react-table'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch'; | ||||
| import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions'; | ||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| export const ChangeRequestConfiguration: VFC = () => { | ||||
|     const [dialogState, setDialogState] = useState<{ | ||||
|         isOpen: boolean; | ||||
|         enableEnvironment?: string; | ||||
|     }>({ | ||||
|         isOpen: false, | ||||
|         enableEnvironment: '', | ||||
|     }); | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const data = [ | ||||
|         { | ||||
|             environment: 'dev', | ||||
|             type: 'test', | ||||
|             isEnabled: false, | ||||
|         }, | ||||
|     ] as any[]; // FIXME: type
 | ||||
| 
 | ||||
|     const onClick = (enableEnvironment: string) => () => { | ||||
|         setDialogState({ isOpen: true, enableEnvironment }); | ||||
|     }; | ||||
| 
 | ||||
|     const columns = useMemo( | ||||
|         () => [ | ||||
|             { | ||||
|                 Header: 'Environment', | ||||
|                 accessor: 'environment', | ||||
|                 disableSortBy: true, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Type', | ||||
|                 accessor: 'type', | ||||
|                 disableGlobalFilter: true, | ||||
|                 disableSortBy: true, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Status', | ||||
|                 accessor: 'isEnabled', | ||||
|                 align: 'center', | ||||
| 
 | ||||
|                 Cell: ({ value, row: { original } }: any) => ( | ||||
|                     <Box | ||||
|                         sx={{ display: 'flex', justifyContent: 'center' }} | ||||
|                         data-loading | ||||
|                     > | ||||
|                         <PermissionSwitch | ||||
|                             checked={value} | ||||
|                             environmentId={original.environment} | ||||
|                             projectId={projectId} | ||||
|                             permission={UPDATE_FEATURE_ENVIRONMENT} // FIXME: permission - enable change request
 | ||||
|                             inputProps={{ 'aria-label': original.environment }} | ||||
|                             onClick={onClick(original.environment)} | ||||
|                         /> | ||||
|                     </Box> | ||||
|                 ), | ||||
|                 width: 100, | ||||
|                 disableGlobalFilter: true, | ||||
|                 disableSortBy: true, | ||||
|             }, | ||||
|         ], | ||||
|         [] | ||||
|     ); | ||||
| 
 | ||||
|     const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = | ||||
|         useTable( | ||||
|             { | ||||
|                 columns, | ||||
|                 data, | ||||
|                 sortTypes, | ||||
|                 autoResetGlobalFilter: false, | ||||
|                 disableSortRemove: true, | ||||
|                 defaultColumn: { | ||||
|                     Cell: TextCell, | ||||
|                 }, | ||||
|             }, | ||||
|             useGlobalFilter | ||||
|         ); | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent | ||||
|             header={<PageHeader titleElement="Change request configuration" />} | ||||
|             // isLoading={loading}
 | ||||
|         > | ||||
|             <Alert severity="info" sx={{ mb: 3 }}> | ||||
|                 If change request is enabled for an environment, then any change | ||||
|                 in that environment needs to be approved before it will be | ||||
|                 applied | ||||
|             </Alert> | ||||
| 
 | ||||
|             <Table {...getTableProps()}> | ||||
|                 <SortableTableHeader | ||||
|                     headerGroups={headerGroups as HeaderGroup<object>[]} | ||||
|                 /> | ||||
|                 <TableBody {...getTableBodyProps()}> | ||||
|                     {rows.map(row => { | ||||
|                         prepareRow(row); | ||||
|                         return ( | ||||
|                             <TableRow hover {...row.getRowProps()}> | ||||
|                                 {row.cells.map(cell => ( | ||||
|                                     <TableCell {...cell.getCellProps()}> | ||||
|                                         {cell.render('Cell')} | ||||
|                                     </TableCell> | ||||
|                                 ))} | ||||
|                             </TableRow> | ||||
|                         ); | ||||
|                     })} | ||||
|                 </TableBody> | ||||
|             </Table> | ||||
| 
 | ||||
|             <Dialogue | ||||
|                 onClick={() => { | ||||
|                     alert('clicked'); | ||||
|                     /* FIXME: API action */ | ||||
|                 }} | ||||
|                 open={dialogState.isOpen} | ||||
|                 onClose={() => | ||||
|                     setDialogState(state => ({ ...state, isOpen: false })) | ||||
|                 } | ||||
|                 primaryButtonText="Enable" | ||||
|                 secondaryButtonText="Cancel" | ||||
|                 title="Enable change request" | ||||
|             > | ||||
|                 <Typography sx={{ mb: 1 }}> | ||||
|                     You are about to enable “Change request” | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(dialogState.enableEnvironment)} | ||||
|                         show={ | ||||
|                             <> | ||||
|                                 {' '} | ||||
|                                 for{' '} | ||||
|                                 <strong>{dialogState.enableEnvironment}</strong> | ||||
|                             </> | ||||
|                         } | ||||
|                     /> | ||||
|                     . | ||||
|                 </Typography> | ||||
|                 <Typography variant="body2" color="text.secondary"> | ||||
|                     When enabling change request for an environment, you need to | ||||
|                     be sure that your Unleash Admin already have created the | ||||
|                     custom project roles in your Unleash instance so you can | ||||
|                     assign your project members from the project access page. | ||||
|                 </Typography> | ||||
|             </Dialogue> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,54 @@ | ||||
| import { | ||||
|     Route, | ||||
|     Routes, | ||||
|     useLocation, | ||||
|     useNavigate, | ||||
|     Navigate, | ||||
| } from 'react-router-dom'; | ||||
| import { ITab, VerticalTabs } from 'component/common/VerticalTabs/VerticalTabs'; | ||||
| import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess'; | ||||
| import ProjectEnvironmentList from 'component/project/ProjectEnvironment/ProjectEnvironment'; | ||||
| import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeRequestConfiguration'; | ||||
| 
 | ||||
| export const ProjectSettings = () => { | ||||
|     const location = useLocation(); | ||||
|     const navigate = useNavigate(); | ||||
| 
 | ||||
|     const tabs = [ | ||||
|         { id: 'access', label: 'Access' }, | ||||
|         { id: 'environments', label: 'Environments' }, | ||||
|         { id: 'change-requests', label: 'Change request configuration' }, | ||||
|     ]; | ||||
| 
 | ||||
|     const onChange = (tab: ITab) => { | ||||
|         navigate(tab.id); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <VerticalTabs | ||||
|             tabs={tabs} | ||||
|             value={ | ||||
|                 tabs.find( | ||||
|                     ({ id }) => id && location.pathname?.includes(`/${id}`) | ||||
|                 )?.id || tabs[0].id | ||||
|             } | ||||
|             onChange={onChange} | ||||
|         > | ||||
|             <Routes> | ||||
|                 <Route path={`${tabs[0].id}/*`} element={<ProjectAccess />} /> | ||||
|                 <Route | ||||
|                     path={`${tabs[1].id}/*`} | ||||
|                     element={<ProjectEnvironmentList />} | ||||
|                 /> | ||||
|                 <Route | ||||
|                     path={`${tabs[2].id}/*`} | ||||
|                     element={<ChangeRequestConfiguration />} | ||||
|                 /> | ||||
|                 <Route | ||||
|                     path="*" | ||||
|                     element={<Navigate replace to={tabs[0].id} />} | ||||
|                 /> | ||||
|             </Routes> | ||||
|         </VerticalTabs> | ||||
|     ); | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user