+
+ Creating new datasets is no longer supported in Renku Legacy. Switch to
+ Renku 2.0 to continue creating and managing your work.
+
) : null;
const projectDropdown = (
-
-
- Project
-
+
+
Project
+
+ 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/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/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 (
@@ -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 (
-
-
- 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{" "}
- window.location.reload()}
- >
- refresh the page
- {" "}
- 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 (
-
-
- );
-}
-
-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 = (
-
- );
- // 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 (
-
- );
-};
-
-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 (
-
- );
- }
-
- 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 = (
-
- );
-
- const onProgress = isFormProcessingOrFinished(meta);
- const creation = (
-
- );
- if (onProgress) return creation;
-
- return !importingDataset ? (
-
- {creation}
-
- {form}
-
- ) : (
- 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 = (
-
-
An error occurred while creating the project.
-
{error}
-
- );
- } else if (creation.projectUpdating) {
- message = "Updating project metadata...";
- } else if (creation.projectError) {
- color = "warning";
- message = (
-
-
- An error occurred while updating project metadata (name or
- visibility). Please, adjust it on GitLab if needed.
-
-
Error details: {creation.projectError}
- handlers.goToProject()}>
- Go to the project
-
-
- );
- } else if (creation.kgUpdating) {
- message = "Activating project indexing...";
- } else if (creation.kgError) {
- color = "warning";
- message = (
-
-
- An error occurred while activating the project indexing. You can
- activate it later to get the lineage.
-
-
Error details: {creation.kgError}
- handlers.goToProject()}>
- Go to the project
-
-
- );
- } else {
- return null;
- }
-
- if (color === "warning") return {message};
-
- if (color === "danger") return {message};
-
- // 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 (
-
-
-
- );
- }
-}
-
-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 = (
-
-);
-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 ;
- return null;
- }
-
- // Show a feedback when the automated part has finished
- // Case 1: errors
- if (automated.error) {
- const error = (
-
- {automated.error}
-
- );
- return (
-
-
-
- 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).
-
-
- We used the default settings instead, but that might not lead to the
- expected results.
-
- 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.
-
-
- We used the default settings instead, but that might not lead to the
- expected results.
-
-
-
-
-
- );
-}
-
-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 (
- 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 (
-
- refresh()}
- disabled={disabled}
- >
-
-
-
- {tip}
-
-
- );
-}
-
-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 {suggestion.full_path};
- };
-
- renderSectionTitle(suggestion) {
- return {suggestion.kind};
- }
-
- 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 (
- 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 ? (
-
- ) : null;
- // loading or autosuggest
- const main = namespaces.fetching ? (
-
-
-
- ) : (
- <>
-
- {info}
- >
- );
-
- return (
-
-
- {refreshButton}
- {main}
-
- );
- }
-}
-
-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 : "";
- const title = input.title ? slugFromTitle(input.title, true) : "";
- const slug = `${namespace}/${title}`;
-
- return (
-
-
-
-
-
- );
-};
-
-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;
- };
- importingDataset: boolean;
- input: NewProjectInputs;
- meta: NewProjectMeta;
-}
-
-function ImportSubmitFormButton({
- handlers,
-}: Pick) {
- return (
- <>
-
- >
- );
-}
-
-function SubmitFormButton({
- createDataAvailable,
- handlers,
- input,
- importingDataset,
- meta,
-}: SubmitFormButtonProps) {
- if (importingDataset) {
- return ;
- }
- return (
-
- );
-}
-
-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 (
-
-
-
- );
-};
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 (
-
-
-
-
- handlers.setProperty("userRepo", false)}
- >
- RenkuLab
-
- handlers.setProperty("userRepo", true)}
- >
- Custom
-
-
-
- );
-};
-
-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 (
-