From 962e779611a092faa9a4b03026cbd303971470ed Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 27 Oct 2023 19:56:25 +0200 Subject: [PATCH] feat: (WIP) flow for creating a renku-native project (#2870)(#2875) --- client/src/App.js | 9 + .../features/projectsV2/new/ProjectV2New.tsx | 81 ++++++ .../projectsV2/new/ProjectV2NewForm.tsx | 253 ++++++++++++++++++ .../projectsV2/new/projectV2New.module.scss | 3 + .../projectsV2/new/projectV2New.slice.ts | 68 +++++ .../features/projectsV2/projectV2.types.ts | 16 ++ client/src/utils/helpers/EnhancedState.ts | 2 + client/src/utils/helpers/url/Url.js | 4 + tests/cypress/e2e/projectV2.spec.ts | 44 +++ 9 files changed, 480 insertions(+) create mode 100644 client/src/features/projectsV2/new/ProjectV2New.tsx create mode 100644 client/src/features/projectsV2/new/ProjectV2NewForm.tsx create mode 100644 client/src/features/projectsV2/new/projectV2New.module.scss create mode 100644 client/src/features/projectsV2/new/projectV2New.slice.ts create mode 100644 client/src/features/projectsV2/projectV2.types.ts create mode 100644 tests/cypress/e2e/projectV2.spec.ts diff --git a/client/src/App.js b/client/src/App.js index eebccc6943..3e40bd8c1a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -50,6 +50,7 @@ import { Cookie, Privacy } from "./privacy"; import { Project } from "./project"; import { ProjectList } from "./project/list"; import { NewProject } from "./project/new"; +import ProjectV2New from "./features/projectsV2/new/ProjectV2New"; import { StyleGuide } from "./styleguide"; import AppContext from "./utils/context/appContext"; import { Url } from "./utils/helpers/url"; @@ -275,6 +276,14 @@ function CentralContentContainer(props) { )} /> + ( + + + + )} + /> ( diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx new file mode 100644 index 0000000000..9c2cf93d8c --- /dev/null +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -0,0 +1,81 @@ +/*! + * Copyright 2023 - 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 FormSchema from "../../../components/formschema/FormSchema"; +import type { NewProjectV2State } from "./projectV2New.slice"; +import { useNewProjectV2Selector } from "./projectV2New.slice"; +import ProjectV2NewForm from "./ProjectV2NewForm"; + +function ProjectV2NewAccessStepHeader() { + return ( + <> + Set up visibility and access +

Decide who can see your project and who is allowed to work in it.

+ + ); +} + +function ProjectV2NewHeader({ + currentStep, +}: Pick) { + return ( + <> +

+ V2 Projects let you group together related resources and control who can + access them. +

+ {currentStep === 0 && } + {currentStep === 1 && } + {currentStep === 2 && } + + ); +} + +function ProjectV2NewMetadataStepHeader() { + return ( + <> + Describe your project +

Provide some information to explain what your project is about.

+ + ); +} + +function ProjectV2NewRepositoryStepHeader() { + return ( + <> + Associate some repositories (optional) +

+ You can associate one or more repositories with the project now if you + want. This can also be done later at any time. +

+ + ); +} + +export default function ProjectV2New() { + const { currentStep } = useNewProjectV2Selector((state) => state); + return ( + } + > + + + ); +} diff --git a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx new file mode 100644 index 0000000000..a7ebf22fdc --- /dev/null +++ b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx @@ -0,0 +1,253 @@ +/*! + * Copyright 2023 - 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 cx from "classnames"; +import { useCallback } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useDispatch } from "react-redux"; + +import { Button, Form, FormText, Input, Label } from "reactstrap"; + +import styles from "./projectV2New.module.scss"; +import type { NewProjectV2State } from "./projectV2New.slice"; +import { + setAccess, + setCurrentStep, + setMetadata, + useNewProjectV2Selector, +} from "./projectV2New.slice"; + +function ProjectFormSubmitGroup({ currentStep }: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + + const previousStep = useCallback(() => { + if (currentStep < 1) return; + const previousStep = (currentStep - 1) as typeof currentStep; + dispatch(setCurrentStep(previousStep)); + }, [currentStep, dispatch]); + + return ( +
+ + {currentStep < 2 && } + {currentStep === 2 && } +
+ ); +} + +interface ProjectV2NewFormProps { + currentStep: NewProjectV2State["currentStep"]; +} +export default function ProjectV2NewForm({ + currentStep, +}: ProjectV2NewFormProps) { + return ( +
+ {currentStep === 0 &&

Describe the project

} + {currentStep === 1 &&

Define access

} + {currentStep === 2 &&

Add repositories

} + {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} +
+ ); +} + +function ProjectV2NewMetadataStepForm({ currentStep }: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + const { project } = useNewProjectV2Selector((state) => state); + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: project.metadata, + }); + + const onSubmit = useCallback( + (data: NewProjectV2State["project"]["metadata"]) => { + dispatch(setMetadata(data)); + if (currentStep > 1) return; + const nextStep = (currentStep + 1) as typeof currentStep; + dispatch(setCurrentStep(nextStep)); + }, + [currentStep, dispatch] + ); + return ( +
+
+ + ( + + )} + rules={{ required: true }} + /> + {errors.name ? null : ( + + The name you will use to refer to the project + + )} +
Please provide a name
+
+ +
+ + ( + + )} + rules={{ required: true }} + /> + {errors.slug ? null : ( + + A short, machine-readable identifier for the project + + )} +
Please provide a slug
+
+ +
+ + ( + + )} + rules={{ maxLength: 500, required: false }} + /> + {errors.description ? null : ( + + A brief (at most 500 character) description of the project. + + )} +
Please provide a description
+
+ + + ); +} + +function ProjectV2NewAccessStepForm({ currentStep }: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + const { project } = useNewProjectV2Selector((state) => state); + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: project.access, + }); + + const onSubmit = useCallback( + (data: NewProjectV2State["project"]["access"]) => { + dispatch(setAccess(data)); + if (currentStep > 1) return; + const nextStep = (currentStep + 1) as typeof currentStep; + dispatch(setCurrentStep(nextStep)); + }, + [currentStep, dispatch] + ); + return ( +
+
+ + ( + + + + + )} + rules={{ required: true }} + /> + {errors.visibility ? null : ( + + Should the project be visible to everyone or only to members? + + )} +
Please select a visibility
+
+
+ + + + Who has access to the project? + +
Please select a visibility
+
+ + + ); +} diff --git a/client/src/features/projectsV2/new/projectV2New.module.scss b/client/src/features/projectsV2/new/projectV2New.module.scss new file mode 100644 index 0000000000..976d2913d4 --- /dev/null +++ b/client/src/features/projectsV2/new/projectV2New.module.scss @@ -0,0 +1,3 @@ +.projectV2NewForm { + width: 100%; +} diff --git a/client/src/features/projectsV2/new/projectV2New.slice.ts b/client/src/features/projectsV2/new/projectV2New.slice.ts new file mode 100644 index 0000000000..e9c4035c99 --- /dev/null +++ b/client/src/features/projectsV2/new/projectV2New.slice.ts @@ -0,0 +1,68 @@ +/*! + * Copyright 2023 - 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 { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +import { createSliceSelector } from "../../../utils/customHooks/UseSliceSelector"; +import type { Project } from "../projectV2.types"; + +type NewProjectV2Step = 0 | 1 | 2; + +export interface NewProjectV2State { + project: Project; + currentStep: NewProjectV2Step; +} + +const initialState: NewProjectV2State = { + project: { + access: { + users: [], + visibility: "private", + }, + content: { + repositories: [], + }, + metadata: { + name: "", + slug: "", + description: "", + }, + }, + currentStep: 0, +}; + +export const projectV2NewSlice = createSlice({ + name: "newProjectV2", + initialState, + reducers: { + setAccess: (state, action: PayloadAction) => { + state.project.access = action.payload; + }, + setCurrentStep: (state, action: PayloadAction) => { + state.currentStep = action.payload; + }, + setMetadata: (state, action: PayloadAction) => { + state.project.metadata = action.payload; + }, + reset: () => initialState, + }, +}); + +export const { setAccess, setCurrentStep, setMetadata, reset } = + projectV2NewSlice.actions; +export const useNewProjectV2Selector = createSliceSelector(projectV2NewSlice); diff --git a/client/src/features/projectsV2/projectV2.types.ts b/client/src/features/projectsV2/projectV2.types.ts new file mode 100644 index 0000000000..0c9cfd7af3 --- /dev/null +++ b/client/src/features/projectsV2/projectV2.types.ts @@ -0,0 +1,16 @@ +export type ProjectVisibility = "private" | "public"; + +export interface Project { + access: { + users: string[]; + visibility: ProjectVisibility; + }; + content: { + repositories: string[]; + }; + metadata: { + name: string; + slug: string; + description: string; + }; +} diff --git a/client/src/utils/helpers/EnhancedState.ts b/client/src/utils/helpers/EnhancedState.ts index 548ce9e32b..4cb5e7c66b 100644 --- a/client/src/utils/helpers/EnhancedState.ts +++ b/client/src/utils/helpers/EnhancedState.ts @@ -48,6 +48,7 @@ import sessionsApi from "../../features/session/sessions.api"; import { sessionSidecarApi } from "../../features/session/sidecarApi"; import startSessionSlice from "../../features/session/startSession.slice"; import { startSessionOptionsSlice } from "../../features/session/startSessionOptionsSlice"; +import { projectV2NewSlice } from "../../features/projectsV2/new/projectV2New.slice"; import keycloakUserApi from "../../features/user/keycloakUser.api"; import { versionsApi } from "../../features/versions/versionsApi"; import { workflowsApi } from "../../features/workflows/WorkflowsApi"; @@ -67,6 +68,7 @@ export const createStore = ( [kgInactiveProjectsSlice.name]: kgInactiveProjectsSlice.reducer, [startSessionSlice.name]: startSessionSlice.reducer, [startSessionOptionsSlice.name]: startSessionOptionsSlice.reducer, + [projectV2NewSlice.name]: projectV2NewSlice.reducer, [workflowsSlice.name]: workflowsSlice.reducer, // APIs [adminComputeResourcesApi.reducerPath]: adminComputeResourcesApi.reducer, diff --git a/client/src/utils/helpers/url/Url.js b/client/src/utils/helpers/url/Url.js index 8484f8707c..14cddd5a6c 100644 --- a/client/src/utils/helpers/url/Url.js +++ b/client/src/utils/helpers/url/Url.js @@ -483,6 +483,10 @@ const Url = { ), }, }, + projectV2: { + base: "/projectV2", + new: "/projectV2/new", + }, sessions: { base: "/sessions", }, diff --git a/tests/cypress/e2e/projectV2.spec.ts b/tests/cypress/e2e/projectV2.spec.ts new file mode 100644 index 0000000000..801971cf49 --- /dev/null +++ b/tests/cypress/e2e/projectV2.spec.ts @@ -0,0 +1,44 @@ +/*! + * Copyright 2023 - 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 Fixtures from "../support/renkulab-fixtures"; + +describe("Add new v2 project", () => { + const fixtures = new Fixtures(cy); + // const newProjectTitle = "new space"; + const slug = "new-project"; + const newProjectPath = `e2e/${slug}`; + + beforeEach(() => { + fixtures.config().versions().userTest().namespaces(); + fixtures.projects().landingUserProjects("getLandingUserProjects"); + cy.visit("projectV2/new"); + }); + + it("create a new project that should change name", () => { + fixtures + .templates() + .createProject() + .project(newProjectPath, "getNewProject", "projects/project.json", false) + .updateProject(newProjectPath); + cy.contains("New Project (V2)").should("be.visible"); + // cy.createProject(newProjectTitle); + // cy.wait("@getTemplates"); + //cy.wait("@createProject"); + }); +});