diff --git a/frontend/src/component/project/Project/ArchiveProject/ArchiveProjectDialogue.tsx b/frontend/src/component/project/Project/ArchiveProject/ArchiveProjectDialogue.tsx new file mode 100644 index 0000000000..10cf5e88eb --- /dev/null +++ b/frontend/src/component/project/Project/ArchiveProject/ArchiveProjectDialogue.tsx @@ -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 ( + + + This will archive the project and all feature flags archived in + it. + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx new file mode 100644 index 0000000000..e2a36fd676 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx @@ -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 ( + +

+ 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' + : ''} + . +

+ 0} + show={ +

+ Currently there {featureCount <= 1 ? 'is' : 'are'}{' '} + + {featureCount} active feature{' '} + {featureCount === 1 ? 'flag' : 'flags'}. + +

+ } + /> + 0 + } + show={ +

+ Currently there {actionsCount <= 1 ? 'is' : 'are'}{' '} + + {actionsCount} enabled{' '} + {actionsCount === 1 ? 'action' : 'actions'}. + +

+ } + /> + + 0} + projectId={projectId} + onClick={() => { + setShowArchiveDialog(true); + }} + tooltipProps={{ + title: 'Archive project', + }} + data-loading + > + Archive project + + + { + setShowArchiveDialog(false); + }} + onSuccess={() => { + navigate('/projects'); + }} + /> +
+ ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx new file mode 100644 index 0000000000..6cf9116547 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/ArchiveProjectForm.tsx @@ -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 ( + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/EditProject.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/EditProject.tsx index 3f7d90fdfe..01a277b16d 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/EditProject.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/EditProject.tsx @@ -14,6 +14,8 @@ import { DeleteProjectForm } from './DeleteProjectForm'; import useProjectOverview, { featuresCount, } from 'hooks/api/getters/useProjectOverview/useProjectOverview'; +import { ArchiveProjectForm } from './ArchiveProjectForm'; +import { useUiFlag } from 'hooks/useUiFlag'; const StyledFormContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -26,6 +28,7 @@ const EditProject = () => { const { hasAccess } = useContext(AccessContext); const id = useRequiredPathParam('projectId'); const { project } = useProjectOverview(id); + const archiveProjectsEnabled = useUiFlag('archiveProjects'); if (!project.name) { return null; @@ -49,7 +52,19 @@ const EditProject = () => { condition={isEnterprise()} show={} /> - + + } + elseShow={ + + } + /> ); diff --git a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts index 4b71f3a4fc..9f41b7e43c 100644 --- a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts +++ b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts @@ -80,6 +80,15 @@ const useProjectApi = () => { 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 ( projectId: string, environment: string, @@ -253,6 +262,7 @@ const useProjectApi = () => { editProject, editProjectSettings, deleteProject, + archiveProject, addEnvironmentToProject, removeEnvironmentFromProject, addAccessToProject,