mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
feat: archive project form (#7797)
This commit is contained in:
parent
8caa1e242c
commit
a01305040d
@ -0,0 +1,56 @@
|
|||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import type React from 'react';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface IDeleteProjectDialogueProps {
|
||||||
|
project: string;
|
||||||
|
open: boolean;
|
||||||
|
onClose: (e: React.SyntheticEvent) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveProjectDialogue = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
project,
|
||||||
|
onSuccess,
|
||||||
|
}: IDeleteProjectDialogueProps) => {
|
||||||
|
const { archiveProject } = useProjectApi();
|
||||||
|
const { refetch: refetchProjectOverview } = useProjects();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
|
const onClick = async (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await archiveProject(project);
|
||||||
|
refetchProjectOverview();
|
||||||
|
setToastData({
|
||||||
|
title: 'Archived project',
|
||||||
|
type: 'success',
|
||||||
|
text: 'Successfully archived project',
|
||||||
|
});
|
||||||
|
onSuccess?.();
|
||||||
|
} catch (ex: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(ex));
|
||||||
|
}
|
||||||
|
onClose(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
open={open}
|
||||||
|
onClick={onClick}
|
||||||
|
onClose={onClose}
|
||||||
|
title='Really archive project'
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
This will archive the project and all feature flags archived in
|
||||||
|
it.
|
||||||
|
</Typography>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,108 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { DELETE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { useActions } from 'hooks/api/getters/useActions/useActions';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ArchiveProjectDialogue } from '../../ArchiveProject/ArchiveProjectDialogue';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
paddingTop: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IDeleteProjectProps {
|
||||||
|
projectId: string;
|
||||||
|
featureCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveProject = ({
|
||||||
|
projectId,
|
||||||
|
featureCount,
|
||||||
|
}: IDeleteProjectProps) => {
|
||||||
|
const { isEnterprise } = useUiConfig();
|
||||||
|
const automatedActionsEnabled = useUiFlag('automatedActions');
|
||||||
|
const archiveProjectsEnabled = useUiFlag('archiveProjects');
|
||||||
|
const { actions } = useActions(projectId);
|
||||||
|
const [showArchiveDialog, setShowArchiveDialog] = useState(false);
|
||||||
|
const actionsCount = actions.filter(({ enabled }) => enabled).length;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<p>
|
||||||
|
Before you can archive a project, you must first archive all the
|
||||||
|
feature flags associated with it
|
||||||
|
{isEnterprise() && automatedActionsEnabled
|
||||||
|
? ' and disable all actions that are in it'
|
||||||
|
: ''}
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featureCount > 0}
|
||||||
|
show={
|
||||||
|
<p>
|
||||||
|
Currently there {featureCount <= 1 ? 'is' : 'are'}{' '}
|
||||||
|
<strong>
|
||||||
|
{featureCount} active feature{' '}
|
||||||
|
{featureCount === 1 ? 'flag' : 'flags'}.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={
|
||||||
|
isEnterprise() &&
|
||||||
|
automatedActionsEnabled &&
|
||||||
|
actionsCount > 0
|
||||||
|
}
|
||||||
|
show={
|
||||||
|
<p>
|
||||||
|
Currently there {actionsCount <= 1 ? 'is' : 'are'}{' '}
|
||||||
|
<strong>
|
||||||
|
{actionsCount} enabled{' '}
|
||||||
|
{actionsCount === 1 ? 'action' : 'actions'}.
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<PermissionButton
|
||||||
|
permission={DELETE_PROJECT}
|
||||||
|
disabled={featureCount > 0}
|
||||||
|
projectId={projectId}
|
||||||
|
onClick={() => {
|
||||||
|
setShowArchiveDialog(true);
|
||||||
|
}}
|
||||||
|
tooltipProps={{
|
||||||
|
title: 'Archive project',
|
||||||
|
}}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Archive project
|
||||||
|
</PermissionButton>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
<ArchiveProjectDialogue
|
||||||
|
project={projectId}
|
||||||
|
open={showArchiveDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowArchiveDialog(false);
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
navigate('/projects');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { ArchiveProject } from '../ArchiveProject';
|
||||||
|
|
||||||
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
|
borderRadius: theme.spacing(2),
|
||||||
|
overflow: 'hidden',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IDeleteProjectForm {
|
||||||
|
featureCount: number;
|
||||||
|
}
|
||||||
|
export const ArchiveProjectForm = ({ featureCount }: IDeleteProjectForm) => {
|
||||||
|
const id = useRequiredPathParam('projectId');
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { loading } = useProjectApi();
|
||||||
|
const formatProjectArchiveApiCode = () => {
|
||||||
|
return `curl --location --request DELETE '${uiConfig.unleashUrl}/api/admin/projects/${id}/archive' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' '`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title='Archive project'
|
||||||
|
description=''
|
||||||
|
documentationLink='https://docs.getunleash.io/reference/projects'
|
||||||
|
documentationLinkLabel='Projects documentation'
|
||||||
|
formatApiCode={formatProjectArchiveApiCode}
|
||||||
|
compact
|
||||||
|
compactPadding
|
||||||
|
showDescription={false}
|
||||||
|
showLink={false}
|
||||||
|
>
|
||||||
|
<ArchiveProject projectId={id} featureCount={featureCount} />
|
||||||
|
</FormTemplate>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -14,6 +14,8 @@ import { DeleteProjectForm } from './DeleteProjectForm';
|
|||||||
import useProjectOverview, {
|
import useProjectOverview, {
|
||||||
featuresCount,
|
featuresCount,
|
||||||
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import { ArchiveProjectForm } from './ArchiveProjectForm';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledFormContainer = styled('div')(({ theme }) => ({
|
const StyledFormContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -26,6 +28,7 @@ const EditProject = () => {
|
|||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const id = useRequiredPathParam('projectId');
|
const id = useRequiredPathParam('projectId');
|
||||||
const { project } = useProjectOverview(id);
|
const { project } = useProjectOverview(id);
|
||||||
|
const archiveProjectsEnabled = useUiFlag('archiveProjects');
|
||||||
|
|
||||||
if (!project.name) {
|
if (!project.name) {
|
||||||
return null;
|
return null;
|
||||||
@ -49,7 +52,19 @@ const EditProject = () => {
|
|||||||
condition={isEnterprise()}
|
condition={isEnterprise()}
|
||||||
show={<UpdateEnterpriseSettings project={project} />}
|
show={<UpdateEnterpriseSettings project={project} />}
|
||||||
/>
|
/>
|
||||||
<DeleteProjectForm featureCount={featuresCount(project)} />
|
<ConditionallyRender
|
||||||
|
condition={archiveProjectsEnabled}
|
||||||
|
show={
|
||||||
|
<ArchiveProjectForm
|
||||||
|
featureCount={featuresCount(project)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<DeleteProjectForm
|
||||||
|
featureCount={featuresCount(project)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledFormContainer>
|
</StyledFormContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -80,6 +80,15 @@ const useProjectApi = () => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const archiveProject = async (projectId: string) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/archive`;
|
||||||
|
const req = createRequest(path, { method: 'POST' });
|
||||||
|
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
const addEnvironmentToProject = async (
|
const addEnvironmentToProject = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
@ -253,6 +262,7 @@ const useProjectApi = () => {
|
|||||||
editProject,
|
editProject,
|
||||||
editProjectSettings,
|
editProjectSettings,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
|
archiveProject,
|
||||||
addEnvironmentToProject,
|
addEnvironmentToProject,
|
||||||
removeEnvironmentFromProject,
|
removeEnvironmentFromProject,
|
||||||
addAccessToProject,
|
addAccessToProject,
|
||||||
|
Loading…
Reference in New Issue
Block a user