diff --git a/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx b/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx index 8a7a342158..591e099098 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectArchiveCard.tsx @@ -22,59 +22,35 @@ import TimeAgo from 'react-timeago'; import { Box, Link, Tooltip } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; import { - CREATE_PROJECT, DELETE_PROJECT, + UPDATE_PROJECT, } from 'component/providers/AccessProvider/permissions'; import Undo from '@mui/icons-material/Undo'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import Delete from '@mui/icons-material/Delete'; -interface IProjectArchiveCardProps { +export type ProjectArchiveCardProps = { id: string; name: string; - createdAt?: string; archivedAt?: string; - featureCount: number; + archivedFeaturesCount?: number; onRevive: () => void; onDelete: () => void; - mode: string; + mode?: string; owners?: ProjectSchemaOwners; -} +}; -export const ProjectArchiveCard: FC = ({ +export const ProjectArchiveCard: FC = ({ id, name, archivedAt, - featureCount = 0, + archivedFeaturesCount, onRevive, onDelete, mode, owners, }) => { const { locationSettings } = useLocationSettings(); - const Actions: FC<{ - id: string; - }> = ({ id }) => ( - - - - - - - - - ); return ( @@ -89,17 +65,6 @@ export const ProjectArchiveCard: FC = ({ - - - {featureCount} - -

- archived {featureCount === 1 ? 'flag' : 'flags'} -

- = ({ } /> + + + {archivedFeaturesCount} + +

+ archived{' '} + {archivedFeaturesCount === 1 + ? 'flag' + : 'flags'} +

+ + } + />
- - + + + + + + + + +
); diff --git a/frontend/src/component/project/NewProjectCard/ProjectCardFooter/ProjectCardFooter.tsx b/frontend/src/component/project/NewProjectCard/ProjectCardFooter/ProjectCardFooter.tsx index 002339b17a..b1947feb00 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectCardFooter/ProjectCardFooter.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectCardFooter/ProjectCardFooter.tsx @@ -10,7 +10,6 @@ interface IProjectCardFooterProps { id: string; isFavorite?: boolean; children?: React.ReactNode; - Actions?: FC<{ id: string; isFavorite?: boolean }>; disabled?: boolean; owners: IProjectOwnersProps['owners']; } diff --git a/frontend/src/component/project/NewProjectCard/ProjectModeBadge/ProjectModeBadge.tsx b/frontend/src/component/project/NewProjectCard/ProjectModeBadge/ProjectModeBadge.tsx index f3e46ea67d..243b345441 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectModeBadge/ProjectModeBadge.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectModeBadge/ProjectModeBadge.tsx @@ -5,7 +5,7 @@ import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; import { Badge } from 'component/common/Badge/Badge'; interface IProjectModeBadgeProps { - mode: 'private' | 'protected' | 'public' | string; + mode?: 'private' | 'protected' | 'public' | string; } export const ProjectModeBadge: FC = ({ mode }) => { diff --git a/frontend/src/component/project/Project/DeleteProject/DeleteProjectDialogue.tsx b/frontend/src/component/project/Project/DeleteProject/DeleteProjectDialogue.tsx index a163647c4a..0bf07a8201 100644 --- a/frontend/src/component/project/Project/DeleteProject/DeleteProjectDialogue.tsx +++ b/frontend/src/component/project/Project/DeleteProject/DeleteProjectDialogue.tsx @@ -23,7 +23,8 @@ export const DeleteProjectDialogue = ({ onSuccess, }: IDeleteProjectDialogueProps) => { const { deleteProject } = useProjectApi(); - const { refetch: refetchProjectOverview } = useProjects(); + const { refetch: refetchProjects } = useProjects(); + const { refetch: refetchProjectArchive } = useProjects({ archived: true }); const { setToastData, setToastApiError } = useToast(); const { isEnterprise } = useUiConfig(); const automatedActionsEnabled = useUiFlag('automatedActions'); @@ -32,7 +33,8 @@ export const DeleteProjectDialogue = ({ e.preventDefault(); try { await deleteProject(project); - refetchProjectOverview(); + refetchProjects(); + refetchProjectArchive(); setToastData({ title: 'Deleted project', type: 'success', diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx index e2a36fd676..66efc96b54 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/ArchiveProject.tsx @@ -34,7 +34,6 @@ export const ArchiveProject = ({ }: 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; diff --git a/frontend/src/component/project/ProjectList/ArchiveProjectList.tsx b/frontend/src/component/project/ProjectList/ArchiveProjectList.tsx index 1a4feeb9de..c8ff91f127 100644 --- a/frontend/src/component/project/ProjectList/ArchiveProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ArchiveProjectList.tsx @@ -1,6 +1,5 @@ import { type FC, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import useProjectsArchive from 'hooks/api/getters/useProjectsArchive/useProjectsArchive'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -9,7 +8,13 @@ import { styled, useMediaQuery } from '@mui/material'; import theme from 'themes/theme'; import { Search } from 'component/common/Search/Search'; import { ProjectGroup } from './ProjectGroup'; -import { ProjectArchiveCard } from '../NewProjectCard/ProjectArchiveCard'; +import { + ProjectArchiveCard, + type ProjectArchiveCardProps, +} from '../NewProjectCard/ProjectArchiveCard'; +import useProjects from 'hooks/api/getters/useProjects/useProjects'; +import { ReviveProjectDialog } from './ReviveProjectDialog/ReviveProjectDialog'; +import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue'; const StyledApiError = styled(ApiError)(({ theme }) => ({ maxWidth: '500px', @@ -25,13 +30,24 @@ const StyledContainer = styled('div')(({ theme }) => ({ type PageQueryType = Partial>; export const ArchiveProjectList: FC = () => { - const { projects, loading, error, refetch } = useProjectsArchive(); + const { projects, loading, error, refetch } = useProjects({ + archived: true, + }); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const [searchParams, setSearchParams] = useSearchParams(); const [searchValue, setSearchValue] = useState( searchParams.get('search') || '', ); + const [reviveProject, setReviveProject] = useState<{ + isOpen: boolean; + id?: string; + name?: string; + }>({ isOpen: false }); + const [deleteProject, setDeleteProject] = useState<{ + isOpen: boolean; + id?: string; + }>({ isOpen: false }); useEffect(() => { const tableState: PageQueryType = {}; @@ -44,6 +60,28 @@ export const ArchiveProjectList: FC = () => { }); }, [searchValue, setSearchParams]); + const ProjectCard: FC< + Omit + > = ({ id, ...props }) => ( + + setReviveProject({ + isOpen: true, + id, + name: projects?.find((project) => project.id === id)?.name, + }) + } + onDelete={() => + setDeleteProject({ + id, + isOpen: true, + }) + } + id={id} + {...props} + /> + ); + return ( { searchValue={searchValue} projects={projects} placeholder='No archived projects found' - ProjectCardComponent={ProjectArchiveCard} + ProjectCardComponent={ProjectCard} link={false} /> + + setReviveProject((state) => ({ ...state, isOpen: false })) + } + /> + { + setDeleteProject((state) => ({ ...state, isOpen: false })); + }} + /> ); }; diff --git a/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx b/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx new file mode 100644 index 0000000000..86f535bd51 --- /dev/null +++ b/frontend/src/component/project/ProjectList/ReviveProjectDialog/ReviveProjectDialog.tsx @@ -0,0 +1,56 @@ +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; +import useProjects from 'hooks/api/getters/useProjects/useProjects'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; + +type ReviveProjectDialogProps = { + name: string; + id: string; + open: boolean; + onClose: () => void; +}; + +export const ReviveProjectDialog = ({ + name, + id, + open, + onClose, +}: ReviveProjectDialogProps) => { + const { reviveProject } = useProjectApi(); + const { refetch: refetchProjects } = useProjects(); + const { refetch: refetchProjectArchive } = useProjects({ archived: true }); + const { setToastData, setToastApiError } = useToast(); + + const onClick = async (e: React.SyntheticEvent) => { + e.preventDefault(); + if (!id) return; + try { + await reviveProject(id); + refetchProjects(); + refetchProjectArchive(); + setToastData({ + title: 'Restored project', + type: 'success', + text: 'Successfully restored project', + }); + } catch (ex: unknown) { + setToastApiError(formatUnknownError(ex)); + } + onClose(); + }; + + return ( + + Are you sure you'd like to restore project {name}{' '} + (id: {id})? + {/* TODO: more explanation */} + + ); +}; diff --git a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts index 9f41b7e43c..d6a334d74c 100644 --- a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts +++ b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts @@ -81,7 +81,16 @@ const useProjectApi = () => { }; const archiveProject = async (projectId: string) => { - const path = `api/admin/projects/${projectId}/archive`; + const path = `api/admin/projects/archive/${projectId}`; + const req = createRequest(path, { method: 'POST' }); + + const res = await makeRequest(req.caller, req.id); + + return res; + }; + + const reviveProject = async (projectId: string) => { + const path = `api/admin/projects/revive/${projectId}`; const req = createRequest(path, { method: 'POST' }); const res = await makeRequest(req.caller, req.id); @@ -263,6 +272,7 @@ const useProjectApi = () => { editProjectSettings, deleteProject, archiveProject, + reviveProject, addEnvironmentToProject, removeEnvironmentFromProject, addAccessToProject, diff --git a/frontend/src/hooks/api/getters/useProjects/useProjects.ts b/frontend/src/hooks/api/getters/useProjects/useProjects.ts index 3553f93f70..907ec49c1d 100644 --- a/frontend/src/hooks/api/getters/useProjects/useProjects.ts +++ b/frontend/src/hooks/api/getters/useProjects/useProjects.ts @@ -4,10 +4,13 @@ import { formatApiPath } from 'utils/formatPath'; import type { IProjectCard } from 'interfaces/project'; import handleErrorResponses from '../httpErrorResponseHandler'; +import type { GetProjectsParams } from 'openapi'; + +const useProjects = (options: SWRConfiguration & GetProjectsParams = {}) => { + const KEY = `api/admin/projects${options.archived ? '?archived=true' : ''}`; -const useProjects = (options: SWRConfiguration = {}) => { const fetcher = () => { - const path = formatApiPath(`api/admin/projects`); + const path = formatApiPath(KEY); return fetch(path, { method: 'GET', }) @@ -15,8 +18,6 @@ const useProjects = (options: SWRConfiguration = {}) => { .then((res) => res.json()); }; - const KEY = `api/admin/projects`; - const { data, error } = useSWR<{ projects: IProjectCard[] }>( KEY, fetcher, diff --git a/frontend/src/hooks/api/getters/useProjectsArchive/useProjectsArchive.ts b/frontend/src/hooks/api/getters/useProjectsArchive/useProjectsArchive.ts deleted file mode 100644 index 57626243bd..0000000000 --- a/frontend/src/hooks/api/getters/useProjectsArchive/useProjectsArchive.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ProjectSchema } from 'openapi'; - -// FIXME: import tpye -interface IProjectArchiveCard { - name: string; - id: string; - createdAt: string; - archivedAt: string; - description: string; - featureCount: number; - owners?: ProjectSchema['owners']; -} - -// TODO: implement data fetching -const useProjectsArchive = () => { - return { - projects: [ - { - name: 'Archived something', - id: 'archi', - createdAt: new Date('2024-08-10 16:06').toISOString(), - archivedAt: new Date('2024-08-12 17:07').toISOString(), - owners: [{ ownerType: 'system' }], - }, - { - name: 'Second example', - id: 'pid', - createdAt: new Date('2024-08-10 16:06').toISOString(), - archivedAt: new Date('2024-08-12 17:07').toISOString(), - owners: [{ ownerType: 'system' }], - }, - ], - error: undefined as any, - loading: false, - refetch: () => {}, - }; -}; - -export default useProjectsArchive; diff --git a/frontend/src/openapi/models/getProjectsParams.ts b/frontend/src/openapi/models/getProjectsParams.ts new file mode 100644 index 0000000000..53621d2e69 --- /dev/null +++ b/frontend/src/openapi/models/getProjectsParams.ts @@ -0,0 +1,9 @@ +/** + * Generated by Orval + * Do not edit manually. + * See `gen:api` script in package.json + */ + +export type GetProjectsParams = { + archived?: boolean; +}; diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index e096a2b988..410d298b9b 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -722,6 +722,7 @@ export * from './getProjectUsers401'; export * from './getProjectUsers403'; export * from './getProjects401'; export * from './getProjects403'; +export * from './getProjectsParams'; export * from './getPublicSignupToken401'; export * from './getPublicSignupToken403'; export * from './getRawFeatureMetrics401'; diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index a6bab18827..d795fc7d40 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -77,6 +77,16 @@ export default class ProjectController extends Controller { summary: 'Get a list of all projects.', description: 'This endpoint returns an list of all the projects in the Unleash instance.', + parameters: [ + { + name: 'archived', + in: 'query', + required: false, + schema: { + type: 'boolean', + }, + }, + ], responses: { 200: createResponseSchema('projectsSchema'), ...getStandardResponses(401, 403),