mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
feat: project UI rework, move edit and delete buttons deeper (#4195)
This commit is contained in:
parent
ec2978b133
commit
a2b06e4222
@ -1 +1 @@
|
||||
export const formTemplateSidebarWidth = '27.5rem';
|
||||
export const formTemplateSidebarWidth = '36%';
|
||||
|
@ -19,12 +19,13 @@ import { formTemplateSidebarWidth } from './FormTemplate.styles';
|
||||
import { relative } from 'themes/themeStyles';
|
||||
|
||||
interface ICreateProps {
|
||||
title: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
documentationLink: string;
|
||||
documentationLinkLabel: string;
|
||||
loading?: boolean;
|
||||
modal?: boolean;
|
||||
disablePadding?: boolean;
|
||||
formatApiCode: () => string;
|
||||
}
|
||||
|
||||
@ -45,20 +46,22 @@ const StyledContainer = styled('section', {
|
||||
|
||||
const StyledRelativeDiv = styled('div')(({ theme }) => relative);
|
||||
|
||||
const StyledFormContent = styled('div')(({ theme }) => ({
|
||||
const StyledFormContent = styled('div', {
|
||||
shouldForwardProp: prop => prop !== 'disablePadding',
|
||||
})<{ disablePadding?: boolean }>(({ theme, disablePadding }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(6),
|
||||
flexGrow: 1,
|
||||
padding: disablePadding ? 0 : theme.spacing(6),
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
padding: theme.spacing(4),
|
||||
padding: disablePadding ? 0 : theme.spacing(4),
|
||||
},
|
||||
[theme.breakpoints.down(1100)]: {
|
||||
width: '100%',
|
||||
},
|
||||
[theme.breakpoints.down(500)]: {
|
||||
padding: theme.spacing(4, 2),
|
||||
padding: disablePadding ? 0 : theme.spacing(4, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
@ -157,6 +160,7 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
||||
loading,
|
||||
modal,
|
||||
formatApiCode,
|
||||
disablePadding,
|
||||
}) => {
|
||||
const { setToastData } = useToast();
|
||||
const smallScreen = useMediaQuery(`(max-width:${1099}px)`);
|
||||
@ -194,13 +198,16 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
||||
</StyledRelativeDiv>
|
||||
}
|
||||
/>
|
||||
<StyledFormContent>
|
||||
<StyledFormContent disablePadding={disablePadding}>
|
||||
<ConditionallyRender
|
||||
condition={loading || false}
|
||||
show={<Loader />}
|
||||
elseShow={
|
||||
<>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
<ConditionallyRender
|
||||
condition={title !== undefined}
|
||||
show={<StyledTitle>{title}</StyledTitle>}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
|
@ -13,9 +13,14 @@ import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { Button, styled } from '@mui/material';
|
||||
|
||||
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const CreateProject = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchUser } = useAuthUser();
|
||||
@ -95,7 +100,6 @@ const CreateProject = () => {
|
||||
<ProjectForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
projectId={projectId}
|
||||
setProjectId={setProjectId}
|
||||
projectName={projectName}
|
||||
@ -115,6 +119,7 @@ const CreateProject = () => {
|
||||
permission={CREATE_PROJECT}
|
||||
data-testid={CREATE_PROJECT_BTN}
|
||||
/>
|
||||
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
||||
</ProjectForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
|
@ -14,13 +14,17 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { Alert } from '@mui/material';
|
||||
import { Alert, Button, styled } from '@mui/material';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
|
||||
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const EditProject = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -114,7 +118,6 @@ const EditProject = () => {
|
||||
<ProjectForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
projectId={projectId}
|
||||
setProjectId={setProjectId}
|
||||
projectName={projectName}
|
||||
@ -133,7 +136,8 @@ const EditProject = () => {
|
||||
permission={UPDATE_PROJECT}
|
||||
projectId={projectId}
|
||||
data-testid={EDIT_PROJECT_BTN}
|
||||
/>
|
||||
/>{' '}
|
||||
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
||||
</ProjectForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
|
@ -165,7 +165,10 @@ export const Project = () => {
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={!isOss()}
|
||||
condition={
|
||||
!isOss() &&
|
||||
!Boolean(uiConfig.flags.newProjectLayout)
|
||||
}
|
||||
show={
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
@ -184,7 +187,10 @@ export const Project = () => {
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={!isOss()}
|
||||
condition={
|
||||
!isOss() &&
|
||||
!Boolean(uiConfig.flags.newProjectLayout)
|
||||
}
|
||||
show={
|
||||
<PermissionIconButton
|
||||
permission={DELETE_PROJECT}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { TextField, Button, styled } from '@mui/material';
|
||||
|
||||
export const StyledForm = styled('form')(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
export const StyledContainer = styled('div')(() => ({
|
||||
maxWidth: '400px',
|
||||
}));
|
||||
|
||||
export const StyledDescription = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1),
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const StyledInput = styled(Input)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const StyledButtonContainer = styled('div')(() => ({
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}));
|
||||
|
||||
export const StyledButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
@ -1,20 +1,12 @@
|
||||
import React from 'react';
|
||||
import { trim } from 'component/common/util';
|
||||
import {
|
||||
StyledButton,
|
||||
StyledButtonContainer,
|
||||
StyledContainer,
|
||||
StyledDescription,
|
||||
StyledForm,
|
||||
StyledInput,
|
||||
StyledTextField,
|
||||
} from './ProjectForm.styles';
|
||||
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Select from 'component/common/select';
|
||||
import { ProjectMode } from '../hooks/useProjectForm';
|
||||
import { Box } from '@mui/material';
|
||||
import { Box, styled, TextField } from '@mui/material';
|
||||
import { CollaborationModeTooltip } from './CollaborationModeTooltip';
|
||||
import Input from 'component/common/Input/Input';
|
||||
|
||||
interface IProjectForm {
|
||||
projectId: string;
|
||||
@ -28,7 +20,6 @@ interface IProjectForm {
|
||||
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||
handleSubmit: (e: any) => void;
|
||||
handleCancel: () => void;
|
||||
errors: { [key: string]: string };
|
||||
mode: 'Create' | 'Edit';
|
||||
clearErrors: () => void;
|
||||
@ -40,10 +31,41 @@ const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT';
|
||||
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
|
||||
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
|
||||
|
||||
const StyledForm = styled('form')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
paddingBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledContainer = styled('div')(() => ({
|
||||
maxWidth: '400px',
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1),
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledTextField = styled(TextField)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(() => ({
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}));
|
||||
|
||||
const ProjectForm: React.FC<IProjectForm> = ({
|
||||
children,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
projectId,
|
||||
projectName,
|
||||
projectDesc,
|
||||
@ -153,10 +175,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
</>
|
||||
</StyledContainer>
|
||||
|
||||
<StyledButtonContainer>
|
||||
{children}
|
||||
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
||||
</StyledButtonContainer>
|
||||
<StyledButtonContainer>{children}</StyledButtonContainer>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
||||
|
@ -12,12 +12,23 @@ import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeR
|
||||
import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess';
|
||||
import { ProjectSegments } from './ProjectSegments/ProjectSegments';
|
||||
import { ProjectDefaultStrategySettings } from './ProjectDefaultStrategySettings/ProjectDefaultStrategySettings';
|
||||
import { Settings } from './Settings/Settings';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
export const ProjectSettings = () => {
|
||||
const location = useLocation();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tabs: ITab[] = [
|
||||
...(uiConfig.flags.newProjectLayout
|
||||
? [
|
||||
{
|
||||
id: '',
|
||||
label: 'Settings',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'environments',
|
||||
label: 'Environments',
|
||||
@ -59,6 +70,9 @@ export const ProjectSettings = () => {
|
||||
onChange={onChange}
|
||||
>
|
||||
<Routes>
|
||||
{uiConfig.flags.newProjectLayout ? (
|
||||
<Route path="/*" element={<Settings />} />
|
||||
) : null}
|
||||
<Route
|
||||
path="environments/*"
|
||||
element={<ProjectEnvironmentList />}
|
||||
|
@ -0,0 +1,79 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { DELETE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
import { DeleteProjectDialogue } from '../../DeleteProject/DeleteProjectDialogue';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const StyledTitle = styled('div')(({ theme }) => ({
|
||||
paddingTop: theme.spacing(4),
|
||||
lineHeight: 2,
|
||||
}));
|
||||
|
||||
const StyledCounter = styled('div')(({ theme }) => ({
|
||||
paddingTop: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
paddingTop: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IDeleteProjectProps {
|
||||
projectId: string;
|
||||
featureCount: number;
|
||||
}
|
||||
|
||||
export const DeleteProject = ({
|
||||
projectId,
|
||||
featureCount,
|
||||
}: IDeleteProjectProps) => {
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTitle>Delete project</StyledTitle>
|
||||
<div>
|
||||
Before you can delete a project, you must first archive all the
|
||||
feature toggles associated with it. Keep in mind that deleting a
|
||||
project will permanently remove all the archived feature
|
||||
toggles, and they cannot be recovered once deleted.
|
||||
</div>
|
||||
<StyledCounter>
|
||||
Currently there are{' '}
|
||||
<strong>{featureCount} feature toggles active</strong>
|
||||
</StyledCounter>
|
||||
<StyledButtonContainer>
|
||||
<PermissionButton
|
||||
permission={DELETE_PROJECT}
|
||||
disabled={featureCount > 0}
|
||||
projectId={projectId}
|
||||
onClick={() => {
|
||||
setShowDelDialog(true);
|
||||
}}
|
||||
tooltipProps={{
|
||||
title: 'Delete project',
|
||||
}}
|
||||
data-loading
|
||||
>
|
||||
Delete project
|
||||
</PermissionButton>
|
||||
</StyledButtonContainer>
|
||||
<DeleteProjectDialogue
|
||||
project={projectId}
|
||||
open={showDelDialog}
|
||||
onClose={() => {
|
||||
setShowDelDialog(false);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
navigate('/projects');
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -0,0 +1,143 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { Alert } from '@mui/material';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import useProjectForm, {
|
||||
DEFAULT_PROJECT_STICKINESS,
|
||||
} from '../../hooks/useProjectForm';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { DeleteProject } from './DeleteProject';
|
||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||
import ProjectForm from '../../ProjectForm/ProjectForm';
|
||||
|
||||
const EditProject = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const id = useRequiredPathParam('projectId');
|
||||
const { project } = useProject(id);
|
||||
const { defaultStickiness } = useDefaultProjectSettings(id);
|
||||
const navigate = useNavigate();
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
|
||||
const {
|
||||
projectId,
|
||||
projectName,
|
||||
projectDesc,
|
||||
projectStickiness,
|
||||
projectMode,
|
||||
setProjectId,
|
||||
setProjectName,
|
||||
setProjectDesc,
|
||||
setProjectStickiness,
|
||||
setProjectMode,
|
||||
getProjectPayload,
|
||||
clearErrors,
|
||||
validateProjectId,
|
||||
validateName,
|
||||
errors,
|
||||
} = useProjectForm(
|
||||
id,
|
||||
project.name,
|
||||
project.description,
|
||||
defaultStickiness,
|
||||
project.mode
|
||||
);
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/projects/${id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const { editProject, loading } = useProjectApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const payload = getProjectPayload();
|
||||
|
||||
const validName = validateName();
|
||||
|
||||
if (validName) {
|
||||
try {
|
||||
await editProject(id, payload);
|
||||
setToastData({
|
||||
title: 'Project information updated',
|
||||
type: 'success',
|
||||
});
|
||||
if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) {
|
||||
trackEvent('project_stickiness_set');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, projectId) && (
|
||||
<Alert severity="error" sx={{ mb: 4 }}>
|
||||
You do not have the required permissions to edit this project.
|
||||
</Alert>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
disablePadding={true}
|
||||
description="Projects allows you to group feature toggles together in the management UI."
|
||||
documentationLink="https://docs.getunleash.io/reference/projects"
|
||||
documentationLinkLabel="Projects documentation"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
{accessDeniedAlert}
|
||||
<PageContent header={<PageHeader title="Settings" />}>
|
||||
<ProjectForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
projectId={projectId}
|
||||
setProjectId={setProjectId}
|
||||
projectName={projectName}
|
||||
projectMode={projectMode}
|
||||
setProjectName={setProjectName}
|
||||
projectStickiness={projectStickiness}
|
||||
setProjectStickiness={setProjectStickiness}
|
||||
setProjectMode={setProjectMode}
|
||||
projectDesc={projectDesc}
|
||||
mode="Edit"
|
||||
setProjectDesc={setProjectDesc}
|
||||
clearErrors={clearErrors}
|
||||
validateProjectId={validateProjectId}
|
||||
>
|
||||
<PermissionButton
|
||||
type="submit"
|
||||
permission={UPDATE_PROJECT}
|
||||
projectId={projectId}
|
||||
>
|
||||
Save changes
|
||||
</PermissionButton>
|
||||
</ProjectForm>
|
||||
<DeleteProject
|
||||
projectId={projectId}
|
||||
featureCount={project.features.length}
|
||||
/>
|
||||
</PageContent>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProject;
|
@ -0,0 +1,29 @@
|
||||
import { useContext } from 'react';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { Alert } from '@mui/material';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
||||
import EditProject from './EditProject';
|
||||
|
||||
export const Settings = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const projectName = useProjectNameOrId(projectId);
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
usePageTitle(`Project configuration – ${projectName}`);
|
||||
|
||||
if (!hasAccess(UPDATE_PROJECT, projectId)) {
|
||||
return (
|
||||
<PageContent header={<PageHeader title="Access" />}>
|
||||
<Alert severity="error">
|
||||
You need project owner permissions to access this section.
|
||||
</Alert>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
return <EditProject />;
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
pageContent: {
|
||||
minHeight: '200px',
|
||||
},
|
||||
divider: {
|
||||
height: '1px',
|
||||
position: 'relative',
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: theme.palette.divider,
|
||||
margin: theme.spacing(4, -4, 3),
|
||||
},
|
||||
inputLabel: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
roleName: {
|
||||
fontWeight: 'bold',
|
||||
padding: '5px 0px',
|
||||
},
|
||||
menuItem: {
|
||||
width: '340px',
|
||||
whiteSpace: 'normal',
|
||||
},
|
||||
projectRoleSelect: {
|
||||
minWidth: '150px',
|
||||
},
|
||||
}));
|
@ -49,7 +49,7 @@ export const ProjectCard = ({
|
||||
isFavorite = false,
|
||||
}: IProjectCardProps) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const { isOss } = useUiConfig();
|
||||
const { isOss, uiConfig } = useUiConfig();
|
||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@ -117,24 +117,34 @@ export const ProjectCard = ({
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
navigate(getProjectEditPath(id));
|
||||
navigate(
|
||||
getProjectEditPath(
|
||||
id,
|
||||
Boolean(uiConfig.flags.newProjectLayout)
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<StyledEditIcon />
|
||||
Edit project
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setShowDelDialog(true);
|
||||
}}
|
||||
disabled={!canDeleteProject}
|
||||
>
|
||||
<StyledDeleteIcon />
|
||||
{id === DEFAULT_PROJECT_ID && !canDeleteProject
|
||||
? "You can't delete the default project"
|
||||
: 'Delete project'}
|
||||
</MenuItem>
|
||||
<ConditionallyRender
|
||||
condition={!Boolean(uiConfig.flags.newProjectLayout)}
|
||||
show={
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setShowDelDialog(true);
|
||||
}}
|
||||
disabled={!canDeleteProject}
|
||||
>
|
||||
<StyledDeleteIcon />
|
||||
{id === DEFAULT_PROJECT_ID && !canDeleteProject
|
||||
? "You can't delete the default project"
|
||||
: 'Delete project'}
|
||||
</MenuItem>
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
</StyledDivHeader>
|
||||
<div data-loading>
|
||||
|
@ -53,6 +53,7 @@ export interface IFlags {
|
||||
advancedPlayground?: boolean;
|
||||
customRootRoles?: boolean;
|
||||
strategySplittedButton?: boolean;
|
||||
newProjectLayout?: boolean;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import useUiConfig from '../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
export const getTogglePath = (projectId: string, featureToggleName: string) => {
|
||||
return `/projects/${projectId}/features/${featureToggleName}`;
|
||||
};
|
||||
@ -23,6 +25,11 @@ export const getCreateTogglePath = (
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getProjectEditPath = (projectId: string) => {
|
||||
return `/projects/${projectId}/edit`;
|
||||
export const getProjectEditPath = (
|
||||
projectId: string,
|
||||
newProjectPath: boolean
|
||||
) => {
|
||||
return newProjectPath
|
||||
? `/projects/${projectId}/settings`
|
||||
: `/projects/${projectId}/edit`;
|
||||
};
|
||||
|
@ -89,6 +89,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"migrationLock": false,
|
||||
"newProjectLayout": false,
|
||||
"personalAccessTokensKillSwitch": false,
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
@ -121,6 +122,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"migrationLock": false,
|
||||
"newProjectLayout": false,
|
||||
"personalAccessTokensKillSwitch": false,
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
|
@ -23,7 +23,8 @@ export type IFlagKey =
|
||||
| 'disableNotifications'
|
||||
| 'advancedPlayground'
|
||||
| 'customRootRoles'
|
||||
| 'strategySplittedButton';
|
||||
| 'strategySplittedButton'
|
||||
| 'newProjectLayout';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -108,6 +109,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES,
|
||||
false,
|
||||
),
|
||||
newProjectLayout: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_NEW_PROJECT_LAYOUT,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -40,6 +40,7 @@ process.nextTick(async () => {
|
||||
segmentContextFieldUsage: true,
|
||||
advancedPlayground: true,
|
||||
strategySplittedButton: true,
|
||||
newProjectLayout: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user