From 9052792a27974acbd2c06f673b7a339298a94436 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Mon, 9 Jun 2025 23:41:13 +0200 Subject: [PATCH 1/4] turn off add project and add dataset in renku legacy --- client/src/App.jsx | 6 ++- client/src/components/navbar/NavBarItems.tsx | 28 +++++----- client/src/dataset/Dataset.present.jsx | 24 +++------ .../dataset/ProjectDatasetsListView.tsx | 42 ++++----------- .../project/dataset/ProjectDatasetsView.tsx | 53 ++----------------- .../projectsV2/shared/SunsetV1Banner.tsx | 2 +- client/src/features/rootV1/ProjectRootV1.tsx | 4 +- 7 files changed, 40 insertions(+), 119 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index f0f2e8b7e7..55a88f63cd 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -31,12 +31,12 @@ import { ToastContainer } from "react-toastify"; import { LoginHelper } from "./authentication"; import { Loader } from "./components/Loader"; -import LazyDatasetAddToProject from "./dataset/addtoproject/LazyDatasetAddToProject"; import { DatasetCoordinator } from "./dataset/Dataset.state"; import LazyShowDataset from "./dataset/LazyShowDataset"; import LazyAdminPage from "./features/admin/LazyAdminPage"; import { Favicon } from "./features/favicon/Favicon"; import { Unavailable } from "./features/maintenance/Maintenance"; +import SunsetBanner from "./features/projectsV2/shared/SunsetV1Banner"; import LazyRootV1 from "./features/rootV1/LazyRootV1"; import LazyRootV2 from "./features/rootV2/LazyRootV2"; import { useGetUserQuery } from "./features/usersV2/api/users.api"; @@ -94,7 +94,9 @@ function CentralContentContainer({ user }) { +
+ +
} /> - - Dataset - + +
Dataset
+ + Adding new datasets is no longer supported in the legacy Renku interface +
) : null; const projectDropdown = ( - - - Project - + +
Project
+ + Adding new projects is no longer supported in the legacy Renku interface +
); diff --git a/client/src/dataset/Dataset.present.jsx b/client/src/dataset/Dataset.present.jsx index ce5bfcda28..8084dd1f41 100644 --- a/client/src/dataset/Dataset.present.jsx +++ b/client/src/dataset/Dataset.present.jsx @@ -297,7 +297,7 @@ function ErrorAfterCreation(props) { ) : null; } -function AddToProjectButton({ insideKg, locked, logged, identifier }) { +function AddToProjectButton({ identifier }) { const navigate = useNavigate(); const addDatasetUrl = `/datasets/${identifier}/add`; @@ -305,27 +305,17 @@ function AddToProjectButton({ insideKg, locked, logged, identifier }) { navigate(addDatasetUrl); }; - const tooltip = - logged && locked ? ( - - Cannot add dataset to project until project modification finishes - - ) : insideKg === false ? ( - - Cannot add dataset to project, the project containing this dataset is - not indexed - - ) : ( - - Import Dataset in new or existing project - - ); + const tooltip = ( + + Adding new datasets is no longer supported in the legacy Renku interface + + ); return ( - - Cannot add dataset until project modification finishes. - - - ); - } return ( -
- + + + Adding new datasets is no longer supported in the legacy Renku interface +
); } diff --git a/client/src/features/project/dataset/ProjectDatasetsView.tsx b/client/src/features/project/dataset/ProjectDatasetsView.tsx index 069ec08efe..1719156168 100644 --- a/client/src/features/project/dataset/ProjectDatasetsView.tsx +++ b/client/src/features/project/dataset/ProjectDatasetsView.tsx @@ -39,11 +39,11 @@ import { DatasetCoordinator } from "../../../dataset/Dataset.state"; import { SpecialPropVal } from "../../../model/Model"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; import { Url } from "../../../utils/helpers/url"; +import SunsetBanner from "../../projectsV2/shared/SunsetV1Banner.tsx"; import { StateModelProject } from "../project.types"; import { useGetProjectIndexingStatusQuery } from "../projectKg.api"; import { useCoreSupport } from "../useProjectCoreSupport"; -import ProjectDatasetImport from "./ProjectDatasetImport"; -import { ProjectDatasetEdit, ProjectDatasetNew } from "./ProjectDatasetNewEdit"; +import { ProjectDatasetEdit } from "./ProjectDatasetNewEdit"; import ProjectDatasetShow from "./ProjectDatasetShow"; import ProjectDatasetListView from "./ProjectDatasetsListView"; @@ -109,41 +109,6 @@ function ProjectDatasetsNav(props: any) { ); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function ProjectAddDataset(props: any) { - const [newDataset, setNewDataset] = React.useState(true); - function toggleNewDataset() { - setNewDataset(!newDataset); - } - - return ( - - {newDataset ? ( - - ) : ( - - )} - - ); -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any function EmptyDatasets({ locked, membership, newDatasetUrl }: any) { return ( @@ -368,19 +333,7 @@ function ProjectDatasetsView(props: any) { path="new" element={ <> - - - - + } /> diff --git a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx index 0f5c8f7c11..6b21e5cf63 100644 --- a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx +++ b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx @@ -8,7 +8,7 @@ import SunsetV1Button from "./SunsetV1Button"; export default function SunsetBanner() { return ( -

New project creation ends July 15

+

Feature Unavailable

You won’t be able to create new projects or datasets in Renku Legacy after July 15, 2025. This is in preparation for Renku Legacy being fully diff --git a/client/src/features/rootV1/ProjectRootV1.tsx b/client/src/features/rootV1/ProjectRootV1.tsx index 3dc9e2a247..ac56138f50 100644 --- a/client/src/features/rootV1/ProjectRootV1.tsx +++ b/client/src/features/rootV1/ProjectRootV1.tsx @@ -20,8 +20,8 @@ import { Route, Routes } from "react-router"; import ContainerWrap from "../../components/container/ContainerWrap"; import LazyNotFound from "../../not-found/LazyNotFound"; import LazyProjectList from "../../project/list/LazyProjectList"; -import LazyNewProject from "../../project/new/LazyNewProject"; import { RELATIVE_ROUTES } from "../../routing/routes.constants"; +import SunsetBanner from "../projectsV2/shared/SunsetV1Banner.tsx"; export default function RootV1() { return ( @@ -54,7 +54,7 @@ export default function RootV1() { path={RELATIVE_ROUTES.v1.projects.new} element={ - + } /> From 8fb088e79529033ac882c155a7dc00869c09bf1b Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Mon, 16 Jun 2025 10:48:28 +0200 Subject: [PATCH 2/4] apply new copy, disable create project button --- client/src/components/navbar/NavBarItems.tsx | 6 +++-- .../components/ProjectsDashboard.tsx | 27 ++++++++++++------- .../dataset/ProjectDatasetsListView.tsx | 3 ++- .../projectsV2/shared/SunsetV1Banner.tsx | 11 +++----- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/client/src/components/navbar/NavBarItems.tsx b/client/src/components/navbar/NavBarItems.tsx index 34330ecd4b..7d7253c152 100644 --- a/client/src/components/navbar/NavBarItems.tsx +++ b/client/src/components/navbar/NavBarItems.tsx @@ -63,7 +63,8 @@ export function RenkuToolbarItemPlus() {

Dataset
- Adding new datasets is no longer supported in the legacy Renku interface + Creating new datasets is no longer supported in Renku Legacy. Switch to + Renku 2.0 to continue creating and managing your work.
) : null; @@ -71,7 +72,8 @@ export function RenkuToolbarItemPlus() {
Project
- Adding new projects is no longer supported in the legacy Renku interface + Creating new projects is no longer supported in Renku Legacy. Switch to + Renku 2.0 to continue creating and managing your work.
); diff --git a/client/src/features/dashboard/components/ProjectsDashboard.tsx b/client/src/features/dashboard/components/ProjectsDashboard.tsx index e5abc31287..47f98c7ab0 100644 --- a/client/src/features/dashboard/components/ProjectsDashboard.tsx +++ b/client/src/features/dashboard/components/ProjectsDashboard.tsx @@ -23,6 +23,7 @@ import cx from "classnames"; import { Fragment, useContext, useEffect, useMemo, useState } from "react"; import { Search } from "react-bootstrap-icons"; import { Link } from "react-router"; +import { UncontrolledTooltip } from "reactstrap"; import { InfoAlert, WarnAlert } from "../../../components/Alert"; import { ExternalLink } from "../../../components/ExternalLinks"; @@ -330,16 +331,22 @@ function ProjectsDashboard() {

Projects

- - - - Create a new project - - +
+ + + + Create a new project + + + + Creating new projects is no longer supported in Renku Legacy. + Switch to Renku 2.0 to continue creating and managing your work. + +
{content} diff --git a/client/src/features/project/dataset/ProjectDatasetsListView.tsx b/client/src/features/project/dataset/ProjectDatasetsListView.tsx index 1fb74e08e1..5434d7cfa7 100644 --- a/client/src/features/project/dataset/ProjectDatasetsListView.tsx +++ b/client/src/features/project/dataset/ProjectDatasetsListView.tsx @@ -87,7 +87,8 @@ function AddDatasetButton({ accessLevel }: AddDatasetButtonProps) { Add Dataset - Adding new datasets is no longer supported in the legacy Renku interface + Creating new datasets is no longer supported in Renku Legacy. Switch to + Renku 2.0 to continue creating and managing your work. ); diff --git a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx index 6b21e5cf63..9c01cd31e9 100644 --- a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx +++ b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx @@ -8,13 +8,10 @@ import SunsetV1Button from "./SunsetV1Button"; export default function SunsetBanner() { return ( -

Feature Unavailable

+

Project creation no longer available

- You won’t be able to create new projects or datasets in Renku Legacy - after July 15, 2025. This is in preparation for Renku Legacy being fully - discontinued in October 2025. Our transition guide will help walk you - through migrating to Renku 2.0 for enhanced features and continued - access to your work. + You can no longer create new projects or datasets in Renku Legacy. + Switch to Renku 2.0 to continue creating and managing your work.

- View transition guide + Learn more
From 69680ae4cefc87e5492791edfe536a7e30fb6d78 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Thu, 19 Jun 2025 13:56:22 +0200 Subject: [PATCH 3/4] Remove unused code, update tests --- client/src/App.jsx | 10 +- .../addDatasetButtons/AddDatasetButtons.tsx | 57 -- .../form-field/FormGeneratorImageInput.jsx | 2 +- .../components/quicknav/QuickNav.present.jsx | 2 +- .../addtoproject/DatasetAdd.present.tsx | 183 ---- .../dataset/addtoproject/DatasetAdd.types.ts | 35 - .../DatasetAddToExistingProject.tsx | 197 ---- .../addtoproject/DatasetAddToNewProject.tsx | 145 --- .../addtoproject/DatasetAddToProject.tsx | 395 -------- .../DatasetAddToProjectStatus.tsx | 113 --- .../addtoproject/LazyDatasetAddToProject.tsx | 32 - client/src/features/landing/AnonymousHome.tsx | 6 +- .../components/ProjectSettingAvatar.tsx | 2 +- .../components/ProjectSettingsGeneral.tsx | 2 +- .../project/dataset/DatasetModify.tsx | 2 +- .../project/dataset/ProjectDatasetImport.tsx | 40 - .../project/dataset/ProjectDatasetNewEdit.tsx | 78 +- .../projectsV2/shared/SunsetV1Banner.tsx | 2 +- .../session/components/SessionSaveWarning.tsx | 41 - .../session/components/StartNewSession.tsx | 40 +- client/src/project/Project.present.jsx | 84 +- .../src/project/{new => }/Project.style.css | 0 .../{new => }/components/FormValidations.tsx | 46 +- .../{new => }/components/NewProjectAvatar.tsx | 4 +- .../{new => }/components/ShareLinkModal.jsx | 2 +- .../{new => }/components/Visibility.tsx | 24 +- .../{new => }/components/newProject.types.ts | 2 +- .../project/datasets/import/DatasetImport.tsx | 461 --------- client/src/project/datasets/import/index.ts | 28 - client/src/project/new/LazyNewProject.tsx | 34 - .../src/project/new/ProjectNew.container.jsx | 693 -------------- client/src/project/new/ProjectNew.present.jsx | 520 ----------- client/src/project/new/ProjectNew.state.js | 876 ------------------ client/src/project/new/ProjectNew.test.jsx | 121 --- .../src/project/new/components/Automated.tsx | 239 ----- .../project/new/components/Description.tsx | 55 -- .../src/project/new/components/Namespaces.jsx | 290 ------ .../new/components/ProjectIdentifier.tsx | 47 - .../new/components/SubmitFormButton.tsx | 130 --- .../src/project/new/components/Template.tsx | 86 -- .../project/new/components/TemplateSource.tsx | 61 -- .../new/components/TemplateVariables.jsx | 217 ----- client/src/project/new/components/Title.tsx | 65 -- .../project/new/components/UserTemplate.jsx | 200 ---- client/src/project/new/index.js | 29 - tests/cypress/e2e/addDatasetToProject.spec.ts | 201 +--- tests/cypress/e2e/newDataset.spec.ts | 260 +----- tests/cypress/e2e/newProject.spec.ts | 320 +------ tests/cypress/e2e/projectDatasets.spec.ts | 18 + 49 files changed, 100 insertions(+), 6397 deletions(-) delete mode 100644 client/src/components/addDatasetButtons/AddDatasetButtons.tsx delete mode 100644 client/src/dataset/addtoproject/DatasetAdd.present.tsx delete mode 100644 client/src/dataset/addtoproject/DatasetAdd.types.ts delete mode 100644 client/src/dataset/addtoproject/DatasetAddToExistingProject.tsx delete mode 100644 client/src/dataset/addtoproject/DatasetAddToNewProject.tsx delete mode 100644 client/src/dataset/addtoproject/DatasetAddToProject.tsx delete mode 100644 client/src/dataset/addtoproject/DatasetAddToProjectStatus.tsx delete mode 100644 client/src/dataset/addtoproject/LazyDatasetAddToProject.tsx delete mode 100644 client/src/features/project/dataset/ProjectDatasetImport.tsx rename client/src/project/{new => }/Project.style.css (100%) rename client/src/project/{new => }/components/FormValidations.tsx (52%) rename client/src/project/{new => }/components/NewProjectAvatar.tsx (93%) rename client/src/project/{new => }/components/ShareLinkModal.jsx (98%) rename client/src/project/{new => }/components/Visibility.tsx (91%) rename client/src/project/{new => }/components/newProject.types.ts (95%) delete mode 100644 client/src/project/datasets/import/DatasetImport.tsx delete mode 100644 client/src/project/datasets/import/index.ts delete mode 100644 client/src/project/new/LazyNewProject.tsx delete mode 100644 client/src/project/new/ProjectNew.container.jsx delete mode 100644 client/src/project/new/ProjectNew.present.jsx delete mode 100644 client/src/project/new/ProjectNew.state.js delete mode 100644 client/src/project/new/ProjectNew.test.jsx delete mode 100644 client/src/project/new/components/Automated.tsx delete mode 100644 client/src/project/new/components/Description.tsx delete mode 100644 client/src/project/new/components/Namespaces.jsx delete mode 100644 client/src/project/new/components/ProjectIdentifier.tsx delete mode 100644 client/src/project/new/components/SubmitFormButton.tsx delete mode 100644 client/src/project/new/components/Template.tsx delete mode 100644 client/src/project/new/components/TemplateSource.tsx delete mode 100644 client/src/project/new/components/TemplateVariables.jsx delete mode 100644 client/src/project/new/components/Title.tsx delete mode 100644 client/src/project/new/components/UserTemplate.jsx delete mode 100644 client/src/project/new/index.js diff --git a/client/src/App.jsx b/client/src/App.jsx index 55a88f63cd..f25059759c 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -24,6 +24,7 @@ */ import { skipToken } from "@reduxjs/toolkit/query"; +import cx from "classnames"; import { Fragment, useContext, useEffect, useState } from "react"; import { Helmet } from "react-helmet"; import { Navigate, Route, Routes, useLocation } from "react-router"; @@ -94,7 +95,14 @@ function CentralContentContainer({ user }) { +
} diff --git a/client/src/components/addDatasetButtons/AddDatasetButtons.tsx b/client/src/components/addDatasetButtons/AddDatasetButtons.tsx deleted file mode 100644 index 0a677ae82e..0000000000 --- a/client/src/components/addDatasetButtons/AddDatasetButtons.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * AddDatasetButtons.tsx - * AddDatasetButtons components. - */ - -import { Button, ButtonGroup } from "reactstrap"; - -export interface AddDatasetButtonsProps { - optionSelected: "addDataset" | "importDataset"; - toggleNewDataset?: React.MouseEventHandler; -} - -const AddDatasetButtons = ({ - optionSelected, - toggleNewDataset, -}: AddDatasetButtonsProps) => { - return ( - - - - - ); -}; - -export default AddDatasetButtons; diff --git a/client/src/components/form-field/FormGeneratorImageInput.jsx b/client/src/components/form-field/FormGeneratorImageInput.jsx index 431ce23c17..5352c54077 100644 --- a/client/src/components/form-field/FormGeneratorImageInput.jsx +++ b/client/src/components/form-field/FormGeneratorImageInput.jsx @@ -46,7 +46,7 @@ import { InputLabel, } from "../formlabels/FormLabels"; import ImageEditor, { CARD_IMAGE_DIMENSIONS } from "../imageEditor/ImageEditor"; -import { DESIRABLE_FINAL_IMAGE_SIZE } from "../../project/new/components/NewProjectAvatar"; +import { DESIRABLE_FINAL_IMAGE_SIZE } from "../../project/components/NewProjectAvatar"; function userInputOption(options) { let userInput = options.find((o) => o[Prop.STOCK] === false); diff --git a/client/src/components/quicknav/QuickNav.present.jsx b/client/src/components/quicknav/QuickNav.present.jsx index 21fc8aa590..dcf7b19167 100644 --- a/client/src/components/quicknav/QuickNav.present.jsx +++ b/client/src/components/quicknav/QuickNav.present.jsx @@ -24,7 +24,7 @@ import { Link } from "react-router"; // ? react-autosuggest styles are defined there q_q // ? also, the order of import matters here q_q -import "../../project/new/Project.style.css"; +import "../../project/Project.style.css"; import "./QuickNav.style.css"; class QuickNavPresent extends Component { diff --git a/client/src/dataset/addtoproject/DatasetAdd.present.tsx b/client/src/dataset/addtoproject/DatasetAdd.present.tsx deleted file mode 100644 index 208cae57ae..0000000000 --- a/client/src/dataset/addtoproject/DatasetAdd.present.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { isEmpty } from "lodash-es"; -import { useState } from "react"; -import { Button, ButtonGroup, Col, Row, Table } from "reactstrap"; - -import { ContainerWrap } from "../../App"; -import { Loader } from "../../components/Loader"; -import LoginAlert from "../../components/loginAlert/LoginAlert"; -import SunsetBanner from "../../features/projectsV2/shared/SunsetV1Banner"; -import useLegacySelector from "../../utils/customHooks/useLegacySelector.hook"; -import { DatasetError } from "../DatasetError"; -import { getDatasetAuthors } from "../DatasetFunctions"; -import { - AddDatasetDataset, - AddDatasetHandlers, - AddDatasetStatus, -} from "./DatasetAdd.types"; -import AddDatasetExistingProject from "./DatasetAddToExistingProject"; -import AddDatasetNewProject from "./DatasetAddToNewProject"; - -type HeaderAddDatasetProps = { - dataset: AddDatasetDataset; -}; - -function HeaderAddDataset({ dataset }: HeaderAddDatasetProps) { - if (!dataset) return null; - const authors = getDatasetAuthors(dataset); - return ( - <> -

Add dataset

-

- Add the dataset to an already existing project, or create a new project - and add the dataset to it. -

- - - - - - - - - - - -
- Dataset Title - {dataset?.name}
- Authors - {authors}
- - ); -} - -function DatasetAddMainContent({ - dataset, - model, - handlers, - isDatasetValid, - currentStatus, - importingDataset, -}: Omit) { - const [isNewProject, setIsNewProject] = useState(false); - const logged = useLegacySelector((state) => state.stateModel.user.logged); - if (!logged) { - const textIntro = "Only authenticated users can create new projects."; - const textPost = "to create new project with dataset."; - return ( - - ); - } - const disabled = ["inProcess", "importing"].includes( - currentStatus?.status || "" - ) - ? true - : false; - const formToDisplay = !isNewProject ? ( - - ) : ( - - ); - return ( - <> - - - - - - {formToDisplay} - - ); -} - -type DatasetAddProps = { - dataset: AddDatasetDataset | null; - model: unknown; - handlers: AddDatasetHandlers; - isDatasetValid: boolean | null; - currentStatus: AddDatasetStatus | null; - importingDataset: boolean; - insideProject: boolean; -}; -function DatasetAdd(props: DatasetAddProps) { - const { dataset, insideProject } = props; - const logged = useLegacySelector((state) => state.stateModel.user.logged); - - // Return early if there is no dataset - if (!dataset) return ; - if (!dataset?.exists) { - if (!isEmpty(dataset?.fetchError)) { - return ( - - ); - } - } - - // Set different content for logged and anonymous users - - return ( - - - - - - - - - - - ); -} - -export default DatasetAdd; diff --git a/client/src/dataset/addtoproject/DatasetAdd.types.ts b/client/src/dataset/addtoproject/DatasetAdd.types.ts deleted file mode 100644 index d207a68816..0000000000 --- a/client/src/dataset/addtoproject/DatasetAdd.types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IDataset } from "../../features/project/project.types"; - -interface AddDatasetDataset extends IDataset { - fetchError: unknown; - project?: ExistingProject; -} -type AddDatasetHandlers = { - setCurrentStatus: (status: AddDatasetStatus | null) => void; - submitCallback: (project: SubmitProject) => void; - validateProject: (project: SubmitProject, isDatasetValid: boolean) => void; -}; - -type AddDatasetStatus = { status: string; text?: string }; - -type SubmitProject = { - name: string; - value: string; - default_branch: string; -}; - -interface ExistingProject extends SubmitProject { - access_level: number; - http_url_to_repo: string; - id: string; - path: string; - path_with_namespace: string; -} - -export type { - AddDatasetDataset, - AddDatasetHandlers, - AddDatasetStatus, - ExistingProject, - SubmitProject, -}; diff --git a/client/src/dataset/addtoproject/DatasetAddToExistingProject.tsx b/client/src/dataset/addtoproject/DatasetAddToExistingProject.tsx deleted file mode 100644 index 68f5f3732a..0000000000 --- a/client/src/dataset/addtoproject/DatasetAddToExistingProject.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useEffect, useRef, useState } from "react"; - -import { Button } from "reactstrap"; - -import { ACCESS_LEVELS } from "../../api-client"; -import SelectAutosuggestInput from "../../components/SelectAutosuggestInput"; -import { Loader } from "../../components/Loader"; -import { groupBy } from "../../utils/helpers/HelperFunctions"; -import useGetUserProjects from "../../utils/customHooks/UseGetProjects"; - -import type { - AddDatasetHandlers, - AddDatasetStatus, - ExistingProject, -} from "./DatasetAdd.types"; -import DatasetAddToProjectStatus from "./DatasetAddToProjectStatus"; - -type THit = Record< - "default_branch" | "id" | "name" | "subgroup" | "value", - string ->; - -type AddDatasetExistingProjectProps = { - dataset: unknown; - handlers: AddDatasetHandlers; - isDatasetValid: boolean | null; - currentStatus: AddDatasetStatus | null; - importingDataset: boolean; - project?: ExistingProject; -}; -function AddDatasetExistingProject({ - dataset, - handlers, - isDatasetValid, - currentStatus, - importingDataset, - project, -}: AddDatasetExistingProjectProps) { - const [existingProject, setExistingProject] = - useState(null); - const mounted = useRef(false); - const setCurrentStatus = handlers.setCurrentStatus; - - const projects = useGetUserProjects(); - const memberProjects = projects.projectsMember; - const isLoadingMemberProjects = projects.isFetchingProjects; - - useEffect(() => { - mounted.current = true; - setCurrentStatus(null); - return () => { - mounted.current = false; - }; - }, [setCurrentStatus]); - - useEffect(() => { - if (existingProject) handlers.validateProject(existingProject, false); - // validate origin only when start import - else setCurrentStatus(null); - }, [existingProject]); // eslint-disable-line - - const startImportDataset = () => { - if (existingProject == null) return; - handlers.submitCallback(existingProject); - }; - const onSuggestionsFetchRequested = ( - value: string, - setSuggestions: (suggestions: unknown) => void - ) => { - if (!memberProjects || isLoadingMemberProjects) return; - const featured = { member: memberProjects }; - - const regex = new RegExp(value, "i"); - const searchDomain = featured.member.filter((project: ExistingProject) => { - return project.access_level >= ACCESS_LEVELS.MAINTAINER; - }); - - const hits: Record = {}; - const groupedSuggestions = []; - - searchDomain.forEach((d: ExistingProject) => { - if (regex.exec(d.path_with_namespace) != null) { - hits[d.path_with_namespace] = { - default_branch: d.default_branch, - value: d.http_url_to_repo, - name: d.path_with_namespace, - subgroup: d.path_with_namespace.split("/")[0], - id: d.id, - }; - } - }); - - const hitValues = Object.values(hits).sort((a, b) => - a.name > b.name ? 1 : b.name > a.name ? -1 : 0 - ); - const groupedHits = groupBy(hitValues, (item: THit) => item.subgroup); - for (const [key, val] of groupedHits) - groupedSuggestions.push({ title: key, suggestions: val }); - - setCurrentStatus(null); - setSuggestions(groupedSuggestions); - }; - const customHandlers = { onSuggestionsFetchRequested }; - - let suggestionInput; - const isProjectListReady = - (memberProjects != null) != null && !isLoadingMemberProjects; - if ( - isProjectListReady && - isDatasetValid && - currentStatus?.status !== "importing" - ) { - suggestionInput = ( - - ); - } else if ( - isDatasetValid === null || - isDatasetValid === false || - currentStatus?.status === "importing" - ) { - suggestionInput = null; - } else { - suggestionInput = ( -
- Loading projects... -
- ); - } - /* buttons */ - const addDatasetButton = - currentStatus?.status === "importing" ? null : ( -
- -
- ); - - const onSubmit = (e: React.FormEvent) => e.preventDefault(); - - const addDatasetStatus = currentStatus ? ( - - ) : null; - - if (!dataset) return null; - - return ( -
-
- {suggestionInput} -
- {addDatasetStatus} - {addDatasetButton} -
- ); -} - -export default AddDatasetExistingProject; diff --git a/client/src/dataset/addtoproject/DatasetAddToNewProject.tsx b/client/src/dataset/addtoproject/DatasetAddToNewProject.tsx deleted file mode 100644 index 5665b6ec63..0000000000 --- a/client/src/dataset/addtoproject/DatasetAddToNewProject.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* ! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useContext, useEffect, useState } from "react"; -import { Link } from "react-router"; - -import { WarnAlert } from "../../components/Alert"; -import { Loader } from "../../components/Loader"; -import { NewProject } from "../../project/new/ProjectNew.container"; -import AppContext from "../../utils/context/appContext"; -import type { - AddDatasetHandlers, - AddDatasetStatus, - ExistingProject, -} from "./DatasetAdd.types"; -import DatasetAddToProjectStatus from "./DatasetAddToProjectStatus"; - -type TNewProject = { - name: string; -}; - -type AddDatasetNewProjectProps = { - dataset: unknown; - model: unknown; - handlers: AddDatasetHandlers; - isDatasetValid: boolean | null; - currentStatus: AddDatasetStatus | null; - importingDataset: boolean; - project?: ExistingProject; -}; - -function AddDatasetNewProject({ - dataset, - model, - handlers, - isDatasetValid, - currentStatus, - importingDataset, - project, -}: AddDatasetNewProjectProps) { - const [newProject, setNewProject] = useState(null); - const setCurrentStatus = handlers.setCurrentStatus; - const { client } = useContext(AppContext); - - useEffect(() => setCurrentStatus(null), [setCurrentStatus]); - - const startImportDataset = async (projectPath: string) => { - if (!client) - setCurrentStatus({ - status: "error", - text: "Unable to import the dataset", - }); - // 1. get github url of project - setCurrentStatus({ status: "importing", text: "Get new project data..." }); - const fetchProject = await client.getProject(projectPath); - const urlProjectOrigin = fetchProject?.data?.all?.web_url; // same as externalUrl - if (!urlProjectOrigin) { - setCurrentStatus({ - status: "error", - text: "Something went wrong in the creation of the project, the project is invalid", - }); - return false; - } - const default_branch = fetchProject?.data?.all?.default_branch; - // 2. create project object for importing - const project = { - default_branch, - value: urlProjectOrigin, - name: projectPath, - }; - setNewProject(project); - // 3. send to import dataset - handlers.submitCallback(project); - }; - - const addDatasetStatus = currentStatus ? ( - - ) : null; - - if (!dataset) return null; - - // if data is not ready display a loader - if (!model || !client) return ; - - // do not display form if is an import in process, error or the dataset is not valid - const form = - importingDataset || - !isDatasetValid || - ["inProcess", "importing", "error"].includes( - currentStatus?.status ?? "" - ) ? null : ( - - ); - - // in case the import fail indicate that the project was created - const extraInfo = - !importingDataset && currentStatus?.status === "error" ? ( - -
- The project was created correctly but it was not possible to import - the dataset. -
- You can view the project{" "} - - here{" "} - - or try again to import the dataset using the Existing Project{" "} - option. -
-
- ) : null; - - return ( -
- {form} - {addDatasetStatus} - {extraInfo} -
- ); -} - -export default AddDatasetNewProject; diff --git a/client/src/dataset/addtoproject/DatasetAddToProject.tsx b/client/src/dataset/addtoproject/DatasetAddToProject.tsx deleted file mode 100644 index fa7b60a578..0000000000 --- a/client/src/dataset/addtoproject/DatasetAddToProject.tsx +++ /dev/null @@ -1,395 +0,0 @@ -/*! - * Copyright 2017 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { useCallback, useContext, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router"; - -import { useCoreSupport } from "../../features/project/useProjectCoreSupport"; -import { ImportStateMessage } from "../../utils/constants/Dataset"; -import AppContext from "../../utils/context/appContext"; -import { cleanGitUrl } from "../../utils/helpers/ProjectFunctions"; -import { DatasetCoordinator } from "../Dataset.state"; -import DatasetAdd from "./DatasetAdd.present"; -import type { - AddDatasetDataset, - AddDatasetStatus, - SubmitProject, -} from "./DatasetAdd.types"; - -type DatasetImportResponse = { - data?: { - result?: { job_id: string }; - error?: { userMessage?: string; reason: string }; - }; -}; - -type ProjectDetails = { - gitUrl: string; - branch: string; -}; - -type AddDatasetToProjectProps = { - datasets: unknown; - insideProject: boolean; - model: { subModel: (arg0: string) => unknown }; -}; -function DatasetAddToProject({ - datasets, - insideProject, - model, -}: AddDatasetToProjectProps) { - const { identifier: identifier_ } = useParams<"identifier">(); - const identifier = identifier_?.replaceAll("-", ""); - - const [currentStatus, setCurrentStatus] = useState( - null - ); - const [importingDataset, setImportingDataset] = useState(false); - const [isDatasetValid, setIsDatasetValid] = useState(null); - const [dataset, setDataset] = useState(null); - const [datasetCoordinator, setDatasetCoordinator] = - useState(null); - const { client } = useContext(AppContext); - const navigate = useNavigate(); - - const [srcProjectDetails, setSrcProjectDetails] = - useState(null); - const [versionUrl, setVersionUrl] = useState(null); - - const [dstProjectDetails, setDstProjectDetails] = - useState(null); - - useEffect(() => { - setDatasetCoordinator( - new DatasetCoordinator(client, model.subModel("dataset")) - ); - }, [client, model]); - - useEffect(() => { - const fetchDataset = async () => { - if (datasetCoordinator == null) return; - await datasetCoordinator.fetchDataset(identifier, datasets, true); - const currentDataset = datasetCoordinator.get("metadata"); - setDataset(currentDataset); - }; - - if (datasetCoordinator && identifier) { - const currentDataset = datasetCoordinator.get("metadata"); - if ( - currentDataset && - currentDataset?.identifier === identifier && - currentDataset.projects - ) - setDataset(currentDataset); - else fetchDataset(); - } - }, [datasetCoordinator && identifier]); // eslint-disable-line - - /* validate project */ - const validateDatasetProject = useCallback( - async (isSubmit = false) => { - // check dataset has valid project url - setCurrentStatus({ - status: isSubmit ? "importing" : "inProcess", - text: "Checking dataset...", - }); - if (!dataset?.project || !dataset?.project.path) { - setCurrentStatus({ - status: "error", - text: "Invalid Dataset, refresh the page to get updated values", - }); - setIsDatasetValid(false); - return false; - } - - // fetch dataset project values and check it has a valid git url - // TODO remove this request when dataset include httpUrlToRepo - try { - const fetchDatasetProject = await client.getProject( - dataset.project?.path - ); - const urlProjectOrigin = fetchDatasetProject?.data?.all?.web_url; - if (!urlProjectOrigin) { - setCurrentStatus({ status: "error", text: "Invalid Dataset" }); - setIsDatasetValid(false); - return false; - } - const branch = fetchDatasetProject?.data?.all?.default_branch; - setSrcProjectDetails({ gitUrl: urlProjectOrigin, branch }); - } catch (e) { - setCurrentStatus({ status: "error", text: "Invalid Dataset" }); - setIsDatasetValid(false); - return false; - } - setIsDatasetValid(true); - }, - [dataset, client] - ); - - useEffect(() => { - if (dataset) validateDatasetProject(); - }, [dataset, validateDatasetProject]); - - const { coreSupport: srcCoreSupport } = useCoreSupport({ - gitUrl: srcProjectDetails?.gitUrl ?? undefined, - branch: srcProjectDetails?.branch ?? undefined, - }); - - // check whether the selected dataset is supported - useEffect(() => { - if (!srcCoreSupport.computed) return; - if ( - !srcCoreSupport.backendAvailable && - srcCoreSupport.backendErrorMessage != null - ) { - setCurrentStatus({ status: "error", text: "Invalid Dataset" }); - setIsDatasetValid(false); - return; - } - setIsDatasetValid(true); - if (!srcCoreSupport.backendAvailable) { - setCurrentStatus({ - status: "error", - text: `The dataset project version ${srcCoreSupport.metadataVersion} is not supported`, - }); - return; - } - setCurrentStatus(null); - }, [srcCoreSupport]); - - const validateProject = async ( - project: SubmitProject, - _validateOrigin: boolean, - isSubmit = false - ) => { - if (!project) return false; - const processStatus = isSubmit ? "importing" : "inProcess"; - - // start checking project - setCurrentStatus({ - status: processStatus, - text: "Checking dataset/project compatibility...", - }); - // check selected project migration status - // We utilize the externalUrl property, which typically doesn't include ".git". - // However, graphql return the httpUrlToRepo, so we remove ".git". - const gitUrl = cleanGitUrl(project.value); - const branch = project.default_branch; - setDstProjectDetails({ gitUrl, branch }); - }; - - // check whether the target dataset is supported - const { coreSupport: dstCoreSupport } = useCoreSupport({ - gitUrl: dstProjectDetails?.gitUrl ?? undefined, - branch: dstProjectDetails?.branch ?? undefined, - }); - useEffect(() => { - if (!dstCoreSupport.computed) return; - if ( - !dstCoreSupport.backendAvailable && - dstCoreSupport.backendErrorMessage != null - ) { - setCurrentStatus({ - status: "error", - text: "There is a problem with the destination project that prevents adding the dataset.", - }); - return; - } - if (!dstCoreSupport.backendAvailable) { - setCurrentStatus({ - status: "error", - text: "The target project is either too old or unavailable.", - }); - return; - } - setVersionUrl(dstCoreSupport.versionUrl); - - // wait for this to be set - if (!srcCoreSupport.computed || !srcCoreSupport.backendAvailable) return; - if (dstCoreSupport.metadataVersion < srcCoreSupport.metadataVersion) { - setCurrentStatus({ - status: "error", - text: `Source project metadata version (${srcCoreSupport.metadataVersion}) - cannot be newer than the project metadata version (${dstCoreSupport.metadataVersion}) for import.`, - }); - return; - } - setCurrentStatus({ - status: "validProject", - text: "Selected project is compatible with dataset.", - }); - }, [srcCoreSupport, dstCoreSupport]); - - /* end validate project */ - - /* import dataset */ - const submitCallback = async (project: SubmitProject) => { - if (!project) setCurrentStatus({ status: "error", text: "Empty project" }); - setCurrentStatus({ - status: "importing", - text: ImportStateMessage.ENQUEUED, - }); - importDataset(project); - }; - - const importDataset = (selectedProject: SubmitProject) => { - if (dataset == null) return; - setImportingDataset(true); - client - .datasetImport( - cleanGitUrl(selectedProject.value), - dataset.url, - versionUrl, // this will be undefined for a new project, so the latest version will be used - selectedProject.default_branch - ? selectedProject.default_branch - : dstProjectDetails?.branch - ) - .then((response: DatasetImportResponse) => { - if (response?.data?.error !== undefined) { - const error = response.data.error; - setCurrentStatus({ - status: "error", - text: error.userMessage ? error.userMessage : error.reason, - }); - setImportingDataset(false); - } else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain - const jobId = response.data?.result?.job_id!; - monitorJobStatusAndHandleResponse( - jobId, - selectedProject.name, - dataset.slug ?? "" - ); - } - }); - }; - - const monitorJobStatusAndHandleResponse = ( - job_id: string, - projectPath: string, - datasetSlug: string - ) => { - let cont = 0; - const INTERVAL = 6000; - const monitorJob = setInterval(async () => { - try { - const job = await client.getJobStatus(job_id, versionUrl); - cont++; - if (job !== undefined) - handleJobResponse( - job, - monitorJob, - (cont * INTERVAL) / 1000, - projectPath, - datasetSlug - ); - } catch (e) { - const error = e as { message?: string }; - setCurrentStatus({ status: "error", text: error.message }); - setImportingDataset(false); - clearInterval(monitorJob); - } - }, INTERVAL); - }; - - function handleJobResponse( - job: { state: string; extras: { error: string } }, - monitorJob: ReturnType, - waitedSeconds: number, - projectPath: string, - datasetSlug: string - ) { - if (!job) return; - switch (job.state) { - case "ENQUEUED": - setCurrentStatus({ - status: "importing", - text: ImportStateMessage.ENQUEUED, - }); - break; - case "IN_PROGRESS": - setCurrentStatus({ - status: "importing", - text: ImportStateMessage.IN_PROGRESS, - }); - break; - case "COMPLETED": - setCurrentStatus({ - status: "completed", - text: ImportStateMessage.COMPLETED, - }); - setImportingDataset(false); - clearInterval(monitorJob); - redirectUser(projectPath, datasetSlug); - break; - case "FAILED": - setCurrentStatus({ - status: "error", - text: ImportStateMessage.FAILED + job.extras.error, - }); - setImportingDataset(false); - clearInterval(monitorJob); - break; - default: - setCurrentStatus({ - status: "error", - text: ImportStateMessage.FAILED_NO_INFO, - }); - setImportingDataset(false); - clearInterval(monitorJob); - break; - } - if ( - (waitedSeconds > 180 && job.state !== "IN_PROGRESS") || - (waitedSeconds > 360 && job.state === "IN_PROGRESS") - ) { - setCurrentStatus({ status: "error", text: ImportStateMessage.TOO_LONG }); - setImportingDataset(false); - clearInterval(monitorJob); - } - } - - const redirectUser = (projectPath: string, datasetSlug: string) => { - setCurrentStatus(null); - navigate( - { pathname: `/projects/${projectPath}/datasets/${datasetSlug}` }, - { state: { reload: true } } - ); - }; - /* end import dataset */ - - const handlers = { - setCurrentStatus, - submitCallback, - validateProject, - }; - - return ( - - ); -} - -export default DatasetAddToProject; diff --git a/client/src/dataset/addtoproject/DatasetAddToProjectStatus.tsx b/client/src/dataset/addtoproject/DatasetAddToProjectStatus.tsx deleted file mode 100644 index 58b19b45a1..0000000000 --- a/client/src/dataset/addtoproject/DatasetAddToProjectStatus.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Link } from "react-router"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faCheck, - faExclamationTriangle, -} from "@fortawesome/free-solid-svg-icons"; - -import { ErrorAlert } from "../../components/Alert"; -import { Loader } from "../../components/Loader"; -import ProgressIndicator, { - ProgressStyle, - ProgressType, -} from "../../components/progress/Progress"; -import { Url } from "../../utils/helpers/url"; - -import type { AddDatasetStatus } from "./DatasetAdd.types"; - -interface DatasetAddToProjectStatusProps extends AddDatasetStatus { - projectName?: string; -} -function DatasetAddToProjectStatus(props: DatasetAddToProjectStatusProps) { - const { status, text, projectName } = props; - let statusProject = null; - const updateUrl = Url.get(Url.pages.project.settings, { - namespace: "", - path: projectName, - }); - switch (status) { - case "errorNeedMigration": - statusProject = ( -
- {" "} - This project must be upgraded. -
- The target project ({projectName}) needs to be upgraded before - datasets can be imported into it. -
- - More info - -
- ); - break; - case "error": - statusProject = ( - -

Error

-

{text}

-
- ); - break; - case "inProcess": - statusProject = ( -
- {text} -
- ); - break; - case "importing": - statusProject = ( - - ); - break; - case "validProject": - statusProject = ( -
- {text} -
- ); - break; - case "completed": - statusProject = ( -
- {text} -
- ); - break; - default: - statusProject = null; - } - return statusProject ? ( -
{statusProject}
- ) : ( - statusProject - ); -} - -export default DatasetAddToProjectStatus; diff --git a/client/src/dataset/addtoproject/LazyDatasetAddToProject.tsx b/client/src/dataset/addtoproject/LazyDatasetAddToProject.tsx deleted file mode 100644 index 5f74b578d7..0000000000 --- a/client/src/dataset/addtoproject/LazyDatasetAddToProject.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/*! - * Copyright 2024 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentProps, Suspense, lazy } from "react"; -import PageLoader from "../../components/PageLoader"; - -const DatasetAddToProject = lazy(() => import("./DatasetAddToProject")); - -export default function LazyDatasetAddToProject( - props: ComponentProps -) { - return ( - }> - - - ); -} diff --git a/client/src/features/landing/AnonymousHome.tsx b/client/src/features/landing/AnonymousHome.tsx index 0eae2b97f8..178d04514e 100644 --- a/client/src/features/landing/AnonymousHome.tsx +++ b/client/src/features/landing/AnonymousHome.tsx @@ -52,11 +52,11 @@ import { BottomNav, TopNav } from "./components/anonymousHomeNav"; // ? react-autosuggest styles are defined there q_q // ? also, the order of import matters here q_q -import "../../project/new/Project.style.css"; +import "../../project/Project.style.css"; // ? the "quick-nav" class is used in this file import "../../components/quicknav/QuickNav.style.css"; -import { GetStarted } from "./components/GetStarted/GetStarted.tsx"; -import { RenkuUsers } from "./components/RenkuUsers/RenkuUsers.tsx"; +import { GetStarted } from "./components/GetStarted/GetStarted"; +import { RenkuUsers } from "./components/RenkuUsers/RenkuUsers"; export default function AnonymousHome() { const { client, model, params } = useContext(AppContext); diff --git a/client/src/features/project/components/ProjectSettingAvatar.tsx b/client/src/features/project/components/ProjectSettingAvatar.tsx index f3a58ca398..def65e2d33 100644 --- a/client/src/features/project/components/ProjectSettingAvatar.tsx +++ b/client/src/features/project/components/ProjectSettingAvatar.tsx @@ -30,7 +30,7 @@ import InlineSubmitImageInput, { ImageValue, } from "../../../components/inlineSubmitImageInput/InlineSubmitImageInput"; import { InputCard } from "../../../components/inlineSubmitInput/InlineSubmitInput"; -import { PROJECT_AVATAR_MAX_SIZE } from "../../../project/new/components/NewProjectAvatar"; +import { PROJECT_AVATAR_MAX_SIZE } from "../../../project/components/NewProjectAvatar.tsx"; import { getEntityImageUrl } from "../../../utils/helpers/HelperFunctions"; import { ImagesLinks } from "../project.types"; import { diff --git a/client/src/features/project/components/ProjectSettingsGeneral.tsx b/client/src/features/project/components/ProjectSettingsGeneral.tsx index 91ffde26e4..adcf6c22a0 100644 --- a/client/src/features/project/components/ProjectSettingsGeneral.tsx +++ b/client/src/features/project/components/ProjectSettingsGeneral.tsx @@ -23,7 +23,7 @@ import { ProjectKnowledgeGraph } from "./migrations/ProjectKgStatus"; import { ProjectSettingsGeneralDeleteProject } from "./ProjectSettingsGeneralDeleteProject"; import { NotificationsManager } from "../../../notifications/notifications.types"; import { ProjectSettingsDescription } from "./ProjectSettingsDescription"; -import { EditVisibility } from "../../../project/new/components/Visibility"; +import { EditVisibility } from "../../../project/components/Visibility"; import { Visibilities } from "../../../components/visibility/Visibility"; import ProjectKeywordsInput from "../../../project/shared/ProjectKeywords"; import { ProjectSettingsAvatar } from "./ProjectSettingAvatar"; diff --git a/client/src/features/project/dataset/DatasetModify.tsx b/client/src/features/project/dataset/DatasetModify.tsx index 32d381c3a2..8a22f61d9d 100644 --- a/client/src/features/project/dataset/DatasetModify.tsx +++ b/client/src/features/project/dataset/DatasetModify.tsx @@ -36,7 +36,7 @@ import KeywordsInput from "../../../components/form-field/KeywordsInput"; import TextAreaInput from "../../../components/form-field/TextAreaInput"; import TextInput from "../../../components/form-field/TextInput"; import type { RenkuUser } from "../../../model/renkuModels.types"; -import { FormErrorFields } from "../../../project/new/components/FormValidations"; +import { FormErrorFields } from "../../../project/components/FormValidations"; import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook"; import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; import type { AppDispatch } from "../../../utils/helpers/EnhancedState"; diff --git a/client/src/features/project/dataset/ProjectDatasetImport.tsx b/client/src/features/project/dataset/ProjectDatasetImport.tsx deleted file mode 100644 index a56bc65af7..0000000000 --- a/client/src/features/project/dataset/ProjectDatasetImport.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import DatasetImport from "../../../project/datasets/import"; -import type { DatasetImportProps } from "../../../project/datasets/import/DatasetImport"; -import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; -import type { StateModelProject } from "../project.types"; - -type ProjectDatasetImportProps = { - client: DatasetImportProps["client"]; - fetchDatasets: DatasetImportProps["fetchDatasets"]; - model: unknown; - notifications: unknown; - params: unknown; - toggleNewDataset: DatasetImportProps["toggleNewDataset"]; -}; - -function ProjectDatasetImport(props: ProjectDatasetImportProps) { - const project = useLegacySelector( - (state) => state.stateModel.project - ); - const projectMetadata = project.metadata; - const accessLevel = projectMetadata.accessLevel; - const externalUrl = projectMetadata.externalUrl; - - const projectPath = projectMetadata.path; - const projectNamespace = projectMetadata.namespace; - const projectPathWithNamespace = `${projectNamespace}/${projectPath}`; - - return ( - - ); -} - -export default ProjectDatasetImport; diff --git a/client/src/features/project/dataset/ProjectDatasetNewEdit.tsx b/client/src/features/project/dataset/ProjectDatasetNewEdit.tsx index 7d2e26a672..5a6549ec9d 100644 --- a/client/src/features/project/dataset/ProjectDatasetNewEdit.tsx +++ b/client/src/features/project/dataset/ProjectDatasetNewEdit.tsx @@ -26,7 +26,6 @@ import { Alert, Button, Col } from "reactstrap"; import { ACCESS_LEVELS } from "../../../api-client"; import { Loader } from "../../../components/Loader"; -import AddDatasetButtons from "../../../components/addDatasetButtons/AddDatasetButtons"; import FormSchema from "../../../components/formschema/FormSchema"; import ProgressIndicator, { ProgressStyle, @@ -35,7 +34,6 @@ import ProgressIndicator, { import useAppDispatch from "../../../utils/customHooks/useAppDispatch.hook"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; import { Url } from "../../../utils/helpers/url"; -import SunsetBanner from "../../projectsV2/shared/SunsetV1Banner"; import type { IDatasetFiles, StateModelProject } from "../project.types"; import type { DatasetModifyDisplayProps, @@ -44,7 +42,7 @@ import type { } from "./DatasetModify"; import DatasetModify from "./DatasetModify"; import type { DatasetPostClient } from "./datasetCore.api"; -import { initializeForDataset, initializeForUser } from "./datasetForm.slice"; +import { initializeForDataset } from "./datasetForm.slice"; type ChangeDatasetProps = { apiVersion: string | undefined; @@ -69,23 +67,6 @@ type ProjectDatasetEditOnlyProps = { datasetId: string; }; -function DatasetCreateHelp(props: ProjectDatasetNewOnlyProps) { - return ( - - Create a new dataset by providing metadata and content. Use  - -  to reuse an existing dataset. - - ); -} - type ProjectDatasetNewEditProps = ChangeDatasetProps & DatasetModifyDisplayProps & Partial & @@ -192,61 +173,6 @@ function ProjectDatasetNewEdit(props: ProjectDatasetNewEditProps) { /> ); } - -function ProjectDatasetNew( - props: Omit & - ProjectDatasetNewOnlyProps -) { - const location = useLocation(); - - const project = useLegacySelector( - (state) => state.stateModel.project - ); - const projectPathWithNamespace = project.metadata.pathWithNamespace; - const user = useLegacySelector((state) => state.stateModel.user); - const dispatch = useAppDispatch(); - React.useEffect(() => { - dispatch(initializeForUser({ location, projectPathWithNamespace, user })); - }, [dispatch, location, projectPathWithNamespace, user]); - - const [submitting, setSubmitting] = React.useState(false); - return ( - - } - > -
- -
- -
- -
-
- ); -} - function ProjectDatasetEditForm( props: ChangeDatasetProps & DatasetModifyDisplayProps & @@ -352,4 +278,4 @@ function ProjectDatasetEdit(props: ProjectDatasetEditProps) { ); } -export { ProjectDatasetEdit, ProjectDatasetNew }; +export { ProjectDatasetEdit }; diff --git a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx index 9c01cd31e9..6c9a584623 100644 --- a/client/src/features/projectsV2/shared/SunsetV1Banner.tsx +++ b/client/src/features/projectsV2/shared/SunsetV1Banner.tsx @@ -7,7 +7,7 @@ import SunsetV1Button from "./SunsetV1Button"; export default function SunsetBanner() { return ( - +

Project creation no longer available

You can no longer create new projects or datasets in Renku Legacy. diff --git a/client/src/features/session/components/SessionSaveWarning.tsx b/client/src/features/session/components/SessionSaveWarning.tsx index f59e829f09..aa561830e9 100644 --- a/client/src/features/session/components/SessionSaveWarning.tsx +++ b/client/src/features/session/components/SessionSaveWarning.tsx @@ -17,8 +17,6 @@ */ import cx from "classnames"; -import { useCallback, useContext, useState } from "react"; -import { Button, Modal } from "reactstrap"; import { ACCESS_LEVELS } from "../../../api-client"; import { useLoginUrl } from "../../../authentication/useLoginUrl.hook"; @@ -26,9 +24,7 @@ import { InfoAlert } from "../../../components/Alert"; import { ExternalLink } from "../../../components/ExternalLinks"; import { User } from "../../../model/renkuModels.types"; import { ProjectMetadata } from "../../../notebooks/components/session.types"; -import { ForkProject } from "../../../project/new"; import { Docs } from "../../../utils/constants/Docs"; -import AppContext from "../../../utils/context/appContext"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; export default function SessionSaveWarning() { @@ -77,9 +73,6 @@ export default function SessionSaveWarning() { save your work, consider one of the following:

    -
  • - and start a session from your fork. -
  • (false); - const toggleIsOpen = useCallback(() => setIsOpen((isOpen) => !isOpen), []); - - const { id, title, visibility } = useLegacySelector< - ProjectMetadata & { id?: number } - >((state) => state.stateModel.project.metadata); - - return ( - <> - - - - - - ); -} diff --git a/client/src/features/session/components/StartNewSession.tsx b/client/src/features/session/components/StartNewSession.tsx index 5fb0fea065..9cb3c17fcb 100644 --- a/client/src/features/session/components/StartNewSession.tsx +++ b/client/src/features/session/components/StartNewSession.tsx @@ -32,7 +32,7 @@ import { useNavigate, type Location, } from "react-router"; -import { Button, Col, DropdownItem, Form, Modal, Row } from "reactstrap"; +import { Button, Col, DropdownItem, Form, Row } from "reactstrap"; import { ACCESS_LEVELS } from "../../../api-client"; import { useLoginUrl } from "../../../authentication/useLoginUrl.hook"; @@ -51,7 +51,6 @@ import ProgressStepsIndicator, { import { ShareLinkSessionModal } from "../../../components/shareLinkSession/ShareLinkSession"; import { LockStatus, User } from "../../../model/renkuModels.types"; import { ProjectMetadata } from "../../../notebooks/components/session.types"; -import { ForkProject } from "../../../project/new"; import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants"; import { Docs } from "../../../utils/constants/Docs"; import AppContext from "../../../utils/context/appContext"; @@ -677,9 +676,6 @@ function SessionSaveWarning() { save your work, consider one of the following:

      -
    • - and start a session from your fork. -
    • (false); - const toggleIsOpen = useCallback(() => setIsOpen((isOpen) => !isOpen), []); - - const { id, title, visibility } = useLegacySelector< - ProjectMetadata & { id?: number } - >((state) => state.stateModel.project.metadata); - - return ( - <> - - - - - - ); -} - function StartNewSessionOptions() { const dispatch = useAppDispatch(); diff --git a/client/src/project/Project.present.jsx b/client/src/project/Project.present.jsx index 5ffb06159f..56055657e7 100644 --- a/client/src/project/Project.present.jsx +++ b/client/src/project/Project.present.jsx @@ -35,7 +35,6 @@ import { CardBody, CardHeader, Col, - Modal, Nav, NavItem, Row, @@ -74,7 +73,6 @@ import GitLabConnectButton, { } from "./components/GitLabConnect"; import { ProjectViewNotFound } from "./components/ProjectViewNotFound"; import FilesTreeView from "./filestreeview/FilesTreeView"; -import { ForkProject } from "./new"; import { ProjectOverviewCommits, ProjectOverviewStats } from "./overview"; function filterPaths(paths, blacklist) { @@ -91,11 +89,7 @@ function isRequestPending(props, request) { return requests[request] === SpecialPropVal.UPDATING; } -function ToggleForkModalButton({ - forkProjectDisabled, - showForkCount, - toggleModal, -}) { +function ToggleForkModalButton({ forkProjectDisabled, showForkCount }) { // display the button a bit differently if showForkCount == false if (showForkCount == false) { return ( @@ -104,7 +98,6 @@ function ToggleForkModalButton({ color="primary" size="sm" disabled={forkProjectDisabled} - onClick={toggleModal} > Fork the project @@ -115,12 +108,8 @@ function ToggleForkModalButton({ id="fork-project" className="btn-outline-rk-green" disabled={forkProjectDisabled} - onClick={toggleModal} > Fork - - Fork your own copy of this project - ); } @@ -137,7 +126,6 @@ function ForkCountButton({ forkProjectDisabled, externalUrl, forksCount }) { rel="noreferrer noopener" > {forksCount} - Forks ); } @@ -146,58 +134,42 @@ class ForkProjectModal extends Component { constructor(props) { super(props); this.state = { open: false }; - this.toggleFunction = this.toggle.bind(this); - } - - toggle() { - this.setState({ open: !this.state.open }); } render() { - let content = null; - // this prevents flashing wrong content during the close animation - if (this.state.open) { - content = ( - - ); - } // Treat undefined as true const buttons = this.props.showForkCount === false ? ( - - ) : ( - +
      - - + + Fork a project is no longer supported in Renku Legacy. Switch to + Renku 2.0 to continue creating and managing your work. + +
      + ) : ( +
      + + + + + + Fork a project is no longer supported in Renku Legacy. Switch to + Renku 2.0 to continue creating and managing your work. + +
      ); - return ( - - {buttons} - - {content} - - - ); + return {buttons}; } } diff --git a/client/src/project/new/Project.style.css b/client/src/project/Project.style.css similarity index 100% rename from client/src/project/new/Project.style.css rename to client/src/project/Project.style.css diff --git a/client/src/project/new/components/FormValidations.tsx b/client/src/project/components/FormValidations.tsx similarity index 52% rename from client/src/project/new/components/FormValidations.tsx rename to client/src/project/components/FormValidations.tsx index 8b36868274..20fc1d869e 100644 --- a/client/src/project/new/components/FormValidations.tsx +++ b/client/src/project/components/FormValidations.tsx @@ -22,51 +22,9 @@ * FormValidation.tsx * FormValidation components. */ -import { toCapitalized as capitalize } from "../../../utils/helpers/HelperFunctions"; -import { - ErrorLabel, - HelperLabel, -} from "../../../components/formlabels/FormLabels"; -import { NewProjectInputs, NewProjectMeta } from "./newProject.types"; +import { ErrorLabel } from "../../components/formlabels/FormLabels"; /* eslint-disable @typescript-eslint/no-explicit-any */ - -interface FormWarningsProps { - meta: NewProjectMeta | any; -} - -const FormWarnings = ({ meta }: FormWarningsProps) => { - const warnings = Object.keys(meta.validation.warnings); - - if (!warnings.length) return null; - - let message = ""; - for (const warningsKey of warnings) - message += `${meta.validation.warnings[warningsKey]}\n`; - - return ( -
      - -
      - ); -}; - -interface FormErrorsProps { - meta: NewProjectMeta | any; - input: NewProjectInputs | any; -} -const FormErrors = ({ meta, input }: FormErrorsProps) => { - const errorFields = meta.validation.errors - ? Object.keys(meta.validation.errors) - .filter((field) => !input[`${field}Pristine`]) // don't consider pristine fields - .map((field) => capitalize(field)) - : []; - - if (!errorFields.length) return null; - - return ; -}; - interface FormErrorFieldsProps { errorFields: string[]; } @@ -83,4 +41,4 @@ const FormErrorFields = ({ errorFields }: FormErrorFieldsProps) => { ); }; -export { FormErrors, FormWarnings, FormErrorFields }; +export { FormErrorFields }; diff --git a/client/src/project/new/components/NewProjectAvatar.tsx b/client/src/project/components/NewProjectAvatar.tsx similarity index 93% rename from client/src/project/new/components/NewProjectAvatar.tsx rename to client/src/project/components/NewProjectAvatar.tsx index b45da4d152..52ccfcbd2c 100644 --- a/client/src/project/new/components/NewProjectAvatar.tsx +++ b/client/src/project/components/NewProjectAvatar.tsx @@ -25,8 +25,8 @@ import { useEffect, useState } from "react"; -import ImageInput from "../../../components/form-field/FormGeneratorImageInput"; -import { ImageInputMode } from "../../../components/form-field/FormGeneratorImageInput"; +import ImageInput from "../../components/form-field/FormGeneratorImageInput"; +import { ImageInputMode } from "../../components/form-field/FormGeneratorImageInput"; // 3 MB -- GitLab has a 200kB limit, but a 3 MB file should be small enough after cropping const PROJECT_AVATAR_MAX_SIZE = 3 * 1024 * 1024; diff --git a/client/src/project/new/components/ShareLinkModal.jsx b/client/src/project/components/ShareLinkModal.jsx similarity index 98% rename from client/src/project/new/components/ShareLinkModal.jsx rename to client/src/project/components/ShareLinkModal.jsx index 83703ca457..e965468e62 100644 --- a/client/src/project/new/components/ShareLinkModal.jsx +++ b/client/src/project/components/ShareLinkModal.jsx @@ -34,7 +34,7 @@ import { Col, Form, } from "reactstrap"; -import { CommandCopy } from "../../../components/commandCopy/CommandCopy"; +import { CommandCopy } from "../../components/commandCopy/CommandCopy"; function ShareLinkModal(props) { const { createUrl, input } = props; diff --git a/client/src/project/new/components/Visibility.tsx b/client/src/project/components/Visibility.tsx similarity index 91% rename from client/src/project/new/components/Visibility.tsx rename to client/src/project/components/Visibility.tsx index d1f5c05eee..dc18d5f03d 100644 --- a/client/src/project/new/components/Visibility.tsx +++ b/client/src/project/components/Visibility.tsx @@ -33,25 +33,25 @@ import { ModalHeader, } from "reactstrap"; -import { SuccessAlert } from "../../../components/Alert"; -import { ExternalLink } from "../../../components/ExternalLinks"; -import { Loader } from "../../../components/Loader"; -import { RtkErrorAlert } from "../../../components/errors/RtkErrorAlert"; -import { LoadingLabel } from "../../../components/formlabels/FormLabels"; +import { SuccessAlert } from "../../components/Alert"; +import { ExternalLink } from "../../components/ExternalLinks"; +import { Loader } from "../../components/Loader"; +import { RtkErrorAlert } from "../../components/errors/RtkErrorAlert"; +import { LoadingLabel } from "../../components/formlabels/FormLabels"; import VisibilitiesInput, { VISIBILITY_ITEMS, Visibilities, -} from "../../../components/visibility/Visibility"; -import { SettingRequiresKg } from "../../../features/project/components/ProjectSettingsUtils"; -import { useGetProjectByIdQuery } from "../../../features/project/projectGitLab.api"; +} from "../../components/visibility/Visibility"; +import { SettingRequiresKg } from "../../features/project/components/ProjectSettingsUtils"; +import { useGetProjectByIdQuery } from "../../features/project/projectGitLab.api"; import { useGetProjectIndexingStatusQuery, useProjectMetadataQuery, useUpdateProjectMutation, -} from "../../../features/project/projectKg.api"; -import { useGetGroupByPathQuery } from "../../../features/projects/projects.api"; -import { GitlabLinks } from "../../../utils/constants/Docs"; -import { computeVisibilities } from "../../../utils/helpers/HelperFunctions"; +} from "../../features/project/projectKg.api"; +import { useGetGroupByPathQuery } from "../../features/projects/projects.api"; +import { GitlabLinks } from "../../utils/constants/Docs"; +import { computeVisibilities } from "../../utils/helpers/HelperFunctions"; import { NewProjectHandlers, NewProjectInputs, diff --git a/client/src/project/new/components/newProject.types.ts b/client/src/project/components/newProject.types.ts similarity index 95% rename from client/src/project/new/components/newProject.types.ts rename to client/src/project/components/newProject.types.ts index e336ceedb6..9a0da9af26 100644 --- a/client/src/project/new/components/newProject.types.ts +++ b/client/src/project/components/newProject.types.ts @@ -23,7 +23,7 @@ * New Project definitions */ -import { Visibilities } from "../../../components/visibility/Visibility"; +import { Visibilities } from "../../components/visibility/Visibility"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ diff --git a/client/src/project/datasets/import/DatasetImport.tsx b/client/src/project/datasets/import/DatasetImport.tsx deleted file mode 100644 index 337284184c..0000000000 --- a/client/src/project/datasets/import/DatasetImport.tsx +++ /dev/null @@ -1,461 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * DatasetImportForm.present.js - * Presentational components. - */ - -import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { useState } from "react"; -import { SubmitHandler, useForm } from "react-hook-form"; -import { useLocation, useNavigate } from "react-router"; -import { Alert, Button, Col, UncontrolledAlert } from "reactstrap"; - -import { ACCESS_LEVELS } from "../../../api-client"; -import AddDatasetButtons from "../../../components/addDatasetButtons/AddDatasetButtons"; -import TextInput from "../../../components/form-field/TextInput"; -import FormSchema from "../../../components/formschema/FormSchema"; -import ProgressIndicator, { - ProgressStyle, - ProgressType, -} from "../../../components/progress/Progress"; -import { useCoreSupport } from "../../../features/project/useProjectCoreSupport"; -import SunsetBanner from "../../../features/projectsV2/shared/SunsetV1Banner"; -import { ImportStateMessage } from "../../../utils/constants/Dataset"; -import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; - -type DatasetImportClient = { - datasetImport: ( - httpProjectUrl: string, - uri: string, - versionUrl: string, - branch: string - ) => Promise; - getJobStatus: (job_id: string, versionUrl: string) => Promise; -}; - -type DatasetInputFormFields = { - uri: string; -}; - -type DatasetImportFormProps = { - accessLevel: number; - formLocation: string; - onCancel: () => void; - serverErrors: string | undefined; - submitCallback: SubmitHandler; - submitLoader: { value: boolean; text: string }; - toggleNewDataset: () => void; -}; - -type DatasetInputSubmitGroupProps = Pick< - DatasetImportFormProps, - "submitLoader" | "onCancel" ->; - -type DatasetImportResult = - | { - data: { - error: undefined; - result: { - job_id: string; - }; - }; - } - | { - data: { error: { userMessage?: string; reason: string } }; - }; - -type JobStatus = - | { - state: "ENQUEUED" | "IN_PROGRESS" | "COMPLETED"; - } - | { - state: "FAILED"; - extras: { - error: string; - }; - }; - -function DatasetInputSubmitGroup({ - onCancel, - submitLoader, -}: DatasetInputSubmitGroupProps) { - const buttonColor = "rk-pink"; - - return ( -
      - - -
      - ); -} - -function DatasetImportForm( - props: DatasetImportFormProps & { formValues: DatasetInputFormFields } -) { - const desc = ( - - Import a published dataset from Zenodo, Dataverse, or from another Renku - project. Use  - -  to make a new dataset. - - ); - - const { - formState: { errors }, - handleSubmit, - register, - } = useForm({ - defaultValues: { - uri: props.formValues.uri, - }, - }); - - return ( - -
      - - -
      -
      - - Renku dataset URL; Dataverse or Zenodo dataset URL or DOI - - } - label="Dataset URI" - name="uri" - required={true} - register={register("uri", { required: "A dataset URI is required" })} - /> - - - {props.serverErrors ? ( - -
      -

      Errors occurred while performing this operation.

      -

      {props.serverErrors}

      -
      -
      - ) : null} -
      - ); -} - -type ImportDatasetArgs = { - client: DatasetImportClient; - branch: string; - externalUrl: string; - formValues: DatasetInputFormFields; - handlers: { - setSubmitLoader: React.Dispatch< - React.SetStateAction<{ - value: boolean; - text: string; - }> - >; - setServerErrors: React.Dispatch>; - }; - redirectUser: () => void; - versionUrl: string; -}; -async function importDatasetAndWaitForResult({ - client, - externalUrl, - formValues, - handlers, - redirectUser, - versionUrl, - branch, -}: ImportDatasetArgs) { - const response = await client.datasetImport( - externalUrl, - formValues.uri, - versionUrl, - branch - ); - if (response.data.error !== undefined) { - const error = response.data.error; - handlers.setSubmitLoader({ value: false, text: "" }); - handlers.setServerErrors( - error.userMessage ? error.userMessage : error.reason - ); - return; - } - - const job_id = response.data.result.job_id; - - // Monitor job status - await new Promise((resolve) => { - let pollCount = 0; - const monitorJob = setInterval(() => { - client - .getJobStatus(job_id, versionUrl) - .then((job) => { - if (job == null) return; - switch (job.state) { - case "ENQUEUED": - handlers.setSubmitLoader({ - value: true, - text: ImportStateMessage.ENQUEUED, - }); - break; - case "IN_PROGRESS": - handlers.setSubmitLoader({ - value: true, - text: ImportStateMessage.IN_PROGRESS, - }); - break; - case "COMPLETED": - handlers.setSubmitLoader({ value: false, text: "" }); - clearInterval(monitorJob); - redirectUser(); - resolve(); - break; - case "FAILED": - handlers.setSubmitLoader({ value: false, text: "" }); - handlers.setServerErrors( - ImportStateMessage.FAILED + job.extras.error - ); - clearInterval(monitorJob); - resolve(); - break; - default: - handlers.setSubmitLoader({ - value: false, - text: ImportStateMessage.FAILED_NO_INFO, - }); - handlers.setServerErrors(ImportStateMessage.FAILED_NO_INFO); - clearInterval(monitorJob); - resolve(); - break; - } - }) - .finally(() => { - pollCount++; - if (pollCount >= 50) { - handlers.setSubmitLoader({ value: false, text: "" }); - handlers.setServerErrors(ImportStateMessage.TOO_LONG); - clearInterval(monitorJob); - resolve(); - return; - } - }); - }, 5_000); - }); -} - -function DatasetImportContainer( - props: DatasetImportProps & { branch: string; versionUrl: string } -) { - const { externalUrl, fetchDatasets, projectPathWithNamespace, versionUrl } = - props; - - const location = useLocation(); - const navigate = useNavigate(); - - const formLocation = location.pathname + "/import"; - const [submitLoader, setSubmitLoader] = useState< - DatasetImportFormProps["submitLoader"] - >({ value: false, text: "Please wait..." }); - const [uri, setUri] = useState(""); - const [serverErrors, setServerErrors] = useState(); - - const onCancel = React.useCallback(() => { - navigate(`/projects/${projectPathWithNamespace}/datasets`); - }, [navigate, projectPathWithNamespace]); - - const redirectUser = React.useCallback(() => { - fetchDatasets(true, versionUrl); - //we should do the redirect to the new dataset - //but for this we need the dataset name in the response of the dataset.import operation :( - navigate(`/projects/${projectPathWithNamespace}/datasets`); - }, [fetchDatasets, navigate, projectPathWithNamespace, versionUrl]); - - const client = props.client; - const submitCallback = React.useCallback( - async (formValues: DatasetInputFormFields) => { - // remember the URI - setUri(formValues.uri); - // clear the information from previous submissions - setServerErrors(undefined); - setSubmitLoader({ - value: true, - text: ImportStateMessage.ENQUEUED, - }); - const handlers = { - setServerErrors, - setSubmitLoader, - }; - try { - await importDatasetAndWaitForResult({ - branch: props.branch, - client, - externalUrl, - formValues, - handlers, - redirectUser, - versionUrl, - }); - } catch (e) { - setServerErrors(ImportStateMessage.FAILED_NO_INFO); - setSubmitLoader({ value: false, text: "" }); - } - }, - [ - externalUrl, - client, - props.branch, - redirectUser, - setServerErrors, - setSubmitLoader, - versionUrl, - ] - ); - - if (submitLoader.value) { - return ( - - ); - } - - return ( - - ); -} - -type DatasetImportProps = { - accessLevel: number; - client: DatasetImportClient; - externalUrl: string; - fetchDatasets: (force: boolean, versionUrl: string) => void; - projectPathWithNamespace: string; - toggleNewDataset: DatasetImportFormProps["toggleNewDataset"]; -}; -function DatasetImport(props: DatasetImportProps) { - const { defaultBranch, externalUrl } = useLegacySelector( - (state) => state.stateModel.project.metadata - ); - const { - coreSupport: { versionUrl }, - } = useCoreSupport({ - gitUrl: externalUrl ?? undefined, - branch: defaultBranch ?? undefined, - }); - - if (props.accessLevel < ACCESS_LEVELS.MAINTAINER) { - return ( - - - You do not have access level necessary to import datasets into this - project. -
      -
      - If you were recently given - access to this project, you might need to{" "} - {" "} - first. -
      - - ); - } - - if (versionUrl == null) { - // I do not think this branch will ever be hit, but just in case... - return ( - - - This project needs to be upgraded before datasets can be created in - the UI. - - - ); - } - - return ( - - ); -} - -export default DatasetImport; -export type { DatasetImportClient, DatasetImportProps }; diff --git a/client/src/project/datasets/import/index.ts b/client/src/project/datasets/import/index.ts deleted file mode 100644 index d00310ee0e..0000000000 --- a/client/src/project/datasets/import/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * Dataset/import - * Components for the import Dataset page - */ - -import DatasetImport from "./DatasetImport"; - -export default DatasetImport; diff --git a/client/src/project/new/LazyNewProject.tsx b/client/src/project/new/LazyNewProject.tsx deleted file mode 100644 index f487187199..0000000000 --- a/client/src/project/new/LazyNewProject.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * Copyright 2024 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Suspense, lazy } from "react"; -import PageLoader from "../../components/PageLoader"; - -const NewProject = lazy(() => - import("./ProjectNew.container").then((module) => ({ - default: module.NewProject, - })) -); - -export default function LazyNewProject() { - return ( - }> - - - ); -} diff --git a/client/src/project/new/ProjectNew.container.jsx b/client/src/project/new/ProjectNew.container.jsx deleted file mode 100644 index 0e4fa53ae4..0000000000 --- a/client/src/project/new/ProjectNew.container.jsx +++ /dev/null @@ -1,693 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * ProjectNew.container.js - * Container components for new project - */ - -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useNavigate } from "react-router"; - -import { useLoginUrl } from "../../authentication/useLoginUrl.hook"; -import { Loader } from "../../components/Loader"; -import { newProjectSchema } from "../../model/RenkuModels"; -import AppContext from "../../utils/context/appContext"; -import { DEFAULT_APP_PARAMS } from "../../utils/context/appParams.constants"; -import useGetNamespaces from "../../utils/customHooks/UseGetNamespaces"; -import useGetUserProjects from "../../utils/customHooks/UseGetProjects"; -import useGetVisibilities from "../../utils/customHooks/UseGetVisibilities"; -import useLegacySelector from "../../utils/customHooks/useLegacySelector.hook"; -import { arrayStringEquals } from "../../utils/helpers/ArrayUtils"; -import { atobUTF8, btoaUTF8 } from "../../utils/helpers/Encoding"; -import { - gitLabUrlFromProfileUrl, - slugFromTitle, -} from "../../utils/helpers/HelperFunctions"; -import { Url, getSearchParams } from "../../utils/helpers/url"; -import { - ForkProject as ForkProjectPresent, - NewProject as NewProjectPresent, -} from "./ProjectNew.present"; -import { - NewProjectCoordinator, - checkTitleDuplicates, - validateTitle, -} from "./ProjectNew.state"; - -const CUSTOM_REPO_NAME = "Custom"; - -function ForkProject(props) { - const { client, forkedId, forkedTitle, projectVisibility, toggleModal } = - props; - const namespaces = useGetNamespaces(true); - const { projectsMember, isFetchingProjects } = useGetUserProjects(); - - const [title, setTitle] = useState(forkedTitle); - const [namespace, setNamespace] = useState(""); - const [fullNamespace, setFullNamespace] = useState(null); - const [visibilities, setVisibilities] = useState(null); - const [visibility, setVisibility] = useState(null); - const [projectsPaths, setProjectsPaths] = useState([]); - const [error, setError] = useState(null); - - const [forking, setForking] = useState(false); - const [forkError, setForkError] = useState(null); - const [forkVisibilityError, setForkVisibilityError] = useState(null); - const [forkUrl, setForkUrl] = useState(null); - - const { availableVisibilities, isFetchingVisibilities } = useGetVisibilities( - fullNamespace, - projectVisibility - ); - const { logged, data: { username } = null } = useLegacySelector( - (state) => state.stateModel.user - ); - - const loginUrl = useLoginUrl(); - - const navigate = useNavigate(); - - useEffect(() => { - if (!logged) { - window.location.assign(loginUrl); - } - }, [logged, loginUrl]); - - // Monitor changes to projects list - useEffect(() => { - if (!projectsMember || !projectsMember.length) setProjectsPaths([]); - else - setProjectsPaths( - projectsMember.map((project) => - project.path_with_namespace.toLowerCase() - ) - ); - }, [projectsMember]); - - // Monitor changes to title, namespace or projects slug list to check for errors - useEffect(() => { - // no errors if I can't check all of the fields -- should be transitory - if (title == null || namespace == null || projectsPaths == null) { - setError(null); - return; - } - const validationError = validateTitle(title); - if (validationError) { - setError(validationError); - return; - } - const duplicateError = checkTitleDuplicates( - title, - namespace, - projectsPaths - ); - if (duplicateError) { - setError( - "Title produces a project identifier (" + - slugFromTitle(title, true) + - ") that is already taken in the selected namespace. Please select a different title or namespace." - ); - return; - } - setError(null); - }, [title, namespace, projectsPaths]); - - // monitor namespace changes to calculate visibility - useEffect(() => { - const getVisibilities = async () => { - // empty values to display fetching - setVisibilities(null); - setVisibility(null); - - // calculate visibilities values - setVisibilities(availableVisibilities ?? null); - setVisibility(availableVisibilities?.default ?? null); - }; - if (fullNamespace && !isFetchingVisibilities) { - getVisibilities(fullNamespace); - } else { - setVisibilities(null); - setVisibility(null); - } - }, [fullNamespace, availableVisibilities, isFetchingVisibilities]); - - // Monitor component mounted state -- helper to prevent illegal action when the fork takes long - const mounted = useRef(false); - useEffect(() => { - // this keeps track of the component status - mounted.current = true; - return () => { - mounted.current = false; - }; - }); - - // fork operations including fork, status check, redirect - const fork = async () => { - // TODO: re-add notifications after #1585 is addressed-- for some reason the project's sub-components - - const path = slugFromTitle(title, true); - // const startingLocation = history.location.pathname; - setForking(true); - setForkError(null); - setForkUrl(null); - try { - const forked = await client.forkProject( - forkedId, - title, - path, - namespace, - props.model.reduxStore - ); - - // handle non-blocking errors from pipelines and hooks - if ( - forked.project.id && - (forked.pipeline.errorData || forked.webhook.errorData) - ) { - // build the final URL -- that requires forked.project(.id) to be available - let newProjectData = { - namespace: forked.project.namespace.full_path, - path: forked.project.path, - }; - setForkUrl(Url.get(Url.pages.project, newProjectData)); - - let verboseError; // = "Project forked, but "; - if (forked.pipeline.errorData) { - verboseError = "Pipeline creation failed: "; - verboseError += - "the forked project is available, but sessions may require building a Docker image."; - throw new Error(verboseError); - } - if (forked.webhook.errorData) { - verboseError = "Indexing error: "; - verboseError = - "the forked project is available, but indexing will need to be activated manually."; - throw new Error(verboseError); - } - - return null; - } - - // wait for operations to finish - let count; - const THRESHOLD = 100, - DELTA = 3000; - for (count = 0; count < THRESHOLD; count++) { - await new Promise((r) => setTimeout(r, DELTA)); // sleep - const forkOperation = await client.getProjectStatus(forked.project.id); - if (forkOperation === "finished") break; - else if (forkOperation === "failed" || forkOperation === "error") - throw new Error("Cloning operation failed"); - else if (count === THRESHOLD - 1) - throw new Error("Cloning is taking too long"); - } - - // set visibility value forked project - let visibilityError; - let visibilityErrorMessage; - await client.setVisibility(forked.project.id, visibility).catch((e) => { - visibilityError = true; - visibilityErrorMessage = e.errorData.message?.visibility_level - ? `, ${e.errorData.message?.visibility_level[0]}` - : `, not supported ${visibility} visibility.`; - setForkVisibilityError(visibilityErrorMessage); - }); - - // Add notification. Mark it as read and redirect automatically only when the modal is still open - let newProjectData = { - namespace: forked.project.namespace.full_path, - path: forked.project.path, - }; - const newUrl = Url.get(Url.pages.project, newProjectData); - newProjectData.name = forked.project.name; - - if (mounted.current && !visibilityError) { - // addForkNotification(notifications, newUrl, newProjectData, startingLocation, true, false); - navigate(newUrl); - } else if (mounted.current && visibilityError) { - setForking(false); // finish forking - setForkUrl(newUrl); // allow display the button to go to the forked project - return; - } else { - // addForkNotification(notifications, newUrl, newProjectData, startingLocation, true, true, - // visibilityError ? { message: visibilityErrorMessage } : false); - } - return null; // this prevents further operations on non-mounted components - } catch (e) { - if (mounted.current) { - setForkError(e.message); - // addForkNotification(notifications, null, null, startingLocation, false, false); - } else { - // addForkNotification(notifications, null, null, startingLocation, false, true); - } - } - if (mounted.current) setForking(false); - }; - - // compatibility layer to re-use the UI components from new project creation - const setPropertyP = (target, value) => { - if (target === "title") { - const localValue = value ? value : ""; - setTitle(localValue); - } - - if (target === "visibility") { - const localValue = value ? value : ""; - setVisibility(localValue); - } - - // ? reset fork error and url when typing - setForkError(null); - setForkUrl(null); - }; - - const setNamespaceP = (value) => { - // it is necessary to save the complete namespace data to obtain the type for visibility purposes - setFullNamespace(value); - setNamespace(value.full_path); - }; - - const adjustedHandlers = { - getNamespaces: namespaces?.refetchNamespaces, - setNamespace: setNamespaceP, - setProperty: setPropertyP, - }; - - return ( - - ); -} - -/** - * Validate and decode query params. - * - * @param {object} params - query params - * @returns {object} parsed parameters, validated and ready to be be used to pre-fill fields. - */ -function getDataFromParams(params) { - // Unrecognized params: should we notify? Let's start without notifications. - if (!params || !Object.keys(params).length || !params.data) return; - const data = JSON.parse(atobUTF8(params.data)); - if (!data || !Object.keys(data).length) return; - // validate metadata - const validKeys = Object.keys(newProjectSchema.createEmpty().automated.data); - const keys = Object.keys(data); - for (let key of keys) { - if (!validKeys.includes(key)) - throw new Error( - "unexpected project field in the encoded metadata: " + key - ); - } - return data; -} - -function NewProjectWrapper(props) { - const { client, model } = useContext(AppContext); - - const user = useLegacySelector((state) => state.stateModel.user); - - const coordinator = useMemo( - () => - new NewProjectCoordinator( - client, - model.subModel("newProject"), - model.subModel("projects") - ), - [client, model] - ); - - if (!client || !model) return ; - return ( - - ); -} - -function NewProject(props) { - const { model, importingDataset, startImportDataset, coordinator } = props; - const { params } = useContext(AppContext); - const navigate = useNavigate(); - const user = useLegacySelector((state) => state.stateModel.user); - const newProject = useLegacySelector((state) => state.stateModel.newProject); - const [namespace, setNamespace] = useState(null); - const [automatedData, setAutomatedData] = useState(null); - const namespaces = useGetNamespaces(true); - const { projectsMember, isFetchingProjects, refetchUserProjects } = - useGetUserProjects(); - const { availableVisibilities, isFetchingVisibilities } = - useGetVisibilities(namespace); - const [metaValidation, setMetaValidation] = useState({ - errors: {}, - warnings: {}, - }); - - const validateForm = useCallback( - (newInput = null, newTemplates = null) => { - const projects = { - members: projectsMember, - fetching: isFetchingProjects, - }; - if (coordinator == null) return; - const result = coordinator.validate( - newInput, - newTemplates, - false, // we manage validation state locally, not in the coordinator - projects, - namespaces, - isFetchingVisibilities - ); - const { errors, warnings } = result; - const mv = { errors: errors.$set, warnings: warnings.$set }; - if ( - arrayStringEquals( - Object.keys(mv.errors), - Object.keys(metaValidation.errors) - ) && - arrayStringEquals( - Object.keys(mv.warnings), - Object.keys(metaValidation.warnings) - ) && - arrayStringEquals( - Object.values(mv.errors), - Object.values(metaValidation.errors) - ) && - arrayStringEquals( - Object.values(mv.warnings), - Object.values(metaValidation.warnings) - ) - ) - return; - setMetaValidation(mv); - }, - [ - coordinator, - isFetchingProjects, - isFetchingVisibilities, - namespaces, - projectsMember, - metaValidation, - ] - ); - - const extractAutomatedData = useCallback(() => { - const searchParams = getSearchParams(); - try { - const data = getDataFromParams(searchParams); - if (data) { - setAutomatedData(data); - if (!importingDataset) { - const newUrl = Url.get(Url.pages.project.new); - navigate(newUrl); - } - } - } catch (e) { - // This usually happens when the link is wrong and the base64 string is broken - coordinator.setAutomated(null, e); - } - }, [coordinator, importingDataset, navigate]); - - const removeAutomated = useCallback( - (manuallyReset = true) => { - coordinator?.resetAutomated(manuallyReset); - }, - [coordinator] - ); - - /* - * Start fetching templates and get automatedData. We can execute that only once - */ - useEffect(() => { - removeAutomated(); - if (!coordinator || !user.logged) return; - const templates = params?.TEMPLATES ?? DEFAULT_APP_PARAMS.TEMPLATES; - coordinator.setConfig(templates.custom, templates.repositories); - coordinator.resetInput(); - coordinator.getTemplates(); - extractAutomatedData(); - }, [coordinator, extractAutomatedData, user, params, removeAutomated]); - - /* - * Start Auto fill form when namespaces are ready - */ - useEffect(() => { - if ( - !automatedData || - newProject.automated.finished || - !namespaces.fetched || - availableVisibilities == null - ) - return; - coordinator?.setAutomated( - automatedData, - undefined, - namespaces, - availableVisibilities, - setNamespace - ); - }, [ - automatedData, - namespaces, - availableVisibilities, - coordinator, - newProject.automated.finished, - ]); - - /* - * Validate form when projects/namespace are ready or the auto fill form finished - */ - useEffect(() => { - if (!user.logged) return; - if ( - !namespaces.fetched || - (newProject.automated.received && !newProject.automated.finished) - ) - return; - validateForm(null, null); - }, [ - user.logged, - validateForm, - namespaces.fetched, - newProject.automated.received, - newProject.automated.finished, - ]); - - const setProperty = useCallback( - (property, value) => { - coordinator?.setProperty(property, value); - let updateObj = { input: { [property]: value } }; - validateForm(updateObj, null); - }, - [coordinator, validateForm] - ); - - /* - * Calculate visibilities when namespace change - */ - useEffect(() => { - if (!namespace || !user.logged) return; - - if (!availableVisibilities || isFetchingVisibilities) { - coordinator?.resetVisibility(namespace); - return; - } - coordinator?.setVisibilities(availableVisibilities, namespace); - if (newProject.input.namespace !== namespace.full_path) { - setProperty("namespace", namespace.full_path); - setProperty("visibility", availableVisibilities.default); - } - if ( - !availableVisibilities.visibilities.includes(newProject.input.visibility) - ) { - setProperty("visibility", availableVisibilities.default); - } - }, [ - availableVisibilities, - coordinator, - isFetchingVisibilities, - namespace, - newProject.input.namespace, - newProject.input.visibility, - setProperty, - user.logged, - ]); - - const createEncodedUrl = (data) => { - if (!data || !Object.keys(data).length) - return Url.get(Url.pages.project.new, {}, true); - const encodedContent = btoaUTF8(JSON.stringify(data)); - return Url.get(Url.pages.project.new, { data: encodedContent }, true); - }; - - const getNamespaces = () => { - namespaces?.refetchNamespaces(); - }; - - const getTemplates = async () => { - return await coordinator?.getTemplates(null, false); - }; - - const getUserTemplates = () => { - const targetRepository = model.get("newProject.meta.userTemplates"); - const repositories = [ - { - name: CUSTOM_REPO_NAME, - url: targetRepository.url, - ref: targetRepository.ref, - }, - ]; - return coordinator.getTemplates(repositories, true); - }; - - const refreshUserProjects = () => { - refetchUserProjects(); - }; - - const setNamespaceProperty = (namespace) => { - setNamespace(namespace); - setProperty("namespace", namespace.full_path); - }; - - const setTemplateProperty = (property, value) => { - coordinator?.setTemplateProperty(property, value); - }; - - const setVariable = (variable, value) => { - coordinator?.setVariable(variable, value); - }; - - const resetCreationResult = () => { - coordinator?.resetCreationResult(); - }; - - const goToProject = () => { - const slug = coordinator?.getSlugAndReset(); - navigate(`/projects/${slug}`); - }; - - const sendProjectToAddDataset = (projectPath) => { - if (projectPath) startImportDataset(projectPath); - }; - - const onSubmit = (e) => { - e.preventDefault(); - - // validate -- we do this cause we don't show errors on pristine variables - if (coordinator?.invalidatePristine()) return; - if ( - Object.keys(metaValidation.errors).length || - Object.keys(metaValidation.warnings).length - ) - return; - - // submit - const gitlabUrl = gitLabUrlFromProfileUrl(user.data.web_url); - coordinator?.postProject(gitlabUrl).then((result) => { - const { creation } = result.meta; - if (creation.created) { - refreshUserProjects(); - if (!creation.kgError && !creation.projectError) { - const slug = `${creation.newNamespace}/${creation.newNameSlug}`; - if (importingDataset) sendProjectToAddDataset(slug); - else navigate(`/projects/${slug}`); - resetCreationResult(); - } - } - }); - }; - - const onAvatarChange = (avatarFile) => { - setProperty("avatar", avatarFile); - }; - - // create handlers - const handlers = { - createEncodedUrl, - getNamespaces, - getTemplates, - getUserTemplates, - goToProject, - onAvatarChange, - onSubmit, - removeAutomated, - resetCreationResult, - setNamespace: setNamespaceProperty, - setProperty, - setTemplateProperty, - setVariable, - }; - const newProjectMeta = { - ...newProject.meta, - ...{ validation: metaValidation }, - }; - const mergedNewProject = { - ...newProject, - ...{ meta: newProjectMeta }, - }; - const newProps = { - ...mergedNewProject, - handlers, - importingDataset, - isFetchingProjects, - namespaces, - user: { - logged: user.logged, - username: user.data && user.data.username ? user.data.username : null, - }, - }; - - if (!coordinator) return ; - - return ; -} - -export { CUSTOM_REPO_NAME, ForkProject, NewProjectWrapper as NewProject }; -// test only -export { getDataFromParams }; diff --git a/client/src/project/new/ProjectNew.present.jsx b/client/src/project/new/ProjectNew.present.jsx deleted file mode 100644 index efa87d5feb..0000000000 --- a/client/src/project/new/ProjectNew.present.jsx +++ /dev/null @@ -1,520 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * ProjectNew.present.js - * New project presentational components. - */ - -import { Component, Fragment } from "react"; -import { Link } from "react-router"; -import { - Button, - Form, - FormText, - ModalBody, - ModalFooter, - ModalHeader, -} from "reactstrap"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; - -import { Loader } from "../../components/Loader"; -import { ErrorAlert, WarnAlert } from "../../components/Alert"; -import LoginAlert from "../../components/loginAlert/LoginAlert"; -import FormSchema from "../../components/formschema/FormSchema"; -import ProgressIndicator, { - ProgressStyle, - ProgressType, -} from "../../components/progress/Progress"; -import SunsetBanner from "../../features/projectsV2/shared/SunsetV1Banner"; -import Automated from "./components/Automated"; -import Title from "./components/Title"; -import Description from "./components/Description"; -import Namespaces from "./components/Namespaces"; -import ProjectIdentifier from "./components/ProjectIdentifier"; -import Visibility from "./components/Visibility"; -import TemplateSource from "./components/TemplateSource"; -import UserTemplate, { ErrorTemplateFeedback } from "./components/UserTemplate"; -import { Template } from "./components/Template"; -import TemplateVariables from "./components/TemplateVariables"; -import { FormErrors, FormWarnings } from "./components/FormValidations"; -import SubmitFormButton from "./components/SubmitFormButton"; -import AppContext from "../../utils/context/appContext"; -import NewProjectAvatar from "./components/NewProjectAvatar"; - -import "./Project.style.css"; - -function ForkProject(props) { - const { - error, - fork, - forkedTitle, - forking, - forkUrl, - namespaces, - isFetchingProjects, - toggleModal, - } = props; - - const fetching = { - projects: isFetchingProjects, - namespaces: namespaces.fetching, - }; - - return ( - - - - - - ); -} - -function ForkProjectHeader(props) { - const { forkedTitle, toggleModal } = props; - return ( - - Fork project {forkedTitle} - - ); -} - -function ForkProjectBody(props) { - const { fetching, forkError, forkVisibilityError, forking } = props; - if (fetching.namespaces || fetching.projects) { - const text = fetching.namespaces ? "namespaces" : "existing projects"; - return ( - -

      Checking your {text}...

      - -
      - ); - } - return ( - - - - - ); -} - -function ForkProjectFooter(props) { - const { error, fetching, fork, forking, forkUrl, toggleModal } = props; - - let forkButton; - if (forking) { - forkButton = null; - } else { - if (forkUrl) { - forkButton = ( - - Go to forked project - - ); - } else { - forkButton = ( - - ); - } - } - - let closeButton = null; - if (toggleModal) { - closeButton = ( - - ); - } - - if (fetching.namespaces || fetching.projects) return null; - return ( - - {closeButton} - {forkButton} - - ); -} - -function ForkProjectStatus(props) { - if (props.forking) { - return ( - - ); - } else if (props.forkError) { - return ( - - {props.forkError} - - ); - } else if (props.forkVisibilityError) { - return ( -

      - The project has been - forked but an error occurred when setting the visibility - {props.forkVisibilityError} -

      - ); - } - return null; -} - -function ForkProjectContent(props) { - const { - fetching, - error, - forking, - handlers, - namespace, - namespaces, - title, - user, - visibility, - visibilities, - forkVisibilityError, - } = props; - - if (forking || forkVisibilityError) return null; - - const input = { - namespace, - title, - titlePristine: false, - visibility, - visibilityPristine: false, - }; - const meta = { - validation: { errors: { title: error } }, - namespace: { - fetching: fetching.namespaces, - visibilities: visibilities?.visibilities, - visibility: visibilities?.default, - }, - }; - - return ( -
      - - <Namespaces - handlers={handlers} - input={input} - namespaces={namespaces} - user={user} - /> - <ProjectIdentifier input={input} isRequired={true} /> - <Visibility handlers={handlers} input={input} meta={meta} /> - </div> - ); -} - -const isFormProcessingOrFinished = (meta) => { - // posting - if ( - meta.creation.creating || - meta.creation.projectUpdating || - meta.creation.kgUpdating - ) - return true; - // posted successfully with visibility or KG warning - if (meta.creation.created) return true; - return meta.creation.projectError || meta.creation.kgError; -}; - -const NewProjectForm = ({ - automated, - config, - handlers, - input, - isFetchingProjects, - meta, - namespaces, - namespace, - user, - importingDataset, - userRepo, - templates, -}) => { - const errorTemplateAlert = ( - <ErrorTemplateFeedback templates={templates} meta={meta} input={input} /> - ); - // We should incorporate templates in deciding this, but the content of templates - // seems to be unreliable, so skip it for the moment - // const createDataAvailable = !isFetchingProjects && namespaces.fetched && - // (templates.fetched || (templates.errors && templates.errors.length > 0 && !templates.fetching)); - const createDataAvailable = !isFetchingProjects && namespaces.fetched; - return ( - <Form data-cy="create-project-form" className="form-rk-green mb-4"> - <Automated - automated={automated} - removeAutomated={handlers.removeAutomated} - /> - <Title handlers={handlers} meta={meta} input={input} /> - <Namespaces - namespaces={namespaces} - handlers={handlers} - automated={automated} - input={input} - namespace={namespace} - user={user} - /> - <ProjectIdentifier input={input} isRequired={true} /> - <Description handlers={handlers} meta={meta} input={input} /> - <Visibility handlers={handlers} meta={meta} input={input} /> - <NewProjectAvatar onAvatarChange={handlers.onAvatarChange} /> - {config.custom ? ( - <TemplateSource handlers={handlers} input={input} isRequired={true} /> - ) : null} - {userRepo ? ( - <UserTemplate - meta={meta} - handlers={handlers} - config={config} - templates={templates} - input={input} - /> - ) : null} - <Template - config={config} - handlers={handlers} - input={input} - templates={templates} - meta={meta} - /> - <TemplateVariables - handlers={handlers} - input={input} - templates={templates} - meta={meta} - /> - {errorTemplateAlert} - <SubmitFormButton - createDataAvailable={createDataAvailable} - handlers={handlers} - input={input} - importingDataset={importingDataset} - meta={meta} - namespaces={namespaces} - templates={templates} - /> - <FormWarnings meta={meta} /> - <FormErrors meta={meta} input={input} /> - </Form> - ); -}; - -class NewProject extends Component { - static contextType = AppContext; - - render() { - const { - automated, - config, - handlers, - input, - user, - importingDataset, - meta, - namespace, - namespaces, - templates, - } = this.props; - const { isFetchingProjects } = this.props; - - if (!user.logged) { - const textIntro = "Only authenticated users can create new projects."; - const textPost = "to create a new project."; - return ( - <LoginAlert - logged={user.logged} - textIntro={textIntro} - textPost={textPost} - /> - ); - } - - const title = "New Project"; - const desc = - "Create a project to house your files, include datasets, " + - "plan your work, and collaborate on code, among other things."; - const userRepo = config.custom && input.userRepo; - const formOnProcess = - meta.creation.creating || - meta.creation.projectUpdating || - meta.creation.kgUpdating; - const form = ( - <NewProjectForm - automated={automated} - config={config} - handlers={handlers} - input={input} - importingDataset={importingDataset} - isFetchingProjects={isFetchingProjects} - meta={meta} - namespaces={namespaces} - namespace={namespace} - user={user} - userRepo={userRepo} - templates={templates} - /> - ); - - const onProgress = isFormProcessingOrFinished(meta); - const creation = ( - <Creation - handlers={handlers} - meta={meta} - importingDataset={importingDataset} - /> - ); - if (onProgress) return creation; - - return !importingDataset ? ( - <FormSchema showHeader={!formOnProcess} title={title} description={desc}> - {creation} - <SunsetBanner /> - {form} - </FormSchema> - ) : ( - form - ); - } -} - -class Creation extends Component { - render() { - const { handlers, importingDataset } = this.props; - const { creation } = this.props.meta; - if ( - !creation.creating && - !creation.createError && - !creation.projectUpdating && - !creation.projectError && - !creation.kgUpdating && - !creation.kgError && - !creation.newName - ) - return null; - - let color = "primary"; - let message = ""; - if (creation.creating) { - message = "Initializing project..."; - } else if (creation.createError) { - color = "danger"; - let error; - if (typeof creation.createError === "string") - error = creation.createError; - else if (creation.createError?.code) - error = creation.createError.userMessage - ? creation.createError.userMessage - : creation.createError.reason; - else error = creation.createError.toString(); - message = ( - <div> - <p>An error occurred while creating the project.</p> - <p>{error}</p> - </div> - ); - } else if (creation.projectUpdating) { - message = "Updating project metadata..."; - } else if (creation.projectError) { - color = "warning"; - message = ( - <div> - <p> - An error occurred while updating project metadata (name or - visibility). Please, adjust it on GitLab if needed. - </p> - <p>Error details: {creation.projectError}</p> - <Button color="primary" onClick={() => handlers.goToProject()}> - Go to the project - </Button> - </div> - ); - } else if (creation.kgUpdating) { - message = "Activating project indexing..."; - } else if (creation.kgError) { - color = "warning"; - message = ( - <div> - <p> - An error occurred while activating the project indexing. You can - activate it later to get the lineage. - </p> - <p>Error details: {creation.kgError}</p> - <Button color="primary" onClick={() => handlers.goToProject()}> - Go to the project - </Button> - </div> - ); - } else { - return null; - } - - if (color === "warning") return <WarnAlert>{message}</WarnAlert>; - - if (color === "danger") return <ErrorAlert>{message}</ErrorAlert>; - - // customize the progress indicator when importing a dataset - const title = importingDataset - ? "Creating a project to import the dataset..." - : "Creating Project..."; - const feedback = importingDataset - ? "Once the process is completed, you will be redirected to the page " + - "of the imported dataset in the created project." - : "You'll be redirected to the new project page when the creation is completed."; - - return ( - <div> - <ProgressIndicator - type={ProgressType.Indeterminate} - style={ProgressStyle.Dark} - title={title} - description="We've received your project information. This may take a while." - currentStatus={message} - feedback={feedback} - /> - </div> - ); - } -} - -export { NewProject, ForkProject }; diff --git a/client/src/project/new/ProjectNew.state.js b/client/src/project/new/ProjectNew.state.js deleted file mode 100644 index cd1c63b8c7..0000000000 --- a/client/src/project/new/ProjectNew.state.js +++ /dev/null @@ -1,876 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * ProjectNew.state.js - * New project controller code. - */ - -import { projectKgApi } from "../../features/project/projectKg.api"; -import { newProjectSchema } from "../../model/RenkuModels"; -import { sleep, slugFromTitle } from "../../utils/helpers/HelperFunctions"; -import { verifyTitleCharacters } from "../../utils/helpers/verifyTitleCharacters.utils"; -import { CUSTOM_REPO_NAME } from "./ProjectNew.container"; - -// ? reference https://docs.gitlab.com/ce/user/reserved_names.html#reserved-project-names -const RESERVED_TITLE_NAMES = [ - "badges", - "blame", - "blob", - "builds", - "commits", - "create", - "create_dir", - "edit", - "sessions/folders", - "files", - "find_file", - "gitlab-lfs/objects", - "info/lfs/objects", - "new", - "preview", - "raw", - "refs", - "tree", - "update", - "wikis", -]; - -/** - * Verify whether the title is valid. - * - * @param {string} title - title to validate. - * @returns {string} error description or null if the string is valid. - */ -function validateTitle(title) { - if (!title || !title.length) return "Title is missing."; - else if (RESERVED_TITLE_NAMES.includes(title)) return "Reserved title name."; - else if (title.length && ["_", "-", " ", "."].includes(title[0])) - return "Title must start with a letter or a number."; - else if (!verifyTitleCharacters(title)) - return "Title can contain only letters, digits, '_', '.', '-' or spaces."; - else if (title && !slugFromTitle(title, true)) - return "Title must contain at least one letter (without any accents) or a number."; - return null; -} - -/** - * Verify whether the title and namespace will produce a duplicate. - * - * @param {string} title - current title. - * @param {string} namespace - current namespace. - * @param {string[]} projectsPaths - list of current own projects paths. - * @returns {boolean} whether the title would create a duplicate or not. - */ -function checkTitleDuplicates(title, namespace, projectsPaths) { - if (!title || !namespace || !projectsPaths || !projectsPaths.length) - return false; - - const expectedTitle = slugFromTitle(title, true); - const expectedSlug = `${namespace}/${expectedTitle}`; - if (projectsPaths.includes(expectedSlug)) return true; - - return false; -} - -class NewProjectCoordinator { - constructor(client, model, projectsModel) { - this.client = client; - this.model = model; - this.projectsModel = projectsModel; - - // Cannot store avatarFile in the model - this.avatarFile = null; - } - - _setTemplateVariables(currentInput, value) { - const templates = currentInput.userRepo - ? this.model.get("meta.userTemplates") - : this.model.get("templates"); - const template = templates.all.filter((t) => t.id === value)[0]; - const variables = - template && template.variables ? Object.keys(template.variables) : []; - - // preserve already set values or set default values when available - const oldValues = currentInput.template ? currentInput.variables : {}; - const oldVariables = Object.keys(oldValues); - const values = variables.reduce((values, variable) => { - let value = ""; - - const variableData = template.variables[variable]; - if (typeof variableData === "object") { - // set first value for enum, and "false" for boolean - if ( - variableData["type"] === "enum" && - variableData["enum"] && - variableData["enum"].length - ) - value = variableData["enum"][0]; - - // set default, if any - if ( - typeof variableData === "object" && - variableData["default_value"] != null - ) - value = variableData["default_value"]; - } - - // set older value, if any - if (oldVariables.includes(variable)) value = oldValues[variable]; - - return { ...values, [variable]: value }; - }, {}); - - return values; - } - - resetAutomated(manuallyReset = false) { - if (this.model.get("automated.received")) { - const pristineAutomated = newProjectSchema.createInitialized().automated; - this.model.setObject({ - automated: { ...pristineAutomated, manuallyReset }, - }); - } - } - - /** - * Set newProject.automated object with new content -- either the provided data or an error - * @param {object} data - data to pre-fill - * @param {object} [error] - error generated while parsing the data - * @param {object} [namespaces] - up-to-date namespaces object - * @param {object} [availableVisibilities] - up-to-date visibilities object - * @param {object} [setNamespace] - function to set namespace, it is necessary to calculate visibilities - */ - setAutomated(data, error, namespaces, availableVisibilities, setNamespace) { - // ? This is a safeguard in case it's accidentally invoked multiple times. - const currentStatus = this.model.get("automated"); - if (currentStatus.received && currentStatus.valid && currentStatus.step > 0) - return; - let automated = newProjectSchema.createInitialized().automated; - if (error) { - automated = { - ...automated, - received: true, - valid: false, - finished: true, - error: error.message ? error.message : error, - }; - this.model.set("automated", automated); - } else { - automated = { - ...automated, - received: true, - valid: true, - data: { ...automated.data, ...data }, - }; - // ? passing the content is more efficient than invoking the `model.set` and then the function. - this.autoFill(automated, namespaces, availableVisibilities, setNamespace); - } - } - - /** - * Get all the auto-fill content and fill in the provided data. - * @param {object} [automatedObject] - pass the up-to-date `automated` object to optimize performance. - * @param {object} [namespaces] - up-to-date namespaces - * @param {object} [availableVisibilities] - available visibilities - * @param {function} [setNamespace] - function to set a new namespace, it is necessary to calculate the visibilities - */ - async autoFill( - automatedObject, - namespaces, - availableVisibilities, - setNamespace - ) { - let automated = automatedObject - ? automatedObject - : newProjectSchema.createInitialized().automated; - let availableVariables = []; - let visibilities = availableVisibilities?.visibilities; - const { data } = automated; - let getDataAttempts = 0; - let maxAttempts = 60; - - // Step 1: wait for templates to be fetched - automated.step = 1; - this.model.set("automated", { ...automated }); - // ? since this is triggered elsewhere, we need to use ugly timeouts here - const intervalLength = 1; - let templates = false; - do { - if (this.model.get("automated.manuallyReset")) return; - // check templates availability - if (!templates) { - getDataAttempts++; - const templatesStatus = this.model.get("templates"); - if (templatesStatus.fetched && !templatesStatus.fetching) - templates = templatesStatus.all; - } - await sleep(intervalLength); - } while (!templates && getDataAttempts <= maxAttempts); - if (!templates) { - automated.error = `Fetching templates takes too long.`; - automated.finished = true; - this.model.set("automated", { ...automated }); - return; - } - - // Step 2: Set title, namespace, template (if no url/ref). Start fetching visibilities - if (this.model.get("automated.manuallyReset")) return; - automated.step = 2; - this.model.set("automated.step", 2); - let newInput = {}; - if (data.title) { - this.setProperty("title", data.title); - newInput.title = data.title; - } - if (data.description) { - this.setProperty("description", data.description); - newInput.description = data.description; - } - if (data.namespace) { - // Check if the namespace is available - const namespaceAvailable = namespaces?.list?.find( - (namespace) => namespace.full_path === data.namespace - ); - if (!namespaceAvailable) { - automated.warnings = [ - ...automated.warnings, - `The namespace "${data.namespace}" is not available.`, - ]; - } else { - this.setProperty("namespace", data.namespace); // full path - newInput.namespace = data.namespace; - setNamespace(namespaceAvailable); // this will trigger to set visibilities according to namespace - } - } - if (data.template && !data.url) { - // Check if the template is available - const templates = this.model.get("templates.all"); - const templateAvailable = templates.find( - (template) => template.id === data.template - ); - if (!templateAvailable) { - automated.error = `The template "${data.template}" is not available.`; - automated.finished = true; - this.model.set("automated", { ...automated }); - return; - } - this.setProperty("template", data.template); - newInput.template = data.template; - availableVariables = templateAvailable.variables; - } - - // Step 3: Set visibility and fetch custom template (requires url/ref). - if (this.model.get("automated.manuallyReset")) return; - automated.step = 3; - this.model.set("automated.step", 3); - if (data.ref && data.url) { - const ref = data.ref ? data.ref : "master"; - this.setProperty("userRepo", true); - this.setTemplateProperty("url", data.url); - this.setTemplateProperty("ref", ref); - const repositories = [ - { name: CUSTOM_REPO_NAME, url: data.url, ref: ref }, - ]; - const templates = await this.getTemplates(repositories, true); - if (this.model.get("automated.manuallyReset")) return; - if (!templates || !templates.length) { - automated.error = - "Something went wrong while fetching the template repositories."; - automated.error += "\nSee below for further details."; - automated.finished = true; - this.model.set("automated", { ...automated }); - return; - } - if (data.template) { - const templateAvailable = templates.find( - (template) => template.id === data.template - ); - if (!templateAvailable) { - automated.error = `The template "${data.template}" is not available.`; - automated.finished = true; - this.model.set("automated", { ...automated }); - return; - } - this.setProperty("template", data.template); - newInput.template = data.template; - availableVariables = templateAvailable.variables; - } - } - - if (data.visibility) { - if (!visibilities) { - // wait for namespace visibilities to be available - let namespace = null; - getDataAttempts = 0; - do { - // check namespaces availability - if (this.model.get("automated.manuallyReset")) return; - if (!visibilities) { - namespace = this.model.get("meta.namespace"); - if (namespace.fetched && !namespace.fetching) - visibilities = namespace.visibilities; - } - getDataAttempts++; - await sleep(intervalLength); - } while (!visibilities && getDataAttempts < maxAttempts); - } - if (!visibilities) { - automated.error = `Fetching visibilities takes too long.`; - automated.finished = true; - this.model.set("automated", { ...automated }); - return; - } - if (visibilities?.includes(data.visibility)) { - this.setProperty("visibility", data.visibility); - } else { - automated.warnings = [ - ...automated.warnings, - `The visibility "${data.namespace}" is not available in the target namespace.`, - ]; - } - } - - // Step 4: Set variables. - if (this.model.get("automated.manuallyReset")) return; - automated.step = 4; - this.model.set("automated.step", 4); - if (data.template && data.variables && Object.keys(data.variables)) { - if (availableVariables && Object.keys(availableVariables)) { - // Try to set variables after checking for availability on the target template. - const templateVariables = Object.keys(availableVariables); - let unavailableVariables = []; - for (let variable of Object.keys(data.variables)) { - if (templateVariables.includes(variable)) - this.setVariable(variable, data.variables[variable]); - else unavailableVariables = [...unavailableVariables, variable]; - } - if (unavailableVariables.length) { - automated.warnings = [ - ...automated.warnings, - `Some variables are not available on this template: ${unavailableVariables.join( - ", " - )}.`, - ]; - } - } else { - automated.warnings = [ - ...automated.warnings, - "No variables available for the target template.", - ]; - } - } - automated.finished = true; - this.model.set("automated", { ...automated }); - } - - resetInput() { - const pristineInput = newProjectSchema.createInitialized().input; - this.avatarFile = null; - this.model.setObject({ input: pristineInput }); - } - - setProperty(property, value) { - const currentInput = this.model.get("input"); - // if the property is tha avatar, handle it a little differently - if (property === "avatar") { - this.avatarFile = value; - // We do not need to track this change in the model - const updateObj = { meta: { avatar: value?.name } }; - return updateObj; - } - // check if the value needs to be updated - if (currentInput[property] === value) return; - let updateObj = { input: { [property]: value } }; - if (currentInput[`${property}Pristine`]) - updateObj.input[`${property}Pristine`] = false; - - // reset knowledgeGraph when needed - if (property === "visibility") updateObj.input.knowledgeGraph = true; - - // unset template when changing source - if (property === "userRepo") updateObj.input.template = ""; - - // pre-set variables and reset when needed - if (property === "template") - updateObj.input.variables = { - $set: this._setTemplateVariables(currentInput, value), - }; - - this.model.setObject(updateObj); - return updateObj; - } - - getSlugAndReset() { - const creation = this.model.get("meta.creation"); - this.resetCreationResult(); - return `${creation.newNamespace}/${creation.newNameSlug}`; - } - - resetCreationResult() { - const pristineCreation = newProjectSchema.createInitialized().meta.creation; - this.model.setObject({ meta: { creation: pristineCreation } }); - } - - setVariable(variable, value) { - this.model.set(`input.variables.${variable}`, value); - } - - setTemplateProperty(property, value) { - const currentInput = this.model.get("meta.userTemplates"); - - // check if the value needs to be updated - if (currentInput[property] === value) return; - this.model.set(`meta.userTemplates.${property}`, value); - let updateObj = { - meta: { userTemplates: { [property]: value, fetched: false } }, - }; - this.model.setObject(updateObj); - } - - setConfig(custom, repositories) { - let updateObject = { - config: { - custom, - repositories: { $set: repositories }, - }, - }; - - // set the user repo to false if not allowed to change it - if (!custom) updateObject.input = { userRepo: false }; - - this.model.setObject(updateObject); - } - - /** - * Put the template data in the proper object in the store and validate them. - * - * @param {object} templateData - template data - * @param {boolean} [userRepo] - whether or not it's a user custom repo - */ - createTemplatesObject(templateData, userRepo = false) { - const validation = this.validate(null, templateData); - const updateObject = userRepo - ? { meta: { userTemplates: templateData, validation } } - : { templates: templateData, meta: { validation } }; - return updateObject; - } - - /** - * Fetch all the templates listed in the sources - * - * @param {object[]} [sources] - List of sources in the format { url, ref, name }. If not provided, - * the list in model.config.repositories will be used instead. - * @param {boolean} [userRepo] - whether or not it's a user custom repo - */ - async getTemplates(sources = null, userRepo = false) { - // use deployment repositories if nothing else is provided - if (!sources || !sources.length) - sources = this.model.get("config.repositories"); - - // verify sources and set fetching status - if (!sources || !sources.length) { - const errorText = - "No project templates are available in this RenkuLab deployment. Please notify a RenkuLab " + - "administrator and ask them to configure a project template repository."; - const templatesObject = { - fetched: false, - fetching: false, - all: { $set: [] }, - errors: { $set: [{ global: errorText }] }, - }; - this.model.setObject( - this.createTemplatesObject(templatesObject, userRepo) - ); - throw errorText; - } - - this.model.setObject( - this.createTemplatesObject({ fetching: true }, userRepo) - ); - - // fetch manifest and collect templates and errors - let errors = [], - templates = []; - for (const source of sources) { - const answer = await this.getTemplate(source.url, source.ref); - if (Array.isArray(answer)) { - for (const template of answer) { - templates.push({ - parentRepo: source.name, - parentTemplate: template.folder, - id: `${source.name}/${template.folder}`, - name: template.name, - description: template.description, - variables: template.variables, - icon: template.icon, - isSshSupported: !!template.ssh_supported, - }); - } - } else { - errors.push({ [source.name]: answer }); - } - } - - const templatesObject = { - fetched: new Date(), - fetching: false, - errors: { $set: errors }, - all: { $set: templates }, - }; - const valObj = this.createTemplatesObject(templatesObject, userRepo); - this.model.setObject(valObj); - return templates; - } - - /** - * Fetch single template and return manifest array or error message. - * - * @param {string} url - Target template repository url - * @param {string} ref - Target ref (tag, commit or branch) - */ - async getTemplate(url, ref) { - const resp = await this.client.getTemplatesManifest(url, ref); - if (resp.error) return resp.error; - else if (resp.result.templates && resp.result.templates.length) - return resp.result.templates; - return "No templates available in this repo."; - } - - resetVisibility(namespace) { - this.model.setObject({ - meta: { - namespace: { - fetched: null, - fetching: true, - id: namespace.full_path, - visibility: null, - visibilities: null, - }, - }, - }); - } - - async setVisibilities(availableVisibilities, namespace) { - let updateObject = { - meta: { - namespace: { - visibility: availableVisibilities.default, - visibilities: availableVisibilities.visibilities, - fetching: false, - id: namespace.full_path, - }, - }, - }; - - // save the model - this.model.setObject(updateObject); - return availableVisibilities; - } - - /** - * Post a project to the target repository, manage visibility and KG. - * - * @param {string} repositoryUrl - target url repository. - */ - async postProject(repositoryUrl) { - const input = this.model.get("input"); - - // set backend project details - let newProjectData = { - project_repository: repositoryUrl, - project_namespace: input.namespace, - project_name: input.title, - project_description: input.description, - }; - - // add template details - if (!input.userRepo) { - const templates = this.model.get("templates.all"); - const referenceTemplate = templates.filter( - (t) => t.id === input.template - )[0]; - newProjectData.identifier = referenceTemplate.parentTemplate; - const repositories = this.model.get("config.repositories"); - const referenceRepository = repositories.filter( - (r) => r.name === referenceTemplate.parentRepo - )[0]; - newProjectData.url = referenceRepository.url; - newProjectData.ref = referenceRepository.ref; - } else { - const userTemplates = this.model.get("meta.userTemplates"); - newProjectData.identifier = input.template.replace( - CUSTOM_REPO_NAME + "/", - "" - ); - newProjectData.url = userTemplates.url; - newProjectData.ref = userTemplates.ref; - } - - // add variables after converting to string (renku core accept string only) - let parameters = []; - for (let variable of Object.keys(input.variables)) - parameters.push({ - key: variable, - value: input.variables[variable].toString(), - }); - newProjectData.parameters = parameters; - - // reset all previous creation progresses and invoke the project creation API - const pristineCreation = newProjectSchema.createInitialized().meta.creation; - let modelUpdates = { meta: { creation: pristineCreation } }; - modelUpdates.meta.creation.creating = true; - this.model.setObject(modelUpdates); - const projectResult = await this.client.postNewProject(newProjectData); - modelUpdates.meta.creation.creating = false; - if (projectResult.error) { - modelUpdates.meta.creation.created = false; - modelUpdates.meta.creation.createError = projectResult.error; - this.model.setObject(modelUpdates); - return modelUpdates; - } - modelUpdates.meta.creation.created = true; - modelUpdates.meta.creation.newName = projectResult.result.name; - modelUpdates.meta.creation.newNameSlug = projectResult.result.slug; - modelUpdates.meta.creation.newNamespace = projectResult.result.namespace; - modelUpdates.meta.creation.newUrl = projectResult.result.url; - const slug = `${projectResult.result.namespace}/${projectResult.result.slug}`; - - // update project details like visibility and name - // currently postNewProject returns the project name as the original project name even when it is created with "-" - // the way to validate if the project name needs to be updated is to check against the slug - modelUpdates.meta.creation.projectError = ""; - if ( - input.visibility !== "private" || - projectResult.result.slug !== input.title - ) { - modelUpdates.meta.creation.projectUpdating = true; - this.model.setObject(modelUpdates); - let projectObject = {}; - if (input.visibility !== "private") - projectObject["visibility"] = input.visibility; - if (projectResult.result.slug !== input.title) - projectObject["name"] = input.title; - try { - await this.client.putProjectField( - encodeURIComponent(slug), - projectObject - ); - modelUpdates.meta.creation.projectUpdated = true; - } catch (error) { - modelUpdates.meta.creation.projectError = error.message - ? error.message - : error; - } - modelUpdates.meta.creation.projectUpdating = false; - this.model.setObject(modelUpdates); - } else { - modelUpdates.meta.creation.projectUpdated = true; - } - // Get the project -- we need it do so some other operations - let newProjectResult; - try { - newProjectResult = await this.client.getProject(slug); - } catch (error) { - modelUpdates.meta.creation.projectError = error.message - ? error.message - : error; - } - - if (newProjectResult != null) { - // upload the project avatar if there is one - if (this.avatarFile != null) { - modelUpdates.meta.creation.projectUpdating = true; - this.model.setObject(modelUpdates); - try { - await this.client.setAvatar( - newProjectResult.data.all.id, - this.avatarFile - ); - modelUpdates.meta.creation.projectUpdated = true; - } catch (error) { - modelUpdates.meta.creation.projectError = error.message - ? error.message - : error; - } - modelUpdates.meta.creation.projectUpdating = false; - this.model.setObject(modelUpdates); - } - - // activate knowledge graph - modelUpdates.meta.creation.kgError = ""; - if (input.knowledgeGraph) { - modelUpdates.meta.creation.kgUpdating = true; - this.model.setObject(modelUpdates); - - // get project id for the KG query - try { - const resp = await this.model.reduxStore.dispatch( - projectKgApi.endpoints.activateIndexing.initiate( - newProjectResult.data.all.id - ) - ); - const succeeded = (resp?.data?.message ?? "").includes("created") - ? true - : false; - if (succeeded) modelUpdates.meta.creation.kgUpdated = true; - else - modelUpdates.meta.creation.kgError = - "Activating project indexing failed on server side."; - } catch (error) { - modelUpdates.meta.creation.kgError = error.message - ? error.message - : "Unknown error."; - } - modelUpdates.meta.creation.kgUpdating = false; - } else { - modelUpdates.meta.creation.kgUpdated = true; - } - } - - // reset all the input/errors if creation was successful - const { creation } = modelUpdates.meta; - if (!creation.createError && !creation.kgError && !creation.projectError) { - const pristineModel = newProjectSchema.createInitialized(); - modelUpdates.input = pristineModel.input; - modelUpdates.automated = pristineModel.automated; - modelUpdates.meta.validation = pristineModel.meta.validation; - } - - this.model.setObject(modelUpdates); - return modelUpdates; - } - - invalidatePristine() { - const input = this.model.get("input"); - const pristineProps = Object.keys(input).filter( - (prop) => prop.endsWith("Pristine") && input[prop] - ); - if (pristineProps.length) { - const inputObj = pristineProps.reduce( - (obj, prop) => ({ ...obj, [prop]: false }), - {} - ); - this.model.setObject({ input: inputObj }); - return true; - } - return false; - } - - getValidation() { - return this.model.get("meta.validation"); - } - - /** - * Perform client-side validation. Optional input and templates objects can be passed with updated values. - * That will be assigned to the current input/templates. - * - * @param {Object} [newInput] - input object containing only the updated fields. - * @param {Object} [newTemplates] - templates object containing only the updated fields. - * @param {bool} [update] - set true to update the values inside the function. - * @param {Object} [projects] - optionally provide the projects object - * @param {Object} [namespaces] - up-to-date namespaces object - * @param {boolean} [isFetchingVisibilities] - to indicate if the visibilities are ready - */ - validate( - newInput, - newTemplates, - update, - projects, - namespaces, - isFetchingVisibilities - ) { - // get all the necessary data - let model = this.model.get(); - let { templates, input, meta } = model; - - const projectsPaths = - projects && projects?.members?.length - ? projects.members?.map((project) => - project.path_with_namespace.toLowerCase() - ) - : []; - // assign input changes-to-be - if (newInput) input = Object.assign({}, input, newInput); - const { userRepo } = input; - - // assign templates changes-to-be - if (userRepo) templates = meta.userTemplates; - if (newTemplates) templates = Object.assign({}, templates, newTemplates); - - // check warnings (temporary problems) - let warnings = {}; - if (namespaces && namespaces.fetching) - warnings["namespace"] = "Fetching namespaces..."; - - if (projects && projects.fetching) - warnings["title"] = "Fetching projects to prevent duplicates..."; - - if (meta.namespace.fetching || isFetchingVisibilities) - warnings["visibility"] = "Verifying visibility constraints..."; - - if (templates.fetching) warnings["template"] = "Fetching templates..."; - else if (!templates.fetched) - warnings["template"] = "Must fetch the templates first."; - - // check title errors (requires user intervention) - let errors = {}; - const titleNotValid = validateTitle(input.title); - if (titleNotValid) { - errors["title"] = titleNotValid; - } else { - const isDuplicate = checkTitleDuplicates( - input.title, - input.namespace, - projectsPaths - ); - if (isDuplicate) { - errors["title"] = - "Title produces a project identifier (" + - slugFromTitle(input.title, true) + - ") that is already taken in the selected namespace. " + - "Please select a different title or namespace."; - } - } - - // check other errors (requires user intervention). Skip if there is already a warning - if (!warnings["namespace"] && !input.namespace) - errors["namespace"] = "Please select the namespace for the project."; - - if (!warnings["visibility"] && !input.visibility) - errors["visibility"] = "Please select a visibility level."; - - if (!warnings["template"] && !input.template) - errors["template"] = "Please select a template."; - - // create validation object and update model directly or return it; - const validation = { - warnings: { $set: warnings }, - errors: { $set: errors }, - }; - if (update) this.model.setObject({ meta: { validation } }); - return validation; - } -} - -export { NewProjectCoordinator, checkTitleDuplicates, validateTitle }; - -// test only -export { RESERVED_TITLE_NAMES }; diff --git a/client/src/project/new/ProjectNew.test.jsx b/client/src/project/new/ProjectNew.test.jsx deleted file mode 100644 index b0434751c6..0000000000 --- a/client/src/project/new/ProjectNew.test.jsx +++ /dev/null @@ -1,121 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * ProjectNew.test.js - * New project test code. - */ - -import { describe, expect, it } from "vitest"; - -import { btoaUTF8 } from "../../utils/helpers/Encoding"; -import { getDataFromParams } from "./ProjectNew.container"; -import { RESERVED_TITLE_NAMES } from "./ProjectNew.state"; -import { checkTitleDuplicates, validateTitle } from "./index"; - -describe("helper functions", () => { - it("validateTitle", () => { - // missing title - expect(validateTitle()).toContain("missing"); - expect(validateTitle("")).toContain("missing"); - expect(validateTitle("anyTitle")).toBe(null); - - // reserved words -- they must be the only word in the sentence to return an error - for (let i = 0; i < 10; i++) { - const randomNumber = Math.floor(Math.random() * Math.floor(32)); - const reservedWord = RESERVED_TITLE_NAMES[randomNumber]; - if (randomNumber < RESERVED_TITLE_NAMES.length / 5) - expect(validateTitle("prefix " + reservedWord)).toBe(null); - else if (randomNumber > (RESERVED_TITLE_NAMES.length / 5) * 4) - expect(validateTitle(reservedWord + " suffix")).toBe(null); - else expect(validateTitle(reservedWord)).toContain("Reserved"); - } - - // first char - expect(validateTitle("_underscore")).toContain( - "must start with a letter or a number" - ); - expect(validateTitle("1_underscore")).toBe(null); - expect(validateTitle("an_underscore")).toBe(null); - - // any valid char - expect(validateTitle("äñ_")).toContain("must contain at least one letter"); - expect(validateTitle("äañ_")).toBe(null); - }); - - it("checkTitleDuplicates", () => { - const projectsPaths = [ - "username/exist", - "username/exist-different", - "group/exist", - ]; - - // no previous projects - expect(checkTitleDuplicates("exist", "username", [])).toBe(false); - expect(checkTitleDuplicates("exist", "group", null)).toBe(false); - - // different name - expect(checkTitleDuplicates("notExists", "username", projectsPaths)).toBe( - false - ); - expect( - checkTitleDuplicates("exist-different", "group", projectsPaths) - ).toBe(false); - - // same final name - expect(checkTitleDuplicates("exist", "group", projectsPaths)).toBe(true); - expect( - checkTitleDuplicates("exist-different", "username", projectsPaths) - ).toBe(true); - expect(checkTitleDuplicates("existä", "group", projectsPaths)).toBe(true); - expect( - checkTitleDuplicates("exist-äõî-different", "username", projectsPaths) - ).toBe(true); - }); - - it("getDataFromParams", () => { - function encode(params) { - return { data: btoaUTF8(JSON.stringify(params)) }; - } - - let params = { title: "pre-filled" }; - let urlParams = encode(params); - - // title - let decoded = getDataFromParams(urlParams); - expect(decoded).toMatchObject(params); - - // complex - params = { - title: "test", - namespace: "renku-qa", - template: "Custom/python-minimal", - url: "https://github.com/SwissDataScienceCenter/renku-project-template", - ref: "0.1.16", - visibility: "private", - variables: { - description: "description here", - }, - }; - urlParams = encode(params); - decoded = getDataFromParams(urlParams); - expect(decoded).toMatchObject(params); - }); -}); diff --git a/client/src/project/new/components/Automated.tsx b/client/src/project/new/components/Automated.tsx deleted file mode 100644 index 37f3f88285..0000000000 --- a/client/src/project/new/components/Automated.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * Automated.tsx - * Automated Project component - */ -import { useState } from "react"; -import { - Button, - Col, - Fade, - Modal, - ModalBody, - ModalHeader, - Row, -} from "reactstrap"; -import { Link } from "react-router"; - -import { ErrorAlert, InfoAlert, WarnAlert } from "../../../components/Alert"; -import { Url } from "../../../utils/helpers/url"; -import { Loader } from "../../../components/Loader"; -import { Docs } from "../../../utils/constants/Docs"; -import { ExternalLink } from "../../../components/ExternalLinks"; - -const docsUrl = Docs.rtdReferencePage( - "templates.html#create-shareable-project-creation-links-with-pre-filled-fields" -); -const moreInfoLink = ( - <ExternalLink - role="text" - iconSup={true} - iconAfter={true} - url={docsUrl} - title="documentation reference" - /> -); -interface Project { - title?: string; - description?: string; - namespace?: string; - visibility?: string; - url?: string; - ref?: string; - template?: string; - variables?: string[]; -} - -interface AutomatedData extends Project { - finished: boolean; - received: boolean; - valid: boolean; - error: string[]; - warnings: string[]; -} - -interface AutomatedProps { - automated: AutomatedData; - removeAutomated: Function; // eslint-disable-line @typescript-eslint/ban-types -} - -interface AutomatedModalProps { - removeAutomated: Function; // eslint-disable-line @typescript-eslint/ban-types -} - -function Automated({ automated, removeAutomated }: AutomatedProps) { - const [showError, setShowError] = useState(false); - const toggleError = () => setShowError(!showError); - - const [showWarnings, setShowWarnings] = useState(false); - const toggleWarn = () => setShowWarnings(!showWarnings); - - if (!automated.finished) { - // Show a static modal while loading the data - if (automated.received && automated.valid) - return <AutomatedModal removeAutomated={removeAutomated} />; - return null; - } - - // Show a feedback when the automated part has finished - // Case 1: errors - if (automated.error) { - const error = ( - <div className="py-3"> - <code>{automated.error}</code> - </div> - ); - return ( - <ErrorAlert key="alert"> - <div data-cy="project-creation-embedded-error"> - <p> - You used a RenkuLab project-creation link containing embedded data ( - {moreInfoLink}). There was an error while pre-filling the fields. - This is usually a sign of a wrong link or outdated information (E.G: - outdated template links). - </p> - <p> - We used the default settings instead, but that might not lead to the - expected results. - </p> - - <Button - color="danger" - className="btn-sm" - onClick={() => toggleError()} - > - {showError ? "Hide error details" : "Show error details"} - </Button> - <Fade in={showError} tag="div"> - {showError ? error : null} - </Fade> - </div> - </ErrorAlert> - ); - } - // Case 2: warnings - else if (automated.warnings.length) { - const warnings = ( - <div className="py-3"> - <code>{automated.warnings.join("\n")}</code> - </div> - ); - return ( - <WarnAlert> - <div data-cy="project-creation-embedded-warning"> - <p> - You used a RenkuLab project-creation link containing embedded data ( - {moreInfoLink}). Some fields could not be pre-filled, likely because - data is missing or outdated. - </p> - <p> - We used the default settings instead, but that might not lead to the - expected results. - </p> - <Button - color="warning" - className="btn-sm" - onClick={() => toggleWarn()} - > - {showWarnings ? "Hide warnings" : "Show warnings"} - </Button> - <Fade in={showWarnings} tag="div"> - {showWarnings ? warnings : null} - </Fade> - </div> - </WarnAlert> - ); - } - // Case 2: all good, just show a feedback - return ( - <InfoAlert dismissible={false} timeout={0}> - <div data-cy="project-creation-embedded-info"> - <p> - Some fields are pre-filled because you used a RenkuLab - project-creation link containing embedded data ({moreInfoLink}). - </p> - <p className="mb-0"> - You can still change any value before creating a new project. - </p> - </div> - </InfoAlert> - ); -} - -function AutomatedModal(props: AutomatedModalProps) { - const { removeAutomated } = props; - - const [showFadeIn, setShowFadeIn] = useState(false); - - const toggle = () => setShowFadeIn(!showFadeIn); - - const button = showFadeIn ? null : ( - <Button className="btn btn-sm" onClick={() => toggle()}> - Taking too long? - </Button> - ); - - const to = Url.get(Url.pages.project.new); - const fadeInContent = ( - <p className="my-3"> - If pre-filling the new project form is taking too long, you can - <Link - className="btn btn-primary btn-sm" - to={to} - onClick={() => { - removeAutomated(); - }} - > - Start from scratch - </Link> - </p> - ); - return ( - <Modal isOpen={true} centered={true} keyboard={false} backdrop="static"> - <ModalHeader data-cy="project-creation-embedded-fetching"> - Fetching initialization data - </ModalHeader> - <ModalBody> - <Row> - <Col> - <p> - You used a RenkuLab project-creation link containing embedded data - ({moreInfoLink}) - </p> - <p> - Please wait while we fetch the required resources...{" "} - <Loader inline size={16} /> - </p> - <div className="my-3"> - {button} - <Fade in={showFadeIn} tag="div"> - {showFadeIn ? fadeInContent : null} - </Fade> - </div> - </Col> - </Row> - </ModalBody> - </Modal> - ); -} - -export default Automated; diff --git a/client/src/project/new/components/Description.tsx b/client/src/project/new/components/Description.tsx deleted file mode 100644 index db7c518f9b..0000000000 --- a/client/src/project/new/components/Description.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * Description.tsx - * Description field group component - */ -import FieldGroup from "../../../components/FieldGroups"; -import { NewProjectInputs, NewProjectMeta } from "./newProject.types"; - -interface DescriptionProps { - handlers: { - setProperty: Function; // eslint-disable-line @typescript-eslint/ban-types - }; - meta: NewProjectMeta; - input: NewProjectInputs; -} - -function Description({ handlers, meta, input }: DescriptionProps) { - const error = meta.validation.errors["description"]; - const isInvalid = !!error && !input.descriptionPristine; - - return ( - <FieldGroup - id="description" - label="Description" - type="textarea" - value={input.description ?? ""} - help="Let people know what the project is about" - isRequired={false} - feedback={error} - invalid={isInvalid} - onChange={(e) => handlers.setProperty("description", e.target.value)} - /> - ); -} - -export default Description; diff --git a/client/src/project/new/components/Namespaces.jsx b/client/src/project/new/components/Namespaces.jsx deleted file mode 100644 index 52f7a3ad29..0000000000 --- a/client/src/project/new/components/Namespaces.jsx +++ /dev/null @@ -1,290 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * Namespace.js - * Namespace form group component. - */ - -import { Component, Fragment } from "react"; -import Autosuggest from "react-autosuggest"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; -import { Button, FormGroup, UncontrolledTooltip } from "reactstrap"; -import { - InputHintLabel, - InputLabel, - LoadingLabel, -} from "../../../components/formlabels/FormLabels"; - -/** - * Generate refresh button - * - * @param {function} refresh - function to invoke - * @param {string} tip - message to display in the tooltip - * @param {boolean} disabled - whether it's disabled or not - */ -function makeRefreshButton(refresh, tip, disabled) { - const id = refresh.name.replace(" ", ""); - - return ( - <Fragment> - <Button - key="button" - className="ms-1 p-0" - color="link" - size="sm" - id={id} - data-cy="refresh-namespace-list" - onClick={() => refresh()} - disabled={disabled} - > - <FontAwesomeIcon icon={faSyncAlt} /> - </Button> - <UncontrolledTooltip key="tooltip" placement="top" target={id}> - {tip} - </UncontrolledTooltip> - </Fragment> - ); -} - -class NamespacesAutosuggest extends Component { - constructor(props) { - super(props); - this.state = { - value: "", - suggestions: [], - preloadUpdated: false, - }; - } - - componentDidMount() { - // set first user namespace as default (at least one should always available) - const { namespaces, namespace, user } = this.props; - if (namespaces.fetched && namespaces.list.length && !namespace) { - let defaultNamespace = null, - personalNs = null; - if (user.logged) - personalNs = namespaces.list.find( - (ns) => ns.kind === "user" && ns.full_path === user.username - ); - if (personalNs) defaultNamespace = personalNs; - else defaultNamespace = namespaces.list.find((ns) => ns.kind === "user"); - - this.props.handlers.setNamespace(defaultNamespace); - this.setState({ value: defaultNamespace.full_path }); - } - } - - // Fix the inconsistent state when automated content modifies the namespace - componentDidUpdate() { - const { automated, input } = this.props; - const { value, preloadUpdated } = this.state; - if ( - automated && - automated.received && - automated.finished && - input.namespace !== value && - !preloadUpdated - ) - this.setState({ value: input.namespace, preloadUpdated: true }); - } - - getSuggestions(value) { - const { namespaces } = this.props; - const inputValue = value.trim().toLowerCase(); - - // filter namespaces - const filtered = - inputValue.length === 0 - ? namespaces.list - : namespaces.list.filter( - (namespace) => - namespace.full_path.toLowerCase().indexOf(inputValue) >= 0 - ); - if (!filtered.length) return []; - - // separate different namespaces kind - const suggestionsObject = filtered.reduce( - (suggestions, namespace) => { - namespace.kind === "group" - ? suggestions.group.push(namespace) - : suggestions.user.push(namespace); - return suggestions; - }, - { user: [], group: [] } - ); - - // filter 0 length groups - return Object.keys(suggestionsObject).reduce( - (suggestions, kind) => - suggestionsObject[kind].length - ? [...suggestions, { kind, namespaces: suggestionsObject[kind] }] - : suggestions, - [] - ); - } - - getSuggestionValue(suggestion) { - return suggestion.full_path; - } - - getSectionSuggestions(suggestion) { - if (!suggestion) return []; - return suggestion.namespaces; - } - - renderSuggestion = (suggestion) => { - const className = - suggestion.full_path === this.state.value ? "highlighted" : ""; - return <span className={className}>{suggestion.full_path}</span>; - }; - - renderSectionTitle(suggestion) { - return <strong>{suggestion.kind}</strong>; - } - - onBlur = (event, { newValue }) => { - if (newValue) this.props.handlers.setNamespace(newValue); - else if (this.props.input.namespace) - this.setState({ value: this.props.input.namespace }); - }; - - onChange = (event, { newValue, method }) => { - if (method === "type") this.setState({ value: newValue }); - }; - - onSuggestionsFetchRequested = ({ value, reason }) => { - // show all namespaces on mouse click - if (reason === "input-focused") value = ""; - this.setState({ suggestions: this.getSuggestions(value) }); - }; - - onSuggestionsClearRequested = () => { - this.setState({ suggestions: [] }); - }; - - onSuggestionSelected = (event, { suggestionValue }) => { - this.setState({ value: suggestionValue }); - const namespace = this.props.namespaces.list.filter( - (ns) => ns.full_path === suggestionValue - )[0]; - this.props.handlers.setNamespace(namespace); - }; - - getTheme() { - const defaultTheme = { - container: "react-autosuggest__container", - containerOpen: "react-autosuggest__container--open", - input: "react-autosuggest__input", - inputOpen: "react-autosuggest__input--open", - inputFocused: "react-autosuggest__input--focused", - suggestionsContainer: "react-autosuggest__suggestions-container", - suggestionsContainerOpen: - "react-autosuggest__suggestions-container--open", - suggestionsList: "react-autosuggest__suggestions-list", - suggestion: "react-autosuggest__suggestion", - suggestionFirst: "react-autosuggest__suggestion--first", - suggestionHighlighted: "react-autosuggest__suggestion--highlighted", - sectionContainer: "react-autosuggest__section-container", - sectionContainerFirst: "react-autosuggest__section-container--first", - sectionTitle: "react-autosuggest__section-title", - }; - // Override the input theme to match our visual style - return { ...defaultTheme, ...{ input: "form-control" } }; - } - - render() { - const { value, suggestions } = this.state; - const theme = this.getTheme(); - - const inputProps = { - placeholder: "Select a namespace...", - value, - onChange: this.onChange, - onBlur: this.onBlur, - id: "namespace-input", - }; - - return ( - <Autosuggest - id="namespace" - multiSection={true} - suggestions={suggestions} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - getSuggestionValue={this.getSuggestionValue} - getSectionSuggestions={this.getSectionSuggestions} - renderSuggestion={this.renderSuggestion} - renderSectionTitle={this.renderSectionTitle} - shouldRenderSuggestions={() => true} - inputProps={inputProps} - theme={theme} - /> - ); - } -} - -class Namespaces extends Component { - async componentDidMount() { - // fetch namespaces if not available yet - const { namespaces, handlers } = this.props; - if (!namespaces.fetched && !namespaces.fetching) handlers.getNamespaces(); - } - - render() { - const { namespaces, handlers } = this.props; - const refreshButton = makeRefreshButton( - handlers.getNamespaces, - "Refresh namespaces", - namespaces.fetching - ); - const { list } = namespaces; - // show info about visibility only when group namespaces are available - const info = - namespaces.fetched && - list.length && - list.filter((n) => n.kind === "group").length ? ( - <InputHintLabel text="Group namespaces may restrict the visibility options" /> - ) : null; - // loading or autosuggest - const main = namespaces.fetching ? ( - <div> - <LoadingLabel text="Refreshing..." /> - </div> - ) : ( - <> - <NamespacesAutosuggest {...this.props} /> - {info} - </> - ); - - return ( - <FormGroup className="field-group"> - <InputLabel text="Namespace" isRequired="true" /> - <span className="mx-2">{refreshButton}</span> - {main} - </FormGroup> - ); - } -} - -export default Namespaces; diff --git a/client/src/project/new/components/ProjectIdentifier.tsx b/client/src/project/new/components/ProjectIdentifier.tsx deleted file mode 100644 index 953e1fcee9..0000000000 --- a/client/src/project/new/components/ProjectIdentifier.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FormGroup, Input } from "reactstrap"; - -import { slugFromTitle } from "../../../utils/helpers/HelperFunctions"; -import { - InputHintLabel, - InputLabel, -} from "../../../components/formlabels/FormLabels"; -import { NewProjectInputs } from "./newProject.types"; - -interface ProjectIdentifierProps { - input: NewProjectInputs; - isRequired: boolean; -} - -const ProjectIdentifier = ({ input, isRequired }: ProjectIdentifierProps) => { - const namespace = input.namespace ? input.namespace : "<no namespace>"; - const title = input.title ? slugFromTitle(input.title, true) : "<no title>"; - const slug = `${namespace}/${title}`; - - return ( - <FormGroup className="field-group"> - <InputLabel text="Identifier" isRequired={isRequired} /> - <Input id="slug" data-cy="project-slug" readOnly value={slug} /> - <InputHintLabel text="This is automatically derived from Namespace and Title" /> - </FormGroup> - ); -}; - -export default ProjectIdentifier; diff --git a/client/src/project/new/components/SubmitFormButton.tsx b/client/src/project/new/components/SubmitFormButton.tsx deleted file mode 100644 index 66e8c5299f..0000000000 --- a/client/src/project/new/components/SubmitFormButton.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MouseEventHandler, useState } from "react"; -import { Button, DropdownItem } from "reactstrap"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faLink } from "@fortawesome/free-solid-svg-icons"; - -import ShareLinkModal from "./ShareLinkModal"; -import { ButtonWithMenu } from "../../../components/buttons/Button"; -import { NewProjectInputs, NewProjectMeta } from "./newProject.types"; - -interface SubmitFormButtonProps { - createDataAvailable: boolean; - handlers: { - createEncodedUrl: Function; // eslint-disable-line @typescript-eslint/ban-types - onSubmit: MouseEventHandler<HTMLButtonElement>; - }; - importingDataset: boolean; - input: NewProjectInputs; - meta: NewProjectMeta; -} - -function ImportSubmitFormButton({ - handlers, -}: Pick<SubmitFormButtonProps, "handlers">) { - return ( - <> - <div className="mt-4 d-flex justify-content-end"> - <Button - data-cy="add-dataset-submit-button" - id="create-new-project" - color="rk-pink" - onClick={handlers.onSubmit} - > - Add Dataset New Project - </Button> - </div> - </> - ); -} - -function StandardSubmitFormButton({ - createDataAvailable, - handlers, - input, - meta, -}: Omit<SubmitFormButtonProps, "importingDataset">) { - const [showModal, setShotModal] = useState(false); - const toggleModal = () => { - setShotModal((showModal) => !showModal); - }; - const shareLinkModal = ( - <ShareLinkModal - show={showModal} - toggle={toggleModal} - input={input} - meta={meta} - createUrl={handlers.createEncodedUrl} - /> - ); - - const createProject = ( - <Button - id="create-new-project" - color="secondary" - data-cy="create-project-button" - disabled={!createDataAvailable} - onClick={handlers.onSubmit} - > - {" "} - Create project - </Button> - ); - const createLink = ( - <DropdownItem onClick={toggleModal}> - <FontAwesomeIcon icon={faLink} /> Create link - </DropdownItem> - ); - - return ( - <> - {shareLinkModal} - <div className="mt-4 d-flex justify-content-end"> - <ButtonWithMenu - color="rk-green" - default={createProject} - direction="up" - isPrincipal={true} - > - {createLink} - </ButtonWithMenu> - </div> - </> - ); -} - -function SubmitFormButton({ - createDataAvailable, - handlers, - input, - importingDataset, - meta, -}: SubmitFormButtonProps) { - if (importingDataset) { - return <ImportSubmitFormButton handlers={handlers} />; - } - return ( - <StandardSubmitFormButton - {...{ createDataAvailable, handlers, input, meta }} - /> - ); -} - -export default SubmitFormButton; diff --git a/client/src/project/new/components/Template.tsx b/client/src/project/new/components/Template.tsx deleted file mode 100644 index 8d0aa5a480..0000000000 --- a/client/src/project/new/components/Template.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FormGroup } from "reactstrap"; -import TemplateSelector from "../../../components/templateSelector/TemplateSelector"; -import { - NewProjectConfig, - NewProjectInput, - NewProjectMeta, - NewProjectTemplate, - NewProjectTemplates, -} from "../../../model/renkuModels.types"; - -interface TemplateProps { - config: NewProjectConfig; - handlers: { - setProperty: (target: string, value: unknown) => void; - }; - input: NewProjectInput; - templates: NewProjectTemplates; - meta: NewProjectMeta; -} - -/** Template field group component */ -export const Template = ({ - config, - handlers, - input, - templates, - meta, -}: TemplateProps) => { - const error = meta.validation.errors["template"]; - const invalid = !!error && !input.templatePristine; - - const isFetching = - (!input.userRepo && !!templates.fetching) || - (input.userRepo && !!meta.userTemplates.fetching); - const noFetchedUserRepo = input.userRepo && !meta.userTemplates.fetched; - // Pass down templates and repository with the same format to the gallery component - const [listedTemplates, repositories] = input.userRepo - ? [ - meta.userTemplates.all, - [ - { - url: meta.userTemplates.url, - ref: meta.userTemplates.ref, - name: "Custom", - }, - ], - ] - : [templates.all, config.repositories]; - - const select = (template: NewProjectTemplate) => - handlers.setProperty("template", template); - - return ( - <FormGroup className="field-group"> - <TemplateSelector - repositories={repositories} - select={select} - selected={input.template} - templates={listedTemplates} - isRequired - isInvalid={invalid} - isFetching={isFetching} - noFetchedUserRepo={noFetchedUserRepo} - error={error} - /> - </FormGroup> - ); -}; diff --git a/client/src/project/new/components/TemplateSource.tsx b/client/src/project/new/components/TemplateSource.tsx deleted file mode 100644 index 5d794f9683..0000000000 --- a/client/src/project/new/components/TemplateSource.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Button, ButtonGroup, FormGroup } from "reactstrap"; - -import { InputLabel } from "../../../components/formlabels/FormLabels"; -import { NewProjectInputs } from "./newProject.types"; - -interface TemplateSourceProps { - handlers: { - setProperty: Function; // eslint-disable-line @typescript-eslint/ban-types - }; - input: NewProjectInputs; - isRequired: boolean; -} - -const TemplateSource = ({ - handlers, - input, - isRequired, -}: TemplateSourceProps) => { - return ( - <FormGroup className="field-group"> - <InputLabel text="Template source" isRequired={isRequired} /> - <br /> - <ButtonGroup size="sm"> - <Button - active={!input.userRepo} - data-cy="renkulab-source-button" - onClick={() => handlers.setProperty("userRepo", false)} - > - RenkuLab - </Button> - <Button - active={!!input.userRepo} - data-cy="custom-source-button" - onClick={() => handlers.setProperty("userRepo", true)} - > - Custom - </Button> - </ButtonGroup> - </FormGroup> - ); -}; - -export default TemplateSource; diff --git a/client/src/project/new/components/TemplateVariables.jsx b/client/src/project/new/components/TemplateVariables.jsx deleted file mode 100644 index 230090bbd4..0000000000 --- a/client/src/project/new/components/TemplateVariables.jsx +++ /dev/null @@ -1,217 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * renku-ui - * - * TemplateVariables.js - * Template Variables field group component - */ -import { Component } from "react"; -import { toCapitalized as capitalize } from "../../../utils/helpers/HelperFunctions"; -import { - Button, - FormGroup, - Input, - Label, - UncontrolledTooltip, -} from "reactstrap"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faUndo } from "@fortawesome/free-solid-svg-icons"; -import { - InputHintLabel, - InputLabel, -} from "../../../components/formlabels/FormLabels"; - -/** - * Create a "restore default" button. - * - * @param {function} restore - function to invoke - * @param {string} tip - message to display in the tooltip - * @param {boolean} disabled - whether it's disabled or not - */ -function RestoreButton({ restore, name, disabled }) { - const id = `restore_${name}`; - const tip = disabled - ? "Default value already selected" - : "Restore default value"; - - return ( - <div id={id} className="d-inline ms-2"> - <Button - key="button" - className="p-0" - color="link" - size="sm" - onClick={() => restore()} - disabled={disabled} - > - <FontAwesomeIcon icon={faUndo} /> - </Button> - <UncontrolledTooltip key="tooltip" placement="top" target={id}> - {tip} - </UncontrolledTooltip> - </div> - ); -} - -class TemplateVariables extends Component { - render() { - const { input, handlers } = this.props; - if (!input.template) return null; - - const templates = input.userRepo - ? this.props.meta.userTemplates - : this.props.templates; - - const template = templates.all.filter((t) => t.id === input.template)[0]; - if ( - !template || - !template.variables || - !Object.keys(template.variables).length - ) - return null; - const variables = Object.keys(template.variables).map((variable) => { - const data = template.variables[variable]; - - // fallback to avoid breaking old variable structure - if (typeof data !== "object") { - return ( - <FormGroup key={variable}> - <InputLabel text={capitalize(variable)} /> - <Input - id={"parameter-" + variable} - type="text" - value={input.variables[variable]} - onChange={(e) => handlers.setVariable(variable, e.target.value)} - /> - <InputHintLabel text={capitalize(template.variables[variable])} /> - </FormGroup> - ); - } - - // expected `data` properties: default_value, description, enum, type. - // changing enum to enumValues to avoid using js reserved word - return ( - <Variable - enumValues={data["enum"]} - handlers={handlers} - key={variable} - input={input} - name={variable} - {...data} - /> - ); - }); - - return variables; - } -} - -function Variable(props) { - const { - default_value, - description, - enumValues, - handlers, - input, - name, - type, - } = props; - const id = `parameter-${name}`; - - const descriptionOutput = description ? ( - <InputHintLabel text={capitalize(description)} /> - ) : null; - - const defaultOutput = - default_value != null ? `Default: ${default_value}` : null; - - const restoreButton = - default_value != null ? ( - <RestoreButton - disabled={input.variables[name] === default_value} - name={name} - restore={() => handlers.setVariable(name, default_value)} - /> - ) : null; - - let inputElement = null; - if (type === "boolean") { - inputElement = ( - <FormGroup className="form-check form-switch d-inline-block"> - <Input - type="switch" - id={id} - label={name} - checked={input.variables[name]} - onChange={(e) => handlers.setVariable(name, e.target.checked)} - className="form-check-input rounded-pill" - /> - <Label check htmlFor={"parameter-" + name}> - {name} - </Label> - {restoreButton} - </FormGroup> - ); - // inputElement = null; - } else if (type === "enum") { - const enumObjects = enumValues.map((enumObject) => { - const enumId = `enum-${id}-${enumObject.toString()}`; - return ( - <option key={enumId} value={enumObject}> - {enumObject} - </option> - ); - }); - inputElement = ( - <FormGroup> - <InputLabel text={name} /> - {restoreButton} - <Input - id={id} - type="select" - value={input.variables[name]} - onChange={(e) => handlers.setVariable(name, e.target.value)} - > - {enumObjects} - </Input> - {descriptionOutput} - </FormGroup> - ); - } else { - const inputType = type === "number" ? "number" : "text"; - inputElement = ( - <FormGroup> - <InputLabel text={name} /> - {restoreButton} - <Input - id={id} - type={inputType} - value={input.variables[name]} - onChange={(e) => handlers.setVariable(name, e.target.value)} - placeholder={defaultOutput} - /> - {descriptionOutput} - </FormGroup> - ); - } - - return inputElement; -} - -export default TemplateVariables; diff --git a/client/src/project/new/components/Title.tsx b/client/src/project/new/components/Title.tsx deleted file mode 100644 index a8b0e36b09..0000000000 --- a/client/src/project/new/components/Title.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * renku-ui - * - * Title.js - * Project Title field group component - */ -import { ExternalLink } from "../../../components/ExternalLinks"; -import FieldGroup from "../../../components/FieldGroups"; -import { - NewProjectHandlers, - NewProjectInputs, - NewProjectMeta, -} from "./newProject.types"; - -interface TitleProps { - handlers: NewProjectHandlers; - meta: NewProjectMeta; - input: NewProjectInputs; -} - -const Title = ({ handlers, meta, input }: TitleProps) => { - const error = meta.validation.errors["title"]; - const url = - "https://docs.gitlab.com/ce/user/reserved_names.html#reserved-project-names"; - - const help = ( - <span> - A brief name to identify the project. There are a few{" "} - <ExternalLink url={url} title="reserved names" role="link" /> you cannot - use - </span> - ); - - return ( - <FieldGroup - id="title" - label="Title" - value={input.title ?? ""} - help={help} - feedback={error} - invalid={!!error && !input.titlePristine} - isRequired={true} - onChange={(e) => handlers.setProperty("title", e.target.value)} - /> - ); -}; - -export default Title; diff --git a/client/src/project/new/components/UserTemplate.jsx b/client/src/project/new/components/UserTemplate.jsx deleted file mode 100644 index 782905fb84..0000000000 --- a/client/src/project/new/components/UserTemplate.jsx +++ /dev/null @@ -1,200 +0,0 @@ -/*! - * Copyright 2022 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * UserTemplate.js - * UserTemplate field group component - */ -import { Component, Fragment } from "react"; -import { Button, FormGroup, FormText, Input } from "reactstrap"; - -import { Docs, Links } from "../../../utils/constants/Docs"; -import { - ErrorLabel, - InputHintLabel, - InputLabel, -} from "../../../components/formlabels/FormLabels"; -import { CoreErrorAlert } from "../../../components/errors/CoreErrorAlert"; -import { ExternalLink } from "../../../components/ExternalLinks"; - -const ErrorTemplateFeedback = ({ templates, meta, input }) => { - if (input.userRepo) templates = meta.userTemplates; - // check template errors and provide adequate feedback - let alert = null; - let error = - templates.errors && templates.errors.length ? templates.errors[0] : null; - - if (templates.fetched != null && !templates.fetching && error) { - const fatal = !(templates.all && templates.all.length); - const suggestion = input.userRepo ? null : ( - <span> - You can try refreshing the page. If the error persists, you should - contact the development team on  - <a href={Links.GITTER} target="_blank" rel="noreferrer noopener"> - Gitter - </a>{" "} - or  - <a href={Links.GITHUB} target="_blank" rel="noreferrer noopener"> - GitHub - </a> - . - </span> - ); - - // extract message and details - let details = null, - errorObject = null; - if (typeof error === "string") { - details = error; - errorObject = { code: 10000 }; - } else { - const first = error[Object.keys(error)[0]]; - if (typeof first === "string") { - details = first; - errorObject = { code: 10000 }; - } else { - details = first.userMessage ? first.userMessage : first.reason; - errorObject = first; - if (fatal && !input.userRepo) errorObject.code = 10000; - } - } - const message = fatal - ? "Unable to fetch templates." - : "Some templates could not be fetched."; - - alert = ( - <CoreErrorAlert - details={details} - error={errorObject} - message={message} - suggestion={suggestion} - /> - ); - } - return alert; -}; - -class UserTemplate extends Component { - constructor(props) { - super(props); - this.state = { - missingUrl: false, - missingRef: false, - }; - } - - fetchTemplates() { - const { meta } = this.props; - - // check if url or ref are missing - const { missingUrl, missingRef } = this.state; - let newState = { - missingUrl: false, - missingRef: false, - }; - if (!meta.userTemplates.url) newState.missingUrl = true; - if (!meta.userTemplates.ref) newState.missingRef = true; - if ( - missingUrl !== newState.missingUrl || - missingRef !== newState.missingRef - ) - this.setState(newState); - - // try to get user templates if repository data are available - if (newState.missingUrl || newState.missingRef) return; - return this.props.handlers.getUserTemplates(); - } - - render() { - const { meta, handlers, config } = this.props; - - // placeholders and links - let urlExample = - "https://github.com/SwissDataScienceCenter/renku-project-template"; - if (config.repositories && config.repositories.length) - urlExample = config.repositories[0].url; - let refExample = "0.1.11"; - if (config.repositories && config.repositories.length) - refExample = config.repositories[0].ref; - const templatesDocs = ( - <ExternalLink - role="text" - title="Renku templates" - url={Docs.rtdReferencePage("templates.html")} - /> - ); - return ( - <Fragment> - <FormGroup className="field-group"> - <InputLabel isRequired="true" text="Repository URL" /> - <Input - type="text" - value={meta.userTemplates.url} - onChange={(e) => - handlers.setTemplateProperty("url", e.target.value) - } - data-cy="url-repository" - invalid={this.state.missingUrl} - /> - {this.state.missingUrl && ( - <ErrorLabel text="Provide a template repository URL" /> - )} - <FormText> - A valid {templatesDocs} repository. E.G. {urlExample} - </FormText> - </FormGroup> - - <FormGroup className="field-group"> - <InputLabel isRequired="true" text="Repository Reference" /> - <Input - type="text" - value={meta.userTemplates.ref} - onChange={(e) => - handlers.setTemplateProperty("ref", e.target.value) - } - data-cy="ref-repository" - invalid={this.state.missingRef} - /> - {this.state.missingRef && ( - <ErrorLabel text="Provide a template repository reference" /> - )} - <InputHintLabel - text={`Preferably a tag or a commit. - A branch is also valid, but it is not a static reference E.G. ${refExample}`} - /> - </FormGroup> - <FormGroup className="field-group"> - <Button - id="fetch-custom-templates" - className="btn-outline-rk-green" - size="sm" - data-cy="fetch-templates-button" - onClick={() => this.fetchTemplates()} - > - Fetch templates - </Button> - </FormGroup> - </Fragment> - ); - } -} - -export default UserTemplate; -export { ErrorTemplateFeedback }; diff --git a/client/src/project/new/index.js b/client/src/project/new/index.js deleted file mode 100644 index 4ece0eb416..0000000000 --- a/client/src/project/new/index.js +++ /dev/null @@ -1,29 +0,0 @@ -/*! - * Copyright 2020 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * renku-ui - * - * project/new - * Components for the new project page - */ - -import { ForkProject } from "./ProjectNew.container"; -import { validateTitle, checkTitleDuplicates } from "./ProjectNew.state"; - -export { ForkProject, validateTitle, checkTitleDuplicates }; diff --git a/tests/cypress/e2e/addDatasetToProject.spec.ts b/tests/cypress/e2e/addDatasetToProject.spec.ts index e2f1cdeccd..f3a05950bf 100644 --- a/tests/cypress/e2e/addDatasetToProject.spec.ts +++ b/tests/cypress/e2e/addDatasetToProject.spec.ts @@ -16,205 +16,14 @@ * limitations under the License. */ -import fixtures from "../support/renkulab-fixtures"; - -describe("Add dataset to existing project", () => { - const datasetName = "abcd"; - const datasetIdentifier = "4577b68957b7478bba1f07d6513b43d2"; - const pathOrigin = "e2e/testing-datasets"; - const projectSelected = "e2e/local-test-project"; - - beforeEach(() => { - fixtures.config().versions().userTest(); - fixtures.landingUserProjects({ fixture: "projects/member-projects.json" }); - fixtures.datasetById({ id: datasetIdentifier }); - fixtures - .project({ projectPath: pathOrigin, statistics: false }) - .cacheProjectList(); - fixtures.projectMigrationUpToDate({ - name: "migrationCheckDatasetProject", - queryUrl: "*", - }); - fixtures.importToProject(); - fixtures.importJobCompleted(); - cy.visit(`datasets/${datasetIdentifier}/add`); - cy.wait("@getLandingUserProjects"); - cy.wait("@getDatasetById"); - cy.wait("@getProject"); - cy.wait("@migrationCheckDatasetProject"); - // verify load dataset info - cy.getDataCy("add-dataset-to-project-title").should("contain", datasetName); - cy.getDataCy("add-dataset-existing-project-option-button").should( - "have.class", - "active" - ); - cy.getDataCy("form-project-exist").should("exist"); - }); - - it("valid dataset", () => { - cy.selectProjectFromAutosuggestionList( - projectSelected, - fixtures, - "project/migrationStatus/level1-all-good.json" - ); - cy.getDataCy("import-dataset-status").should( - "contain.text", - "Selected project is compatible with dataset." - ); - cy.getDataCy("add-dataset-submit-button").should("not.be.disabled"); - }); - - it("successfully import dataset", () => { - fixtures.projectTestContents({ coreServiceV8: { coreVersion: 9 } }); - fixtures.projectKGDatasetList({ projectPath: projectSelected }); - fixtures.projectDatasetList(); - fixtures.projectTest(); - fixtures.projectLockStatus(); - cy.selectProjectFromAutosuggestionList( - projectSelected, - fixtures, - "project/migrationStatus/level1-all-good.json" - ); - cy.getDataCy("add-dataset-submit-button").click(); - cy.wait("@importToProject"); - cy.wait("@importJobCompleted", { timeout: 20_000 }); - cy.url().should( - "include", - `projects/${projectSelected}/datasets/${datasetName}` - ); - cy.getDataCy("dataset-title").should("contain.text", datasetName); - }); - - it("error importing dataset", () => { - cy.selectProjectFromAutosuggestionList( - projectSelected, - fixtures, - "project/migrationStatus/level1-all-good.json" - ); - fixtures.importJobError(); - cy.getDataCy("add-dataset-submit-button").click(); - cy.wait("@importToProject"); - cy.wait("@importJobError", { timeout: 20_000 }); - cy.getDataCy("import-dataset-status").should( - "contain.text", - "Dataset import failed" - ); - }); - - it("error importing from project with different metadata version", () => { - cy.selectProjectFromAutosuggestionList( - projectSelected, - fixtures, - "project/migrationStatus/level4-old-updatable.json" - ); - cy.getDataCy("import-dataset-status").contains( - "cannot be newer than the project metadata version" - ); - cy.getDataCy("add-dataset-submit-button").should("be.disabled"); - }); -}); - -describe("Add dataset to new project", () => { - const datasetName = "abcd"; +describe("Add dataset to project", () => { const datasetIdentifier = "4577b68957b7478bba1f07d6513b43d2"; - const pathOrigin = "e2e/testing-datasets"; - const newProjectTitle = "new-project"; - const newProjectPath = `e2e/${newProjectTitle}`; - beforeEach(() => { - fixtures.config().versions().userTest().namespaces().templates(); - fixtures - .projects() - .landingUserProjects({ fixture: "projects/member-projects.json" }); - fixtures.datasetById({ id: datasetIdentifier }); - fixtures - .project({ projectPath: pathOrigin, statistics: false }) - .cacheProjectList(); - fixtures.interceptMigrationCheck({ - name: "migrationCheckDatasetProject", - queryUrl: "*", - }); + it("Add dataset to existing project not supported", () => { cy.visit(`datasets/${datasetIdentifier}/add`); - fixtures.importToProject(); - fixtures.importJobCompleted(); - cy.wait("@getDatasetById"); - cy.wait("@getProject"); - cy.wait("@migrationCheckDatasetProject"); - // check contain dataset info - cy.getDataCy("add-dataset-to-project-title").should("contain", datasetName); - // go to add dataset to new project option - cy.getDataCy("add-dataset-new-project-option-button").click(); - }); - - it("valid dataset, successful import", () => { - fixtures.interceptMigrationCheck({ - name: "migrationCheckSelectedProject", - queryUrl: "*", - }); - fixtures.projectTestContents({ coreServiceV8: { coreVersion: 9 } }); - fixtures.projectKGDatasetList({ projectPath: newProjectPath }); - fixtures.projectDatasetList(); - // fill form new project - cy.createProjectAndAddDataset(newProjectTitle, newProjectPath, fixtures); - fixtures.project({ name: "getNewProject2", projectPath: newProjectPath }); - cy.wait("@importToProject"); - cy.wait("@importJobCompleted", { timeout: 20_000 }); - cy.url().should( - "include", - `projects/${newProjectPath}/datasets/${datasetName}` - ); - cy.getDataCy("dataset-title").should("contain.text", datasetName); - }); - - it("error importing dataset", () => { - fixtures.interceptMigrationCheck({ - name: "migrationCheckSelectedProject", - queryUrl: "*", - }); - fixtures.importJobError(); - cy.createProjectAndAddDataset(newProjectTitle, newProjectPath, fixtures); - cy.wait("@importToProject"); - cy.wait("@importJobError", { timeout: 20_000 }); - cy.getDataCy("import-dataset-status").should( - "contain.text", - "Something fail" + cy.getDataCy("sunset-banner").should( + "contain", + "Project creation no longer available" ); }); }); - -describe("Invalid dataset", () => { - beforeEach(() => { - fixtures.config().versions().userTest(); - fixtures - .projects() - .landingUserProjects({ fixture: "projects/member-projects.json" }); - }); - - it("displays warning when dataset doesn't exist", () => { - const datasetIdentifier = "4577b68957b7478bba1f07d6513b43d2"; - fixtures.invalidDataset({ id: datasetIdentifier }); - cy.visit(`datasets/${datasetIdentifier}/add`); - cy.wait("@invalidDataset"); - cy.get("h3").contains("Dataset not found").should("be.visible"); - }); - - it("displays warning when dataset is invalid", () => { - const datasetIdentifier = "4577b68957b7478bba1f07d6513b43d2"; - fixtures.datasetById({ id: datasetIdentifier }); - const pathOrigin = "e2e/testing-datasets"; - fixtures - .errorProject({ project: { projectPath: pathOrigin } }) - .cacheProjectList(); - fixtures.interceptMigrationCheck({ - name: "migrationCheckDatasetProject", - queryUrl: "*", - }); - cy.visit(`datasets/${datasetIdentifier}/add`); - cy.wait("@getDatasetById"); - cy.wait("@getErrorProject"); - cy.getDataCy("add-dataset-submit-button").should("be.disabled"); - cy.getDataCy("import-dataset-status") - .contains("Invalid Dataset") - .should("be.visible"); - }); -}); diff --git a/tests/cypress/e2e/newDataset.spec.ts b/tests/cypress/e2e/newDataset.spec.ts index 4cd54fd621..69b01dd1ca 100644 --- a/tests/cypress/e2e/newDataset.spec.ts +++ b/tests/cypress/e2e/newDataset.spec.ts @@ -32,265 +32,17 @@ describe("Project new dataset", () => { fixtures.projectMigrationUpToDate({ queryUrl: "*" }); fixtures.projectLockStatus(); cy.visit(`projects/${projectPath}/datasets/new`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@getMigration"); cy.wait("@datasetList"); }); - it("complete new dataset", () => { - fixtures.createDataset(); - fixtures.uploadDatasetFile(); - cy.newDataset({ - name: "New dataset completed", - keywords: ["test key 1", "key 2"], - description: "This is a dataset description", - file: "count_flights.txt", - image: "sdsc.jpeg", - }); - cy.wait("@uploadDatasetFile"); - // check that the default creator was added - cy.get("input[name=default-creator]") - .should("be.disabled") - .should("have.value", "E2E User (e2e@renku.ch)"); - cy.getDataCy("submit-button").click(); - cy.wait("@createDataset", { timeout: 20_000 }); - cy.wait("@addFile"); - cy.url().should( - "include", - `projects/${projectPath}/datasets/new-dataset-completed` - ); - }); - - it("complete new dataset with non-default creator", () => { - fixtures.createDataset(); - fixtures.uploadDatasetFile(); - cy.newDataset({ - name: "New dataset completed", - creators: { - name: "Name Creator", - email: "email@creator.com", - affiliation: "SDSC", - }, - keywords: ["test key 1", "key 2"], - description: "This is a dataset description", - file: "count_flights.txt", - image: "sdsc.jpeg", - }); - cy.wait("@uploadDatasetFile"); - // There should be a default creator and a non-default creator - cy.get("input[name=default-creator]") - .should("be.disabled") - .should("have.value", "E2E User (e2e@renku.ch)"); - cy.getDataCy("creator-email").should("have.value", "email@creator.com"); - cy.getDataCy("submit-button").click(); - cy.wait("@createDataset", { timeout: 20_000 }); - cy.wait("@addFile"); - cy.url().should( - "include", - `projects/${projectPath}/datasets/new-dataset-completed` - ); - }); - - it("resets form when going to a new project", () => { - const secondProjectPath = "e2e/random-project"; - fixtures.project({ projectPath: secondProjectPath }); - fixtures.createDataset(); - fixtures.uploadDatasetFile(); - cy.newDataset({ - name: "New dataset completed", - creators: { - name: "Name Creator", - email: "email@creator.com", - affiliation: "SDSC", - }, - keywords: ["test key 1", "key 2"], - description: "This is a dataset description", - file: "count_flights.txt", - image: "sdsc.jpeg", - }); - // check that the default creator was added - cy.get("input[name=default-creator]") - .should("be.disabled") - .should("have.value", "E2E User (e2e@renku.ch)"); - cy.getDataCy("creator-email").should("have.value", "email@creator.com"); - - // Visit a new project and check that the form was cleared - cy.visit(`projects/${secondProjectPath}/datasets/new`); - cy.wait("@getProject"); - cy.wait("@getMigration"); - cy.wait("@datasetList"); - cy.getDataCy("input-title").should("have.value", ""); - cy.get("input[name=default-creator]") - .should("be.disabled") - .should("have.value", "E2E User (e2e@renku.ch)"); - cy.getDataCy("creator-email").should("not.exist"); - }); - - it("upload dataset file", () => { - fixtures.uploadDatasetFile({ - fixture: "datasets/upload-dataset-multiple-files.json", - name: "multipleFilesUpload", - overrideExisting: true, - unpackArchive: true, - }); - cy.newDataset({ - name: "New dataset completed", - }); - cy.get('[data-cy="dropzone"]').attachFile( - "/datasets/files/datasetFiles.zip", - { subjectType: "drag-n-drop" } - ); - cy.getDataCy("upload-compressed-yes").click(); - cy.wait("@multipleFilesUpload"); - cy.getDataCy("file-name-column").contains("Show unzipped files"); - cy.getDataCy("display-zip-files-link").click(); - cy.getDataCy("file-name-column").contains("New Folder With Items"); - cy.getDataCy("delete-file-button").click(); - cy.getDataCy("file-name-column").should("not.exist"); - - // load multiple files - fixtures.uploadDatasetFile(); - cy.get('[data-cy="dropzone"]').attachFile( - "/datasets/files/datasetFiles.zip", - { subjectType: "drag-n-drop" } - ); - cy.get('[data-cy="dropzone"]').attachFile("/datasets/files/bigFile.bin", { - subjectType: "drag-n-drop", - }); - // ? Needed for tests running through GitHub actions - cy.wait(1000, { log: false }); // eslint-disable-line cypress/no-unnecessary-waiting - cy.get('[data-cy="dropzone"]').attachFile( - "/datasets/files/count_flights.txt", - { subjectType: "drag-n-drop" } - ); - // ? Needed for tests running through GitHub actions - cy.wait(5000, { log: false }); // eslint-disable-line cypress/no-unnecessary-waiting - cy.wait("@uploadDatasetFile"); - cy.getDataCy("file-name-column").should("have.length", 3); - }); - - it("error upload dataset file", () => { - fixtures.uploadDatasetFile({ - fixture: "", - name: "errorUploadFile", - overrideExisting: true, - statusCode: 500, - }); - cy.newDataset({ - name: "New dataset fail", - }); - cy.get('[data-cy="dropzone"]').attachFile("/datasets/files/bigFile.bin", { - subjectType: "drag-n-drop", - }); - cy.wait("@errorUploadFile", { timeout: 10_000 }); - cy.getDataCy("upload-error-message").contains( - "Server responded with 500 code." - ); - cy.getDataCy("submit-button").click(); - cy.get("div.error-feedback") - .contains("Please fix problems in the following field: Files") - .should("exist"); - }); - - it("shows error on empty title", () => { - fixtures.createDataset({ - fixture: "datasets/create-dataset-title-error.json", - name: "createDatasetError", - }); - cy.getDataCy("submit-button").click(); - cy.get("div.error-feedback") - .contains("Please fix problems") - .should("exist"); - }); - - it("shows error on invalid title", () => { - fixtures.createDataset({ - fixture: "datasets/create-dataset-title-error.json", - name: "createDatasetError", - }); - cy.newDataset({ - name: "test@", - }); - cy.getDataCy("submit-button").click(); - cy.wait("@createDatasetError"); - cy.get("div.alert-danger") - .contains("Errors occurred while performing this operation.") - .should("exist"); - }); -}); - -describe("Project new dataset without access", () => { - const projectPath = "e2e/local-test-project"; - - beforeEach(() => { - fixtures.config().versions().userTest(); - fixtures.projects().landingUserProjects().projectTestObserver(); - fixtures.projectLockStatus(); - fixtures.cacheProjectList(); - fixtures.projectKGDatasetList({ projectPath }); - fixtures.projectDatasetList(); - fixtures.createDataset(); - fixtures.projectTestContents({ coreServiceV8: { coreVersion: 9 } }); - fixtures.projectMigrationUpToDate({ queryUrl: "*" }); - fixtures.projectLockStatus(); - }); - - it("correctly handles missing access", () => { - cy.visit(`projects/${projectPath}/datasets/new`); - cy.wait("@getProject"); - cy.contains("If you were recently given access").should("be.visible"); - }); -}); - -describe("Project import dataset", () => { - const projectPath = "e2e/testing-datasets"; - - beforeEach(() => { - fixtures.config().versions().userTest(); - fixtures.projects().landingUserProjects(); - fixtures.project({ projectPath }).cacheProjectList(); - fixtures.projectKGDatasetList({ projectPath }); - fixtures.projectDatasetList(); - fixtures.projectTestContents({ coreServiceV8: { coreVersion: 9 } }); - fixtures.projectMigrationUpToDate({ queryUrl: "*" }); - fixtures.projectLockStatus(); - fixtures.importToProject(); - }); - - it("import dataset", () => { - fixtures.importJobCompleted(); - cy.visit(`projects/${projectPath}/datasets/new`); - cy.wait("@getProject"); - cy.wait("@getMigration"); - cy.wait("@datasetList"); - cy.contains("Import").click(); - cy.getDataCy("input-uri") - .click() - .type("https://www.doi.org/10.7910/DVN/WTZS4K"); - cy.getDataCy("submit-button").click(); - cy.wait("@importToProject"); - cy.contains("Creating Dataset...").should("be.visible"); - cy.wait("@importJobCompleted", { timeout: 20000 }); - cy.wait("@datasetList"); - cy.contains("Datasets List").should("be.visible"); - }); - - it("shows error on invalid url", () => { - fixtures.importJobError(); - cy.visit(`projects/${projectPath}/datasets/new`); - cy.wait("@getProject"); - cy.wait("@getMigration"); - cy.wait("@datasetList"); - cy.contains("Import").click(); - cy.getDataCy("input-uri") - .click() - .type("https://www.doi.org/10.7910/DVN/WTZS4K"); - cy.getDataCy("submit-button").click(); - cy.wait("@importToProject"); - cy.contains("Creating Dataset...").should("be.visible"); - cy.wait("@importJobError", { timeout: 20000 }); - cy.contains("Errors occurred while performing this operation.").should( - "be.visible" + it("new dataset not supported", () => { + cy.getDataCy("sunset-banner").should( + "contain", + "Project creation no longer available" ); }); }); diff --git a/tests/cypress/e2e/newProject.spec.ts b/tests/cypress/e2e/newProject.spec.ts index 624d383786..11baf515f6 100644 --- a/tests/cypress/e2e/newProject.spec.ts +++ b/tests/cypress/e2e/newProject.spec.ts @@ -29,7 +29,7 @@ describe("Add new project", () => { cy.visit("/v1/projects/new"); }); - it("create a new project that should change name", () => { + it("create a new project is not longer supported", () => { fixtures .templates() .createProject() @@ -39,321 +39,9 @@ describe("Add new project", () => { statistics: false, }) .updateProject({ projectPath: newProjectPath }); - cy.createProject(newProjectTitle); - cy.wait("@getTemplates"); - cy.wait("@createProject"); - cy.wait("@getNewProject"); - cy.wait("@updateProject").should((result) => { - const request = result.request.body; - expect(request).to.have.property("name"); - }); - cy.url().should("include", `projects/${newProjectPath}`); - }); - - it("error on getting templates", () => { - fixtures.templates({ error: true }); - cy.wait("@getTemplates"); - cy.contains("Unable to fetch templates").should("be.visible"); - }); - - it("error on creating a new project", () => { - fixtures - .templates() - .createProject({ fixture: "errors/core-error-1102.json" }); - cy.createProject(newProjectTitle); - cy.wait("@getTemplates"); - cy.wait("@createProject"); - cy.contains("error occurred while creating the project").should( - "be.visible" + cy.getDataCy("sunset-banner").should( + "contain", + "Project creation no longer available" ); }); - - it("form validations", () => { - fixtures - .templates({ - urlSource: - "url=https%3A%2F%2Fgithub.com%2FSwissDataScienceCenter%2Frenku-project-template&ref=master", - }) - .getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }) - .getNamespace({ - namespace: "private-space", - name: "getPrivateNamespace", - fixture: "projects/namespace-129.json", - }); - cy.wait("@getTemplates"); - - // validate public visibility is disabled when namespace selected has internal visibility - cy.get("#namespace-input").click(); - cy.get("div").contains("internal-space").click(); - cy.getDataCy("visibility-public").should("be.disabled"); - - cy.getDataCy("refresh-namespace-list").click(); - // validate public and internal visibility are disabled when namespace selected has private visibility - cy.get("#namespace-input").click(); - cy.get("div").contains("private-space").click(); - cy.getDataCy("visibility-public").should("be.disabled"); - cy.getDataCy("visibility-internal").should("be.disabled"); - - // validate fetch custom templates - fixtures.templates({ - error: true, - urlSource: "url=invalid-url&ref=master", - name: "getCustomTemplates", - }); - cy.getDataCy("custom-source-button").click(); - cy.getDataCy("url-repository").type("invalid-url"); - cy.getDataCy("ref-repository").type("master"); - cy.getDataCy("fetch-templates-button").click(); - cy.wait("@getCustomTemplates"); - - // when invalid source info - cy.contains("Unable to fetch templates").should("be.visible"); - - //when valid source info - fixtures.templates({ - urlSource: "url=valid-url&ref=master", - name: "getCustomTemplatesValid", - }); - cy.getDataCy("url-repository").clear().type("valid-url"); - cy.getDataCy("fetch-templates-button").click(); - cy.wait("@getCustomTemplatesValid"); - cy.getDataCy("project-template-card").should("have.length", 6); - - // cannot submit the form if the title and template are missing - cy.getDataCy("create-project-button").click(); - cy.contains("Title is missing.").should("be.visible"); - cy.contains("Please select a template.").should("be.visible"); - - // after send to create project should show progress indicator and hide form - cy.createProject(newProjectTitle); - cy.contains("Creating Project...").should("be.visible"); - cy.contains( - "You'll be redirected to the new project page when the creation is completed." - ).should("be.visible"); - cy.getDataCy("create-project-form").should("not.exist"); - }); - - it("create a new project with an avatar", () => { - fixtures - .templates() - .createProject() - .project({ - name: "getNewProject", - projectPath: newProjectPath, - statistics: false, - }) - .updateProject({ projectPath: newProjectPath }) - .updateAvatar(); - cy.wait("@getTemplates"); - cy.get("#project-avatar-file-input-hidden").selectFile( - "cypress/fixtures/avatars/avatar.png", - { force: true } - ); - cy.createProject(newProjectTitle); - cy.wait("@createProject"); - cy.wait("@getNewProject"); - cy.wait("@updateProject").should((result) => { - const request = result.request.body; - expect(request).to.have.property("name"); - }); - cy.wait("@updateAvatar").should((result) => { - const request = result.request.body; - expect(request.byteLength).to.lessThan(200 * 1024 * 1024); - }); - cy.url().should("include", `projects/${newProjectPath}`); - }); -}); - -describe("Add new project shared link", () => { - beforeEach(() => { - fixtures.config().versions().userTest().namespaces(); - fixtures.projects().landingUserProjects(); - }); - - it("prefill values all values (custom template)", () => { - const customValues = - "?data=eyJ0aXRsZSI6Im5ldyBwcm9qZWN0IiwiZGVzY3JpcHRpb24iOiIgdGhpcyBhIGN1c3RvbSBkZXNjcml" + - "wdGlvbiIsIm5hbWVzcGFjZSI6ImUyZSIsInZpc2liaWxpdHkiOiJpbnRlcm5hbCIsInVybCI6Imh0dHBzOi8v" + - "Z2l0aHViLmNvbS9Td2lzc0RhdGFTY2llbmNlQ2VudGVyL3Jlbmt1LXByb2plY3QtdGVtcGxhdGUiLCJyZWYiO" + - "iJtYXN0ZXIiLCJ0ZW1wbGF0ZSI6IkN1c3RvbS9SLW1pbmltYWwifQ%3D%3D"; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("be.visible", { - timeout: 20_000, - }); - - // Check the prefill values - cy.getDataCy("field-group-title").should("contain.value", "new project"); - cy.getDataCy("field-group-description").should( - "contain.text", - "this a custom description" - ); - cy.getDataCy("project-slug").should("contain.value", "e2e/new-project"); - cy.getDataCy("visibility-public").should("not.be.checked"); - cy.getDataCy("visibility-internal").should("be.checked"); - cy.getDataCy("visibility-private").should("not.be.checked"); - cy.getDataCy("url-repository").should( - "contain.value", - "https://github.com/SwissDataScienceCenter/renku-project-template" - ); - cy.getDataCy("ref-repository").should("contain.value", "master"); - cy.getDataCy("project-template-input") - .filter(":checked") - .parent() - .find("[data-cy='project-template-card']") - .should("be.visible") - .and("contain.text", "Basic R (4.1.2) Project"); - }); - - it("prefill values custom template", () => { - const customValues = - "?data=eyJ0aXRsZSI6Im5ldyBwcm9qZWN0IiwiZGVzY3JpcHRpb24iOiIgdGhpcyBhIGN1c3RvbSBkZXNjcml" + - "wdGlvbiIsIm5hbWVzcGFjZSI6ImUyZSIsInZpc2liaWxpdHkiOiJpbnRlcm5hbCIsInVybCI6Imh0dHBzOi8v" + - "Z2l0aHViLmNvbS9Td2lzc0RhdGFTY2llbmNlQ2VudGVyL3Jlbmt1LXByb2plY3QtdGVtcGxhdGUiLCJyZWYiO" + - "iJtYXN0ZXIifQ%3D%3D"; - const templateUrl = - "https://github.com/SwissDataScienceCenter/renku-project-template"; - const templateRef = "master"; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("be.visible", { - timeout: 20_000, - }); - - // Check custom templates - cy.getDataCy("url-repository").should("contain.value", templateUrl); - cy.getDataCy("ref-repository").should("contain.value", templateRef); - }); - - it("prefill values renkuLab template", () => { - const customValues = - "?data=eyJ0ZW1wbGF0ZSI6IlJlbmt1L2p1bGlhLW1pbmltYWwifQ%3D%3D"; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("be.visible", { - timeout: 20_000, - }); - - // Check selected template - cy.getDataCy("project-template-input") - .filter(":checked") - .parent() - .find("[data-cy='project-template-card']") - .should("be.visible") - .and("contain.text", "Basic Julia (1.7.1) Project"); - }); - - it("use the target template and select the custom variables", () => { - const customValues = - "?data=eyJ1cmwiOiJodHRwczovL2dpdGh1Yi5jb20vb21uaWJlbmNobWFyay9jb250cmlidXRlZC1wcm9qZWN" + - "0LXRlbXBsYXRlcyIsInJlZiI6Im1haW4iLCJ0ZW1wbGF0ZSI6IkN1c3RvbS9vbW5pLWRhdGEtcHkiLCJ2YXJp" + - "YWJsZXMiOnsiYmVuY2htYXJrX25hbWUiOiJvbW5pX2NsdXN0ZXJpbmciLCJkYXRhc2V0X2tleXdvcmQiOiJ0Z" + - "XN0IHZhbHVlIiwibWV0YWRhdGFfZGVzY3JpcHRpb24iOiIiLCJwcm9qZWN0X3RpdGxlIjoiYW5vdGhlciByYW" + - "5kb20gdmFsdWUiLCJzdHVkeV9saW5rIjoiIiwic3R1ZHlfbm90ZSI6IiIsInN0dWR5X3Rpc3N1ZSI6IiJ9fQ"; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("be.visible", { - timeout: 20_000, - }); - - // Check selected template - cy.getDataCy("project-template-input") - .filter(":checked") - .parent() - .find("[data-cy='project-template-card']") - .should("be.visible") - .and("contain.text", "Omnibenchmark dataset"); - - cy.get("#parameter-benchmark_name").should("have.value", "omni_clustering"); - cy.get("#parameter-dataset_keyword").should("have.value", "test value"); - cy.get("#parameter-project_title").should( - "have.value", - "another random value" - ); - }); - - it("display warning on non-essential fields", () => { - const customValues = - "?data=eyJ0aXRsZSI6Im5ldyBwcm9qZWN0IiwibmFtZXNwYWNlIjoiZmFrZSJ9"; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-warning") - .should("be.visible", { - timeout: 20_000, - }) - .contains("button", "Show warnings") - .click(); - cy.getDataCy("project-creation-embedded-warning") - .contains(`The namespace "fake" is not available.`) - .should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("not.exist"); - - // Other valid fields should be filled in correctly - cy.getDataCy("field-group-title").should("contain.value", "new project"); - }); - - it("display errors on essential fields", () => { - const customValues = - "?data=eyJ0aXRsZSI6Im5ldyBwcm9qZWN0IiwidGVtcGxhdGUiOiJmYWtlIn0="; - fixtures.templates().getNamespace({ - namespace: "internal-space", - name: "getInternalNamespace", - }); - cy.visit(`/v1/projects/new${customValues}`); - cy.wait("@getTemplates"); - - // Check feedback messages - cy.getDataCy("project-creation-embedded-fetching").should("be.visible"); - cy.getDataCy("project-creation-embedded-error") - .should("be.visible", { - timeout: 20_000, - }) - .contains("button", "Show error") - .click(); - cy.getDataCy("project-creation-embedded-error") - .contains(`The template "fake" is not available.`) - .should("be.visible"); - cy.getDataCy("project-creation-embedded-info").should("not.exist"); - - // Other valid fields should be filled in correctly - cy.getDataCy("field-group-title").should("contain.value", "new project"); - }); }); diff --git a/tests/cypress/e2e/projectDatasets.spec.ts b/tests/cypress/e2e/projectDatasets.spec.ts index f18db0937a..c8ee1fb4c1 100644 --- a/tests/cypress/e2e/projectDatasets.spec.ts +++ b/tests/cypress/e2e/projectDatasets.spec.ts @@ -105,6 +105,8 @@ describe("Project dataset", () => { it("displays project datasets", () => { cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -121,6 +123,8 @@ describe("Project dataset", () => { fixtures.getFiles().uploadDatasetFile().addFileDataset().editDataset(); cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -196,6 +200,8 @@ describe("Project dataset", () => { }); cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -230,6 +236,8 @@ describe("Project dataset", () => { it("delete project dataset", () => { cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -280,6 +288,8 @@ describe("Project dataset", () => { fixtures.invalidDataset({ id: datasetIdentifier }); fixtures.getFiles(); cy.visit(`projects/${projectPath}/datasets/${datasetName}`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList"); cy.wait("@invalidDataset"); @@ -305,6 +315,8 @@ describe("Project dataset (legacy ids)", () => { it("displays legacy project datasets", () => { cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -340,6 +352,8 @@ describe("Error loading datasets", () => { it("display project datasets", () => { cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@datasetList") .its("response.body") @@ -365,6 +379,8 @@ describe("Migration check errors", () => { it("display project datasets", () => { fixtures.projectMigrationError({ errorNumber: 2200, queryUrl: "*" }); cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@getMigration"); cy.get("div.alert-danger") @@ -389,6 +405,8 @@ describe("Project dataset (locked)", () => { it("display project datasets", () => { cy.visit(`projects/${projectPath}/datasets`); + cy.wait("@getConfig"); + cy.wait("@getUser"); cy.wait("@getProject"); cy.wait("@getProjectLockStatus"); cy.wait("@datasetList") From a9b3f128c5a467f3db5b88bc877f665fa866dd62 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba <dandrea.cordoba@gmail.com> Date: Thu, 19 Jun 2025 14:08:38 +0200 Subject: [PATCH 4/4] fix e2e lint --- tests/cypress/e2e/newProject.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/cypress/e2e/newProject.spec.ts b/tests/cypress/e2e/newProject.spec.ts index 11baf515f6..92ff19fa79 100644 --- a/tests/cypress/e2e/newProject.spec.ts +++ b/tests/cypress/e2e/newProject.spec.ts @@ -19,7 +19,6 @@ import fixtures from "../support/renkulab-fixtures"; describe("Add new project", () => { - const newProjectTitle = "new project"; const slug = "new-project"; const newProjectPath = `e2e/${slug}`;