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';
|
import { relative } from 'themes/themeStyles';
|
||||||
|
|
||||||
interface ICreateProps {
|
interface ICreateProps {
|
||||||
title: string;
|
title?: string;
|
||||||
description: string;
|
description: string;
|
||||||
documentationLink: string;
|
documentationLink: string;
|
||||||
documentationLinkLabel: string;
|
documentationLinkLabel: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
modal?: boolean;
|
modal?: boolean;
|
||||||
|
disablePadding?: boolean;
|
||||||
formatApiCode: () => string;
|
formatApiCode: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,20 +46,22 @@ const StyledContainer = styled('section', {
|
|||||||
|
|
||||||
const StyledRelativeDiv = styled('div')(({ theme }) => relative);
|
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,
|
backgroundColor: theme.palette.background.paper,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: theme.spacing(6),
|
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
padding: disablePadding ? 0 : theme.spacing(6),
|
||||||
[theme.breakpoints.down('lg')]: {
|
[theme.breakpoints.down('lg')]: {
|
||||||
padding: theme.spacing(4),
|
padding: disablePadding ? 0 : theme.spacing(4),
|
||||||
},
|
},
|
||||||
[theme.breakpoints.down(1100)]: {
|
[theme.breakpoints.down(1100)]: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
[theme.breakpoints.down(500)]: {
|
[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,
|
loading,
|
||||||
modal,
|
modal,
|
||||||
formatApiCode,
|
formatApiCode,
|
||||||
|
disablePadding,
|
||||||
}) => {
|
}) => {
|
||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
const smallScreen = useMediaQuery(`(max-width:${1099}px)`);
|
const smallScreen = useMediaQuery(`(max-width:${1099}px)`);
|
||||||
@ -194,13 +198,16 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
|||||||
</StyledRelativeDiv>
|
</StyledRelativeDiv>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StyledFormContent>
|
<StyledFormContent disablePadding={disablePadding}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={loading || false}
|
condition={loading || false}
|
||||||
show={<Loader />}
|
show={<Loader />}
|
||||||
elseShow={
|
elseShow={
|
||||||
<>
|
<>
|
||||||
<StyledTitle>{title}</StyledTitle>
|
<ConditionallyRender
|
||||||
|
condition={title !== undefined}
|
||||||
|
show={<StyledTitle>{title}</StyledTitle>}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,14 @@ import useToast from 'hooks/useToast';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { Button, styled } from '@mui/material';
|
||||||
|
|
||||||
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
const CreateProject = () => {
|
const CreateProject = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { refetchUser } = useAuthUser();
|
const { refetchUser } = useAuthUser();
|
||||||
@ -95,7 +100,6 @@ const CreateProject = () => {
|
|||||||
<ProjectForm
|
<ProjectForm
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
setProjectId={setProjectId}
|
setProjectId={setProjectId}
|
||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
@ -115,6 +119,7 @@ const CreateProject = () => {
|
|||||||
permission={CREATE_PROJECT}
|
permission={CREATE_PROJECT}
|
||||||
data-testid={CREATE_PROJECT_BTN}
|
data-testid={CREATE_PROJECT_BTN}
|
||||||
/>
|
/>
|
||||||
|
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
||||||
</ProjectForm>
|
</ProjectForm>
|
||||||
</FormTemplate>
|
</FormTemplate>
|
||||||
);
|
);
|
||||||
|
@ -14,13 +14,17 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { Alert } from '@mui/material';
|
import { Alert, Button, styled } from '@mui/material';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
const EditProject = () => {
|
const EditProject = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
@ -114,7 +118,6 @@ const EditProject = () => {
|
|||||||
<ProjectForm
|
<ProjectForm
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
setProjectId={setProjectId}
|
setProjectId={setProjectId}
|
||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
@ -133,7 +136,8 @@ const EditProject = () => {
|
|||||||
permission={UPDATE_PROJECT}
|
permission={UPDATE_PROJECT}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
data-testid={EDIT_PROJECT_BTN}
|
data-testid={EDIT_PROJECT_BTN}
|
||||||
/>
|
/>{' '}
|
||||||
|
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
||||||
</ProjectForm>
|
</ProjectForm>
|
||||||
</FormTemplate>
|
</FormTemplate>
|
||||||
);
|
);
|
||||||
|
@ -165,7 +165,10 @@ export const Project = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!isOss()}
|
condition={
|
||||||
|
!isOss() &&
|
||||||
|
!Boolean(uiConfig.flags.newProjectLayout)
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={UPDATE_PROJECT}
|
permission={UPDATE_PROJECT}
|
||||||
@ -184,7 +187,10 @@ export const Project = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!isOss()}
|
condition={
|
||||||
|
!isOss() &&
|
||||||
|
!Boolean(uiConfig.flags.newProjectLayout)
|
||||||
|
}
|
||||||
show={
|
show={
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={DELETE_PROJECT}
|
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 React from 'react';
|
||||||
import { trim } from 'component/common/util';
|
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 { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import Select from 'component/common/select';
|
import Select from 'component/common/select';
|
||||||
import { ProjectMode } from '../hooks/useProjectForm';
|
import { ProjectMode } from '../hooks/useProjectForm';
|
||||||
import { Box } from '@mui/material';
|
import { Box, styled, TextField } from '@mui/material';
|
||||||
import { CollaborationModeTooltip } from './CollaborationModeTooltip';
|
import { CollaborationModeTooltip } from './CollaborationModeTooltip';
|
||||||
|
import Input from 'component/common/Input/Input';
|
||||||
|
|
||||||
interface IProjectForm {
|
interface IProjectForm {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -28,7 +20,6 @@ interface IProjectForm {
|
|||||||
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
mode: 'Create' | 'Edit';
|
mode: 'Create' | 'Edit';
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
@ -40,10 +31,41 @@ const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT';
|
|||||||
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
|
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
|
||||||
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_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> = ({
|
const ProjectForm: React.FC<IProjectForm> = ({
|
||||||
children,
|
children,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleCancel,
|
|
||||||
projectId,
|
projectId,
|
||||||
projectName,
|
projectName,
|
||||||
projectDesc,
|
projectDesc,
|
||||||
@ -153,10 +175,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
|||||||
</>
|
</>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>{children}</StyledButtonContainer>
|
||||||
{children}
|
|
||||||
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
|
||||||
</StyledButtonContainer>
|
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,12 +12,23 @@ import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeR
|
|||||||
import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess';
|
import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess';
|
||||||
import { ProjectSegments } from './ProjectSegments/ProjectSegments';
|
import { ProjectSegments } from './ProjectSegments/ProjectSegments';
|
||||||
import { ProjectDefaultStrategySettings } from './ProjectDefaultStrategySettings/ProjectDefaultStrategySettings';
|
import { ProjectDefaultStrategySettings } from './ProjectDefaultStrategySettings/ProjectDefaultStrategySettings';
|
||||||
|
import { Settings } from './Settings/Settings';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
export const ProjectSettings = () => {
|
export const ProjectSettings = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const tabs: ITab[] = [
|
const tabs: ITab[] = [
|
||||||
|
...(uiConfig.flags.newProjectLayout
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
label: 'Settings',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: 'environments',
|
id: 'environments',
|
||||||
label: 'Environments',
|
label: 'Environments',
|
||||||
@ -59,6 +70,9 @@ export const ProjectSettings = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
{uiConfig.flags.newProjectLayout ? (
|
||||||
|
<Route path="/*" element={<Settings />} />
|
||||||
|
) : null}
|
||||||
<Route
|
<Route
|
||||||
path="environments/*"
|
path="environments/*"
|
||||||
element={<ProjectEnvironmentList />}
|
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,
|
isFavorite = false,
|
||||||
}: IProjectCardProps) => {
|
}: IProjectCardProps) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { isOss } = useUiConfig();
|
const { isOss, uiConfig } = useUiConfig();
|
||||||
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
|
||||||
const [showDelDialog, setShowDelDialog] = useState(false);
|
const [showDelDialog, setShowDelDialog] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -117,24 +117,34 @@ export const ProjectCard = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(getProjectEditPath(id));
|
navigate(
|
||||||
|
getProjectEditPath(
|
||||||
|
id,
|
||||||
|
Boolean(uiConfig.flags.newProjectLayout)
|
||||||
|
)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledEditIcon />
|
<StyledEditIcon />
|
||||||
Edit project
|
Edit project
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<ConditionallyRender
|
||||||
onClick={e => {
|
condition={!Boolean(uiConfig.flags.newProjectLayout)}
|
||||||
e.preventDefault();
|
show={
|
||||||
setShowDelDialog(true);
|
<MenuItem
|
||||||
}}
|
onClick={e => {
|
||||||
disabled={!canDeleteProject}
|
e.preventDefault();
|
||||||
>
|
setShowDelDialog(true);
|
||||||
<StyledDeleteIcon />
|
}}
|
||||||
{id === DEFAULT_PROJECT_ID && !canDeleteProject
|
disabled={!canDeleteProject}
|
||||||
? "You can't delete the default project"
|
>
|
||||||
: 'Delete project'}
|
<StyledDeleteIcon />
|
||||||
</MenuItem>
|
{id === DEFAULT_PROJECT_ID && !canDeleteProject
|
||||||
|
? "You can't delete the default project"
|
||||||
|
: 'Delete project'}
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</StyledDivHeader>
|
</StyledDivHeader>
|
||||||
<div data-loading>
|
<div data-loading>
|
||||||
|
@ -53,6 +53,7 @@ export interface IFlags {
|
|||||||
advancedPlayground?: boolean;
|
advancedPlayground?: boolean;
|
||||||
customRootRoles?: boolean;
|
customRootRoles?: boolean;
|
||||||
strategySplittedButton?: boolean;
|
strategySplittedButton?: boolean;
|
||||||
|
newProjectLayout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import useUiConfig from '../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
export const getTogglePath = (projectId: string, featureToggleName: string) => {
|
export const getTogglePath = (projectId: string, featureToggleName: string) => {
|
||||||
return `/projects/${projectId}/features/${featureToggleName}`;
|
return `/projects/${projectId}/features/${featureToggleName}`;
|
||||||
};
|
};
|
||||||
@ -23,6 +25,11 @@ export const getCreateTogglePath = (
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectEditPath = (projectId: string) => {
|
export const getProjectEditPath = (
|
||||||
return `/projects/${projectId}/edit`;
|
projectId: string,
|
||||||
|
newProjectPath: boolean
|
||||||
|
) => {
|
||||||
|
return newProjectPath
|
||||||
|
? `/projects/${projectId}/settings`
|
||||||
|
: `/projects/${projectId}/edit`;
|
||||||
};
|
};
|
||||||
|
@ -89,6 +89,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"migrationLock": false,
|
"migrationLock": false,
|
||||||
|
"newProjectLayout": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
@ -121,6 +122,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"migrationLock": false,
|
"migrationLock": false,
|
||||||
|
"newProjectLayout": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
@ -23,7 +23,8 @@ export type IFlagKey =
|
|||||||
| 'disableNotifications'
|
| 'disableNotifications'
|
||||||
| 'advancedPlayground'
|
| 'advancedPlayground'
|
||||||
| 'customRootRoles'
|
| 'customRootRoles'
|
||||||
| 'strategySplittedButton';
|
| 'strategySplittedButton'
|
||||||
|
| 'newProjectLayout';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -108,6 +109,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES,
|
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
newProjectLayout: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_NEW_PROJECT_LAYOUT,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -40,6 +40,7 @@ process.nextTick(async () => {
|
|||||||
segmentContextFieldUsage: true,
|
segmentContextFieldUsage: true,
|
||||||
advancedPlayground: true,
|
advancedPlayground: true,
|
||||||
strategySplittedButton: true,
|
strategySplittedButton: true,
|
||||||
|
newProjectLayout: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user