diff --git a/api/src/routes/projects/delete_projects.rs b/api/src/routes/projects/delete_projects.rs index a652c4da399c1cd208fe44b38bde600c05d09769..b90142f4ddd9d8fcf6517dca25ceab059817a92a 100644 --- a/api/src/routes/projects/delete_projects.rs +++ b/api/src/routes/projects/delete_projects.rs @@ -4,7 +4,10 @@ use http::StatusCode; use crate::{ errors::http_errors::ErrResponse, middlewares::authorization_extractors::UserData, - routes::{jobs::models::job_data::Job, utils::authorization::authorize_if_owned_or_admin}, + routes::{ + jobs::models::job_data::Job, pipelines::models::pipelines_data::Pipeline, + utils::authorization::authorize_if_owned_or_admin, + }, state::AppState, utils::config::GITLAB_PRIVATE_TOKEN_KEY, }; @@ -95,3 +98,75 @@ pub async fn delete_project( _ => Err((delete_response_status, "Error while deleting".to_owned())), } } + +pub async fn delete_empty_project( + State(app_state): State<AppState>, + Path(project_id): Path<u32>, + user_data: UserData, +) -> Result<StatusCode, (StatusCode, String)> { + let baseurl = &app_state.gitlab_config.baseurl; + let private_token = &app_state.gitlab_config.private_token; + + authorize_if_owned_or_admin(&app_state, &user_data, &project_id).await?; + + let client = reqwest::Client::new(); + + let url = format!("{}/projects/{}/pipelines", baseurl, project_id); + + let pipeline_response = client + .get(url) + .header(GITLAB_PRIVATE_TOKEN_KEY, private_token) + .send() + .await + .map_err(|e| { + ErrResponse::S500(( + "Failed to get pipelines list while deleting".to_owned(), + Some(Box::new(e)), + )) + })?; + + if !pipeline_response.status().is_success() { + let status = pipeline_response.status(); + return Err((status, "Failed to find the project to delete".to_owned())); + } + + let pipelines: Vec<Pipeline> = pipeline_response.json().await.map_err(|e| { + ErrResponse::S500(( + "Failed to serialize pipelines while deleting".to_owned(), + Some(Box::new(e)), + )) + })?; + + if !pipelines.is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + "Cannot destroy a project if the infrastructure deployment is not successful." + .to_owned(), + )); + } + + let delete_response = client + .delete(&format!("{}/projects/{}", baseurl, project_id)) + .header("PRIVATE-TOKEN", private_token) + .send() + .await + .map_err(|e| { + ErrResponse::S500(("Failed to delete projects".to_owned(), Some(Box::new(e)))) + })?; + + let delete_response_status = delete_response.status(); + + match delete_response_status { + StatusCode::ACCEPTED => Ok(delete_response_status), + StatusCode::BAD_REQUEST => { + let delete_response_body = delete_response.text().await.map_err(|e| { + ErrResponse::S500(( + "Failed to get body from response".to_owned(), + Some(Box::new(e)), + )) + })?; + Err((delete_response_status, delete_response_body)) + } + _ => Err((delete_response_status, "Error while deleting".to_owned())), + } +} diff --git a/api/src/routes/projects/mod.rs b/api/src/routes/projects/mod.rs index 05173635642119d798535055d1b538ba45c408d1..fe9d7457faae29f7cb060a0255ad90340ae1c76d 100644 --- a/api/src/routes/projects/mod.rs +++ b/api/src/routes/projects/mod.rs @@ -1,5 +1,5 @@ pub mod delete_projects; +pub mod get_project_access; pub mod get_projects; -pub mod post_projects; pub mod models; -pub mod get_project_access; \ No newline at end of file +pub mod post_projects; diff --git a/api/src/server.rs b/api/src/server.rs index ba4af01fa1847fa6f9c128622776e130dbd04358..df15f6eb5fb85d75e8cc371e6b9e9ee00b4a1fa7 100644 --- a/api/src/server.rs +++ b/api/src/server.rs @@ -13,7 +13,7 @@ use crate::routes::oauth::{login_authorized, logout, service_auth, user_status}; use crate::routes::pipelines::delete_pipelines::delete_pipeline; use crate::routes::pipelines::get_pipelines::{get_pipeline, get_pipelines}; use crate::routes::pipelines::post_pipelines::create_pipeline; -use crate::routes::projects::delete_projects::delete_project; +use crate::routes::projects::delete_projects::{delete_empty_project, delete_project}; use crate::routes::projects::get_project_access::get_project_access; use crate::routes::projects::get_projects::{get_project_details, get_projects, get_templates}; use crate::routes::projects::post_projects::{create_project, get_variables}; @@ -74,6 +74,7 @@ pub fn app(app_state: &AppState) -> Router { .route("/projects/atrium_health", post(atrium_health_check)) .route("/projects/:id/access", get(get_project_access)) .route("/projects/:id/pipeline/:id", delete(delete_project)) + .route("/projects/:id", delete(delete_empty_project)) .route("/projects/:id", get(get_project_details)) .route("/projects", get(get_projects)) .route("/projects", post(create_project)) diff --git a/front/src/api/projects/delete_empty_project.tsx b/front/src/api/projects/delete_empty_project.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d4280668736afac3c9a64f9d386c36131d18974 --- /dev/null +++ b/front/src/api/projects/delete_empty_project.tsx @@ -0,0 +1,22 @@ +import { BACKEND_BASEURL } from "../../utils/constants"; + +export async function deleteEmptyProject(project_id: number): Promise<number | undefined> { + try { + const response = await fetch(BACKEND_BASEURL + "/projects/" + project_id, + { + method: 'DELETE', + credentials: 'include', + headers: { "Content-Type": "application/json" } + }); + + if (response.ok) { + const result: number = response.status; + + return result; + } else { + console.error("Error deleting projects."); + } + } catch (error) { + console.error("Error deleting projects."); + } +} diff --git a/front/src/components/project-dashboard/ProjectRetry.tsx b/front/src/components/project-dashboard/ProjectRetry.tsx new file mode 100644 index 0000000000000000000000000000000000000000..409414c7c9759e28d357279599aff347eaafb7fe --- /dev/null +++ b/front/src/components/project-dashboard/ProjectRetry.tsx @@ -0,0 +1,105 @@ +import { Button, Card, Modal, Spin } from "antd" +import { Project } from "../../models/projects" +import PageSubTitle from "../PageSubTitle" +import { useNavigate } from "react-router-dom" +import { DeleteOutlined, LoadingOutlined } from "@ant-design/icons" +import { okButtonStyle } from "../create-project/form/SecondStepForm" +import { useState } from "react" +import { deleteEmptyProject } from "../../api/projects/delete_empty_project" +import { postPipelines } from "../../api/pipelines/post_pipelines" + +interface Props { + project: Project | undefined, +} + +export default function ProjectRetry(props: Readonly<Props>) { + const navigate = useNavigate(); + + const [openProject, setOpenProject] = useState(false); + const [loadingPostingProject, setLoadingPostingProject] = useState<boolean>(false); + const [errorPostingProject, setErrorPostingProject] = useState<boolean>(false); + + const showModalProject = () => { + setOpenProject(true); + }; + + const hideModalProject = (isOk: boolean) => { + if (isOk) + fetchDestroyProject() + + setOpenProject(false); + }; + + const fetchDestroyProject = async () => { + if (props.project?.id === undefined) return null; + const projectData = await deleteEmptyProject(props.project.id); + if (projectData !== undefined) + navigate("/projets") + } + + + const relaunchPipeline = async () => { + if (props.project?.id === undefined) return null; + setLoadingPostingProject(true); + const fetchPipeline = await postPipelines(props.project.id); + + if (fetchPipeline === undefined) { + setErrorPostingProject(true); + setLoadingPostingProject(false); + } + else { + setLoadingPostingProject(false); + navigate(`/projets/${props.project.id}`); + } + } + + + return ( + <div style={{ marginTop: "5px" }}> + { + <div style={{ maxWidth: "800px", display: "flex", flexWrap: "wrap" }}> + {errorPostingProject ? ( + <Card bordered={false} style={{ marginRight: "20px", backgroundColor: "red", color: "white" }}> + Une erreur s'est produite lors du déploiement + </Card> + ) : null} + <Card bordered={false} style={{ marginRight: "20px" }}> + <div style={{ marginBottom: "10px", display: "flex", justifyContent: "space-between" }}> + <PageSubTitle subtitle={"Actions Project"} /> + </div> + <div style={{ display: "flex", justifyContent: "space-between" }}> + <div style={{ marginRight: "20px" }}> + <Button + type="primary" + icon={loadingPostingProject ? <LoadingOutlined /> : <DeleteOutlined />} + onClick={() => relaunchPipeline()} + style={okButtonStyle} + > + Retenter le déploiement + </Button> + </div> + <div> + <Modal + title="Supprimer le projet ?" + open={openProject} + onOk={() => hideModalProject(true)} + onCancel={() => hideModalProject(false)} + okText="Oui" + cancelText="Non" + /> + <Button + type="primary" + icon={<DeleteOutlined />} + onClick={() => showModalProject()} + style={okButtonStyle} + > + Supprimer le projet + </Button> + </div> + </div> + </Card> + </div> + } + </div> + ) +} \ No newline at end of file diff --git a/front/src/pages/ProjectDetails.tsx b/front/src/pages/ProjectDetails.tsx index 072a0b4f686feb0ab9b87975151c389fd24e729b..2256c41dc4b73809a7fbe83e90c913de677a5e76 100644 --- a/front/src/pages/ProjectDetails.tsx +++ b/front/src/pages/ProjectDetails.tsx @@ -15,6 +15,7 @@ import { getJobs } from "../api/jobs/get_jobs"; import { Job } from "../models/jobs"; import ServiceAccess from "../components/project-dashboard/ServiceAccess"; import PageLoader from "../components/PageLoader"; +import ProjectRetry from "../components/project-dashboard/ProjectRetry"; export default function ProjectDetails() { const navigate = useNavigate(); @@ -82,6 +83,24 @@ export default function ProjectDetails() { if ((loadingJobs) || loadingPipelines || loadingProject || loadingProjectAccess) return <PageLoader /> + if (project !== undefined && (projectAccess == undefined || pipelines == undefined || jobs == undefined || jobs.length == 0)) + return ( + <div> + { + <Button + type="link" + icon={<ArrowLeftOutlined />} + onClick={() => navigateBack()} + style={{ color: '#000', padding: "0px 0px 8px 0px" }} + > + Retour + </Button > + } + <PageTitle title={"Project - " + project.name} /> + <ProjectRetry project={project} /> + </div> + ) + if (project === undefined || projectAccess === undefined || pipelines === undefined || pipelines.length === 0 || jobs === undefined || jobs.length === 0) return <NoDataFound data_type="détails du projet" />