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,6 +79,8 @@ const Project = () => {
 | 
			
		||||
                path: `${basePath}/health`,
 | 
			
		||||
                name: 'health',
 | 
			
		||||
            },
 | 
			
		||||
            ...(!uiConfig?.flags?.changeRequests
 | 
			
		||||
                ? [
 | 
			
		||||
                      {
 | 
			
		||||
                          title: 'Access',
 | 
			
		||||
                          path: `${basePath}/access`,
 | 
			
		||||
@ -88,11 +91,22 @@ const Project = () => {
 | 
			
		||||
                          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