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 { MainLayout } from 'component/layout/MainLayout/MainLayout';
 | 
				
			||||||
import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
 | 
					import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
				
			||||||
 | 
					import { ProjectSettings } from './ProjectSettings/ProjectSettings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledDiv = styled('div')(() => ({
 | 
					const StyledDiv = styled('div')(() => ({
 | 
				
			||||||
    display: 'flex',
 | 
					    display: 'flex',
 | 
				
			||||||
@ -58,7 +59,7 @@ const Project = () => {
 | 
				
			|||||||
    const { classes: styles } = useStyles();
 | 
					    const { classes: styles } = useStyles();
 | 
				
			||||||
    const navigate = useNavigate();
 | 
					    const navigate = useNavigate();
 | 
				
			||||||
    const { pathname } = useLocation();
 | 
					    const { pathname } = useLocation();
 | 
				
			||||||
    const { isOss } = useUiConfig();
 | 
					    const { isOss, uiConfig } = useUiConfig();
 | 
				
			||||||
    const basePath = `/projects/${projectId}`;
 | 
					    const basePath = `/projects/${projectId}`;
 | 
				
			||||||
    const projectName = project?.name || projectId;
 | 
					    const projectName = project?.name || projectId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -78,6 +79,8 @@ const Project = () => {
 | 
				
			|||||||
                path: `${basePath}/health`,
 | 
					                path: `${basePath}/health`,
 | 
				
			||||||
                name: 'health',
 | 
					                name: 'health',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            ...(!uiConfig?.flags?.changeRequests
 | 
				
			||||||
 | 
					                ? [
 | 
				
			||||||
                      {
 | 
					                      {
 | 
				
			||||||
                          title: 'Access',
 | 
					                          title: 'Access',
 | 
				
			||||||
                          path: `${basePath}/access`,
 | 
					                          path: `${basePath}/access`,
 | 
				
			||||||
@ -88,11 +91,22 @@ const Project = () => {
 | 
				
			|||||||
                          path: `${basePath}/environments`,
 | 
					                          path: `${basePath}/environments`,
 | 
				
			||||||
                          name: 'environments',
 | 
					                          name: 'environments',
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
 | 
					                  ]
 | 
				
			||||||
 | 
					                : []),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                title: 'Archive',
 | 
					                title: 'Archive',
 | 
				
			||||||
                path: `${basePath}/archive`,
 | 
					                path: `${basePath}/archive`,
 | 
				
			||||||
                name: 'archive',
 | 
					                name: 'archive',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            ...(uiConfig?.flags?.changeRequests
 | 
				
			||||||
 | 
					                ? [
 | 
				
			||||||
 | 
					                      {
 | 
				
			||||||
 | 
					                          title: 'Project settings',
 | 
				
			||||||
 | 
					                          path: `${basePath}/settings`,
 | 
				
			||||||
 | 
					                          name: 'settings',
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                  ]
 | 
				
			||||||
 | 
					                : []),
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                title: 'Event log',
 | 
					                title: 'Event log',
 | 
				
			||||||
                path: `${basePath}/logs`,
 | 
					                path: `${basePath}/logs`,
 | 
				
			||||||
@ -263,6 +277,7 @@ const Project = () => {
 | 
				
			|||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
 | 
					                <Route path="settings/*" element={<ProjectSettings />} />
 | 
				
			||||||
                <Route path="*" element={<ProjectOverview />} />
 | 
					                <Route path="*" element={<ProjectOverview />} />
 | 
				
			||||||
            </Routes>
 | 
					            </Routes>
 | 
				
			||||||
        </MainLayout>
 | 
					        </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